diff --git a/.changeset/offense-end-at-eof.md b/.changeset/offense-end-at-eof.md new file mode 100644 index 000000000..c918a04e6 --- /dev/null +++ b/.changeset/offense-end-at-eof.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-common': patch +--- + +Fix the line and character of an offense that ends at the end of a file. They were one character short, or on the previous line when the file ends with a newline. diff --git a/packages/theme-check-common/src/utils/position.spec.ts b/packages/theme-check-common/src/utils/position.spec.ts new file mode 100644 index 000000000..7e593c8bb --- /dev/null +++ b/packages/theme-check-common/src/utils/position.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { getPosition } from './position'; + +describe('Unit: getPosition', () => { + it('returns the line and character of an index', () => { + expect(getPosition('a\nbc', 3)).toEqual({ index: 3, line: 1, character: 1 }); + }); + + it('returns the position of the end of the file', () => { + expect(getPosition('a\nbc', 4)).toEqual({ index: 4, line: 1, character: 2 }); + expect(getPosition('a\n', 2)).toEqual({ index: 2, line: 1, character: 0 }); + expect(getPosition('', 0)).toEqual({ index: 0, line: 0, character: 0 }); + expect(getPosition('a\nbc', 9)).toEqual({ index: 9, line: 1, character: 2 }); + }); + + it('ends a line at the \\n of a Windows line break', () => { + expect(getPosition('a\r\nbc', 2)).toEqual({ index: 2, line: 0, character: 2 }); + expect(getPosition('a\r\nbc', 4)).toEqual({ index: 4, line: 1, character: 1 }); + expect(getPosition('a\r\n', 3)).toEqual({ index: 3, line: 1, character: 0 }); + }); + + it('returns -1 for a negative index', () => { + expect(getPosition('a\nbc', -1)).toEqual({ index: -1, line: -1, character: -1 }); + }); +}); diff --git a/packages/theme-check-common/src/utils/position.ts b/packages/theme-check-common/src/utils/position.ts index f9e234c77..731f5aa2c 100644 --- a/packages/theme-check-common/src/utils/position.ts +++ b/packages/theme-check-common/src/utils/position.ts @@ -1,14 +1,23 @@ import lineColumn from 'line-column'; import { Position } from '../types'; +/** + * The line and character of an index, both 0-based. An index past the end of + * the source is at its end, and a negative one, like the end of an unclosed + * block, has line and character -1. + */ export function getPosition(source: string, index: number): Position { - const lineCol = lineColumn(source, { origin: 0 }).fromIndex(Math.min(index, source.length - 1)); - - return { - index, - line: lineCol ? lineCol.line : -1, - character: lineCol ? lineCol.col : -1, - }; + if (index < 0) return { index, line: -1, character: -1 }; + // Not lineColumn's fromIndex, which has no position for the end of the file, + // and splits the whole source on every call. + const end = Math.min(index, source.length); + let line = 0; + let lineStart = 0; + for (let i = source.indexOf('\n'); i !== -1 && i < end; i = source.indexOf('\n', i + 1)) { + line++; + lineStart = i + 1; + } + return { index, line, character: end - lineStart }; } export function getOffset(source: string, line: number, column: number): number {