Skip to content
Open
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
20 changes: 19 additions & 1 deletion src/rules/no-bare-urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,25 @@ export default /** @satisfies {NoBareUrlsRuleDefinition} */ ({
node: linkNode,
messageId: "bareUrl",
fix(fixer) {
return fixer.replaceText(linkNode, `<${text}>`);
let replacementText = `<${text}>`;
// GFM parses `www` autolinks with an `http://` URL.
if (url === `http://${text}`) {
const escapedLinkText = text.replace(
/[[\]\\*_~`]/gu,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we move the two regular expressions to top-level helpers? That way, we wouldn’t have to create RegExp objects every time the fix is performed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the $ character needs to be added to this regex, as it could result in an incorrect auto-fix when the math option is enabled. Adding a regression test for this case would also be helpful.

http://www.example.com/$a$b$c

"\\$&",
);
const escapedLinkDestination = url.replace(
/[\\()]/gu,
"\\$&",
);
Comment on lines +174 to +181

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The current escaping breaks www autolinks containing an escaped pipe when they occur inside a GFM table cell.

For example:

| URL |
| --- |
| www.example.com/a\|b |

The current fixer produces:

| [www.example.com/a\\|b](http://www.example.com/a\\|b) |

Because the pipe is now preceded by an even number of backslashes, it is interpreted as a table-cell delimiter. This changes the table structure and can cause Custom getRange() method must be implemented in the subclass on the next autofix pass.

Could we escape | in both the link text and the destination, and add a regression test for a www autolink inside a table cell?

Comment on lines +174 to +181

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

& also needs to be escaped in both the link text and the destination.

For example, the GFM autolink:

www.example.com/a&amp;b

currently becomes:

[www.example.com/a&amp;b](http://www.example.com/a&amp;b)

When the fixed output is parsed again, CommonMark interprets &amp; as a character reference. As a result, both the displayed text and the destination change from the literal &amp; to &, silently changing the original link.

Escaping the ampersand as \&amp; prevents the character reference from being decoded. Could we include & in both replacement patterns and add tests for named and numeric character references?


replacementText = `[${escapedLinkText}](${escapedLinkDestination})`;
}

return fixer.replaceText(
linkNode,
replacementText,
);
},
});
}
Expand Down
104 changes: 104 additions & 0 deletions tests/rules/no-bare-urls.test.js

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

GFM allows any character other than whitespace and < after the domain, and Example 626 treats the following entire URL as a valid autolink:

Playground

www.google.com/search?q=(business))+ok

The current autofix produces:

[www.google.com/search?q=(business))+ok](http://www.google.com/search?q=(business))+ok)

When parsed again, the actual link destination is truncated to:

http://www.google.com/search?q=(business)

This happens because CommonMark requires parentheses in a link destination without angle brackets to be escaped or balanced. Similarly, after autofixing www.example.com/a\*b, the backslash is interpreted as a backslash escape, changing the URL to /a*b. In other words, the autofix silently changes valid URLs to different destinations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another incorrect autofix behavior occurs when autofixing the following valid GFM input:

Playground

www.example.com/a]b

produces invalid link text:

[www.example.com/a]b](http://www.example.com/a]b)

I reproduced an issue where no-bare-urls throws Custom getRange() method must be implemented in the subclass during ESLint’s next autofix pass.

Additionally, after autofixing www.example.com/a*b*c and www.example.com/a~b~c, characters in the label are parsed as emphasis and strikethrough, respectively, changing the rendered output. The link text need to be escaped separately from the destination. CommonMark explicitly requires brackets within link text to be either backslash-escaped or part of a matched pair per the link text definition and Examples 528–529.

Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,110 @@ ruleTester.run("no-bare-urls", rule, {
" Code https://example.com/code?type=indent code",
],
invalid: [
{
code: "Visit www.example.com for details.",
output: "Visit [www.example.com](http://www.example.com) for details.",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 7,
endLine: 1,
endColumn: 22,
},
],
},
{
code: "www.google.com/search?q=(business))+ok",
output: "[www.google.com/search?q=(business))+ok](http://www.google.com/search?q=\\(business\\)\\)+ok)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 39,
},
],
},
{
code: "www.example.com/a\\*b",
output: "[www.example.com/a\\\\\\*b](http://www.example.com/a\\\\*b)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 21,
},
],
},
{
code: "www.example.com/a]b",
output: "[www.example.com/a\\]b](http://www.example.com/a]b)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 20,
},
],
},
{
code: "www.example.com/a[b",
output: "[www.example.com/a\\[b](http://www.example.com/a[b)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 20,
},
],
},
{
code: "www.example.com/a*b*c",
output: "[www.example.com/a\\*b\\*c](http://www.example.com/a*b*c)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 22,
},
],
},
{
code: "www.example.com/a~b~c",
output: "[www.example.com/a\\~b\\~c](http://www.example.com/a~b~c)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 22,
},
],
},
{
code: "www.example.com/a`b`c",
output: "[www.example.com/a\\`b\\`c](http://www.example.com/a`b`c)",
errors: [
{
messageId: "bareUrl",
line: 1,
column: 1,
endLine: 1,
endColumn: 22,
},
],
},
{
code: "https://www.example.com/",
output: "<https://www.example.com/>",
Expand Down