From ac1bc1c2615184128ecad07e44079213b4ecc7da Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 16 Oct 2024 17:27:19 +0200 Subject: [PATCH] fix: no-missing-label-refs should not crash on undefined labels (#290) * fix: no-missing-label-refs should not crash on undefined labels Fixes #289 * fix locations * add a note to function `findOffsets` * add a comment for the loop * add a comment for the regex --- src/rules/no-missing-label-refs.js | 109 +++++-------- src/util.js | 3 + tests/rules/no-missing-label-refs.test.js | 182 ++++++++++++++++++++++ 3 files changed, 225 insertions(+), 69 deletions(-) diff --git a/src/rules/no-missing-label-refs.js b/src/rules/no-missing-label-refs.js index fbc66053..d65afc34 100644 --- a/src/rules/no-missing-label-refs.js +++ b/src/rules/no-missing-label-refs.js @@ -21,106 +21,77 @@ import { findOffsets, illegalShorthandTailPattern } from "../util.js"; // Helpers //----------------------------------------------------------------------------- -const labelPatterns = [ - // [foo][bar] - /\]\[([^\]]+)\]/u, - - // [foo][] - /(\]\[\])/u, - - // [foo] - /\[([^\]]+)\]/u, -]; - -const shorthandTailPattern = /\]\[\]$/u; - /** * Finds missing references in a node. * @param {TextNode} node The node to check. - * @param {string} docText The text of the node. + * @param {string} nodeText The text of the node. * @returns {Array<{label:string,position:Position}>} The missing references. */ -function findMissingReferences(node, docText) { +function findMissingReferences(node, nodeText) { const missing = []; - let startIndex = 0; - const offset = node.position.start.offset; const nodeStartLine = node.position.start.line; const nodeStartColumn = node.position.start.column; /* - * This loop works by searching the string inside the node for the next - * label reference. If there is, it reports an error. - * It then moves the start index to the end of the label reference and - * continues searching the text until the end of the text is found. + * Matches substrings like "[foo]", "[]", "[foo][bar]", "[foo][]", "[][bar]", or "[][]". + * `left` is the content between the first brackets. It can be empty. + * `right` is the content between the second brackets. It can be empty, and it can be undefined. */ - while (startIndex < node.value.length) { - const value = node.value.slice(startIndex); + const labelPattern = /\[(?[^\]]*)\](?:\[(?[^\]]*)\])?/dgu; - const match = labelPatterns.reduce((previous, pattern) => { - if (previous) { - return previous; - } - - return value.match(pattern); - }, null); - - // check for array instead of null to appease TypeScript - if (!Array.isArray(match)) { - break; - } + let match; + /* + * This loop searches the text inside the node for sequences that + * look like label references and reports an error for each one found. + */ + while ((match = labelPattern.exec(nodeText))) { // skip illegal shorthand tail -- handled by no-invalid-label-refs if (illegalShorthandTailPattern.test(match[0])) { - startIndex += match.index + match[0].length; continue; } - // Calculate the match index relative to just the node. - let columnStart = startIndex + match.index; - let label = match[1]; - - // need to look backward to get the label - if (shorthandTailPattern.test(match[0])) { - // adding 1 to the index just in case we're in a ![] and need to skip the !. - const startFrom = offset + startIndex + 1; - const lastOpenBracket = docText.lastIndexOf("[", startFrom); - - if (lastOpenBracket === -1) { - startIndex += match.index + match[0].length; - continue; - } - - label = docText - .slice(lastOpenBracket, match.index + match[0].length) - .match(/!?\[([^\]]+)\]/u)?.[1]; - columnStart -= label.length; - } else if (match[0].startsWith("]")) { - columnStart += 2; + const { left, right } = match.groups; + + // `[][]` or `[]` + if (!left && !right) { + continue; + } + + let label, labelIndices; + + if (right) { + label = right; + labelIndices = match.indices.groups.right; } else { - columnStart += 1; + label = left; + labelIndices = match.indices.groups.left; } const { lineOffset: startLineOffset, columnOffset: startColumnOffset } = - findOffsets(node.value, columnStart); - - const startLine = nodeStartLine + startLineOffset; - const startColumn = nodeStartColumn + startColumnOffset; + findOffsets(nodeText, labelIndices[0]); + const { lineOffset: endLineOffset, columnOffset: endColumnOffset } = + findOffsets(nodeText, labelIndices[1]); missing.push({ label: label.trim(), position: { start: { - line: startLine, - column: startColumn, + line: nodeStartLine + startLineOffset, + column: + startLineOffset > 0 + ? startColumnOffset + 1 + : nodeStartColumn + startColumnOffset, }, end: { - line: startLine, - column: startColumn + label.length, + line: nodeStartLine + endLineOffset, + column: + endLineOffset > 0 + ? endColumnOffset + 1 + : nodeStartColumn + endColumnOffset, }, }, }); - - startIndex += match.index + match[0].length; } return missing; @@ -164,7 +135,7 @@ export default { text(node) { allMissingReferences.push( - ...findMissingReferences(node, sourceCode.text), + ...findMissingReferences(node, sourceCode.getText(node)), ); }, diff --git a/src/util.js b/src/util.js index 3648d964..38b31a9e 100644 --- a/src/util.js +++ b/src/util.js @@ -15,6 +15,9 @@ export const illegalShorthandTailPattern = /\]\[\s+\]$/u; * @param {string} text The text to search. * @param {number} offset The offset to find. * @returns {{lineOffset:number,columnOffset:number}} The location of the offset. + * Note that `columnOffset` should be used as an offset to the column number + * of the given text in the source code only when `lineOffset` is 0. + * Otherwise, it should be used as a 0-based column number in the source code. */ export function findOffsets(text, offset) { let lineOffset = 0; diff --git a/tests/rules/no-missing-label-refs.test.js b/tests/rules/no-missing-label-refs.test.js index 5c221aae..fa06cb33 100644 --- a/tests/rules/no-missing-label-refs.test.js +++ b/tests/rules/no-missing-label-refs.test.js @@ -35,6 +35,15 @@ ruleTester.run("no-missing-label-refs", rule, { "[ foo ][]\n\n[foo]: http://bar.com/image.jpg", "[foo][ ]\n\n[foo]: http://bar.com/image.jpg", "[\nfoo\n][\n]\n\n[foo]: http://bar.com/image.jpg", + "[]", + "][]", + "[][]", + "[] []", + "[foo", + "foo]", + "foo][bar]\n\n[bar]: http://bar.com", + "foo][bar][baz]\n\n[baz]: http://baz.com", + "[][foo]\n\n[foo]: http://foo.com", ], invalid: [ { @@ -149,5 +158,178 @@ ruleTester.run("no-missing-label-refs", rule, { }, ], }, + { + code: "foo][bar]\n\n[baz]: http://baz.com", + errors: [ + { + messageId: "notFound", + data: { label: "bar" }, + line: 1, + column: 6, + endLine: 1, + endColumn: 9, + }, + ], + }, + { + code: "foo][bar][baz]\n\n[bar]: http://bar.com", + errors: [ + { + messageId: "notFound", + data: { label: "baz" }, + line: 1, + column: 11, + endLine: 1, + endColumn: 14, + }, + ], + }, + { + code: "[foo]\n[foo][bar]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + { + messageId: "notFound", + data: { label: "bar" }, + line: 2, + column: 7, + endLine: 2, + endColumn: 10, + }, + ], + }, + { + code: "[Foo][foo]\n[Bar][]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 10, + }, + { + messageId: "notFound", + data: { label: "Bar" }, + line: 2, + column: 2, + endLine: 2, + endColumn: 5, + }, + ], + }, + { + code: "[Foo][]\n[Bar][]", + errors: [ + { + messageId: "notFound", + data: { label: "Foo" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + { + messageId: "notFound", + data: { label: "Bar" }, + line: 2, + column: 2, + endLine: 2, + endColumn: 5, + }, + ], + }, + { + code: "[Foo][foo]\n[Bar][bar]\n[Hoge][]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 10, + }, + { + messageId: "notFound", + data: { label: "bar" }, + line: 2, + column: 7, + endLine: 2, + endColumn: 10, + }, + { + messageId: "notFound", + data: { label: "Hoge" }, + line: 3, + column: 2, + endLine: 3, + endColumn: 6, + }, + ], + }, + { + code: "[Foo][]\n[Bar][bar]\n[Hoge][hoge]", + errors: [ + { + messageId: "notFound", + data: { label: "Foo" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + { + messageId: "notFound", + data: { label: "bar" }, + line: 2, + column: 7, + endLine: 2, + endColumn: 10, + }, + { + messageId: "notFound", + data: { label: "hoge" }, + line: 3, + column: 8, + endLine: 3, + endColumn: 12, + }, + ], + }, + { + code: "[][foo]", + errors: [ + { + messageId: "notFound", + data: { label: "foo" }, + line: 1, + column: 4, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: " foo\n [bar]", + errors: [ + { + messageId: "notFound", + data: { label: "bar" }, + line: 2, + column: 4, + endLine: 2, + endColumn: 7, + }, + ], + }, ], });