Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 12 additions & 35 deletions src/rules/no-invalid-label-refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// Imports
//-----------------------------------------------------------------------------

import { findOffsets, illegalShorthandTailPattern } from "../util.js";
import { illegalShorthandTailPattern } from "../util.js";

//-----------------------------------------------------------------------------
// Type Definitions
Expand All @@ -17,33 +17,30 @@ import { findOffsets, illegalShorthandTailPattern } from "../util.js";
* @import { Position } from "unist";
* @import { Text } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @import { MarkdownSourceCode } from "../language/markdown-source-code.js";
* @typedef {"invalidLabelRef"} NoInvalidLabelRefsMessageIds
* @typedef {[]} NoInvalidLabelRefsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoInvalidLabelRefsOptions, MessageIds: NoInvalidLabelRefsMessageIds }>} NoInvalidLabelRefsRuleDefinition
* @typedef {Parameters<MarkdownRuleDefinition['create']>[0]['sourceCode']} SourceCode
*/

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

// matches i.e., [foo][bar]
/** matches i.e., `[foo][bar]` */
const labelPattern = /\]\[([^\]]+)\]/u;

/**
* Finds missing references in a node.
* @param {Text} node The node to check.
* @param {SourceCode} sourceCode The Markdown source code object.
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
* @returns {Array<{label:string,position:Position}>} The missing references.
*/
function findInvalidLabelReferences(node, sourceCode) {
const nodeText = sourceCode.getText(node);
const docText = sourceCode.text;
const invalid = [];
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
Expand All @@ -66,16 +63,16 @@ function findInvalidLabelReferences(node, sourceCode) {
}

/*
* Calculate the match index relative to just the node and
* to the entire document text.
* Adjust `labelPattern` match index to the full source code.
*/
const nodeMatchIndex = startIndex + match.index;
const docMatchIndex = offset + nodeMatchIndex;
const startOffset =
startIndex + match.index + node.position.start.offset;
const endOffset = startOffset + match[0].length;

/*
* Search the entire document text to find the preceding open bracket.
*/
const lastOpenBracketIndex = docText.lastIndexOf("[", docMatchIndex);
const lastOpenBracketIndex = docText.lastIndexOf("[", startOffset);

if (lastOpenBracketIndex === -1) {
startIndex += match.index + match[0].length;
Expand All @@ -87,34 +84,14 @@ function findInvalidLabelReferences(node, sourceCode) {
* take that into account when calculating the line and column offsets.
*/
const label = docText
.slice(lastOpenBracketIndex, docMatchIndex + match[0].length)
.slice(lastOpenBracketIndex, endOffset)
.match(/!?\[([^\]]+)\]/u)[1];

// find location of [ in the document text
const { lineOffset: startLineOffset, columnOffset: startColumnOffset } =
findOffsets(nodeText, nodeMatchIndex + 1);

// find location of [ in the document text
const { lineOffset: endLineOffset, columnOffset: endColumnOffset } =
findOffsets(nodeText, nodeMatchIndex + match[0].length);

const startLine = nodeStartLine + startLineOffset;
const startColumn = nodeStartColumn + startColumnOffset;
const endLine = nodeStartLine + endLineOffset;
const endColumn =
(endLine === startLine ? nodeStartColumn : 1) + endColumnOffset;

invalid.push({
label: label.trim(),
position: {
start: {
line: startLine,
column: startColumn,
},
end: {
line: endLine,
column: endColumn,
},
start: sourceCode.getLocFromIndex(startOffset + 1),
end: sourceCode.getLocFromIndex(endOffset),
},
});

Expand Down
14 changes: 14 additions & 0 deletions tests/rules/no-invalid-label-refs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ruleTester.run("no-invalid-label-refs", rule, {
"![foo][]\n\n[foo]: http://bar.com/image.jpg",
"[ foo ][]\n\n[foo]: http://bar.com/image.jpg",
"[eslint][\n\n]",
"[*eslint*][]\n\n[*eslint*]: http://bar.com",
Copy link
Member Author

@lumirlumir lumirlumir Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using strong or emphasis inside a label is an easy-to-forget edge case and often leads to unintended side effects when the check is missed, so I've added one more test.

This is a valid label according to the AST:

image

],
invalid: [
{
Expand Down Expand Up @@ -206,5 +207,18 @@ ruleTester.run("no-invalid-label-refs", rule, {
},
],
},
{
code: "[*eslint*][ ]",
errors: [
{
messageId: "invalidLabelRef",
data: { label: "*eslint*" },
line: 1,
column: 11,
endLine: 1,
endColumn: 14,
},
],
},
],
});