-
-
Notifications
You must be signed in to change notification settings - Fork 383
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
369 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Enforce consistent relative URL style | ||
|
||
✅ *This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.* | ||
|
||
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).* | ||
|
||
When using a relative URL in [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), the URL should either never or always use the `./` prefix consistently. | ||
|
||
## Fail | ||
|
||
```js | ||
const url = new URL('./foo', base); | ||
``` | ||
|
||
## Pass | ||
|
||
```js | ||
const url = new URL('foo', base); | ||
``` | ||
|
||
## Options | ||
|
||
Type: `string`\ | ||
Default: `'never'` | ||
|
||
- `'never'` (default) | ||
- Never use a `./` prefix. | ||
- `'always'` | ||
- Always add a `./` prefix to the relative URL when possible. | ||
|
||
```js | ||
// eslint unicorn/relative-url-style: ["error", "always"] | ||
const url = new URL('foo', base); // Fail | ||
const url = new URL('./foo', base); // Pass | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
'use strict'; | ||
|
||
function replaceStringLiteral(fixer, node, text, relativeRangeStart, relativeRangeEnd) { | ||
const firstCharacterIndex = node.range[0] + 1; | ||
const start = Number.isInteger(relativeRangeEnd) ? relativeRangeStart + firstCharacterIndex : firstCharacterIndex; | ||
const end = Number.isInteger(relativeRangeEnd) ? relativeRangeEnd + firstCharacterIndex : node.range[1] - 1; | ||
|
||
return fixer.replaceTextRange([start, end], text); | ||
} | ||
|
||
module.exports = replaceStringLiteral; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
'use strict'; | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {replaceStringLiteral} = require('./fix/index.js'); | ||
|
||
const MESSAGE_ID_NEVER = 'never'; | ||
const MESSAGE_ID_ALWAYS = 'always'; | ||
const messages = { | ||
[MESSAGE_ID_NEVER]: 'Remove the `./` prefix from the relative URL.', | ||
[MESSAGE_ID_ALWAYS]: 'Add a `./` prefix to the relative URL.', | ||
}; | ||
|
||
const selector = [ | ||
newExpressionSelector({name: 'URL', argumentsLength: 2}), | ||
' > .arguments:first-child', | ||
].join(''); | ||
|
||
const DOT_SLASH = './'; | ||
const TEST_URL_BASE = 'https://example.com/'; | ||
const isSafeToAddDotSlash = url => { | ||
try { | ||
return new URL(url, TEST_URL_BASE).href === new URL(`${DOT_SLASH}${url}`, TEST_URL_BASE).href; | ||
} catch {} | ||
|
||
return false; | ||
}; | ||
|
||
function removeDotSlash(node) { | ||
if ( | ||
node.type === 'TemplateLiteral' | ||
&& node.quasis[0].value.raw.startsWith(DOT_SLASH) | ||
) { | ||
const firstPart = node.quasis[0]; | ||
return fixer => { | ||
const start = firstPart.range[0] + 1; | ||
return fixer.removeRange([start, start + 2]); | ||
}; | ||
} | ||
|
||
if (node.type !== 'Literal' || typeof node.value !== 'string') { | ||
return; | ||
} | ||
|
||
if (!node.raw.slice(1, -1).startsWith(DOT_SLASH)) { | ||
return; | ||
} | ||
|
||
return fixer => replaceStringLiteral(fixer, node, '', 0, 2); | ||
} | ||
|
||
function addDotSlash(node) { | ||
if (node.type !== 'Literal' || typeof node.value !== 'string') { | ||
return; | ||
} | ||
|
||
const url = node.value; | ||
|
||
if (url.startsWith(DOT_SLASH)) { | ||
return; | ||
} | ||
|
||
if ( | ||
url.startsWith('.') | ||
|| url.startsWith('/') | ||
|| !isSafeToAddDotSlash(url) | ||
) { | ||
return; | ||
} | ||
|
||
return fixer => replaceStringLiteral(fixer, node, DOT_SLASH, 0, 0); | ||
} | ||
|
||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const style = context.options[0] || 'never'; | ||
return {[selector](node) { | ||
const fix = (style === 'never' ? removeDotSlash : addDotSlash)(node); | ||
|
||
if (!fix) { | ||
return; | ||
} | ||
|
||
return { | ||
node, | ||
messageId: style, | ||
fix, | ||
}; | ||
}}; | ||
}; | ||
|
||
const schema = [ | ||
{ | ||
enum: ['never', 'always'], | ||
default: 'never', | ||
}, | ||
]; | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Enforce consistent relative URL style.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* eslint-disable no-template-curly-in-string */ | ||
import {getTester} from './utils/test.mjs'; | ||
|
||
const {test} = getTester(import.meta); | ||
|
||
test.snapshot({ | ||
valid: [ | ||
'URL("./foo", base)', | ||
'new URL(...["./foo"], base)', | ||
'new URL(["./foo"], base)', | ||
'new URL("./foo")', | ||
'new URL("./foo", base, extra)', | ||
'new URL("./foo", ...[base])', | ||
'new NOT_URL("./foo", base)', | ||
'new URL', | ||
// Not checking this case | ||
'new globalThis.URL("./foo", base)', | ||
'const foo = "./foo"; new URL(foo, base)', | ||
'const foo = "/foo"; new URL(`.${foo}`, base)', | ||
'new URL(`.${foo}`, base)', | ||
'new URL(".", base)', | ||
'new URL(".././foo", base)', | ||
// We don't check cooked value | ||
'new URL(`\\u002E/${foo}`, base)', | ||
// We don't check escaped string | ||
'new URL("\\u002E/foo", base)', | ||
'new URL(\'\\u002E/foo\', base)', | ||
], | ||
invalid: [ | ||
'new URL("./foo", base)', | ||
'new URL(\'./foo\', base)', | ||
'new URL("./", base)', | ||
'new URL("././a", base)', | ||
'new URL(`./${foo}`, base)', | ||
], | ||
}); | ||
|
||
const alwaysAddDotSlashOptions = ['always']; | ||
test.snapshot({ | ||
valid: [ | ||
'URL("foo", base)', | ||
'new URL(...["foo"], base)', | ||
'new URL(["foo"], base)', | ||
'new URL("foo")', | ||
'new URL("foo", base, extra)', | ||
'new URL("foo", ...[base])', | ||
'new NOT_URL("foo", base)', | ||
'/* 2 */ new URL', | ||
// Not checking this case | ||
'new globalThis.URL("foo", base)', | ||
'new URL(`${foo}`, base2)', | ||
'new URL(`.${foo}`, base2)', | ||
'new URL(".", base2)', | ||
'new URL("//example.org", "https://example.com")', | ||
'new URL("//example.org", "ftp://example.com")', | ||
'new URL("ftp://example.org", "https://example.com")', | ||
'new URL("https://example.org:65536", "https://example.com")', | ||
'new URL("/", base)', | ||
'new URL("/foo", base)', | ||
'new URL("../foo", base)', | ||
'new URL(".././foo", base)', | ||
'new URL("C:\\foo", base)', | ||
'new URL("\\u002E/foo", base)', | ||
'new URL("\\u002Ffoo", base)', | ||
].map(code => ({code, options: alwaysAddDotSlashOptions})), | ||
invalid: [ | ||
'new URL("foo", base)', | ||
'new URL(\'foo\', base)', | ||
].map(code => ({code, options: alwaysAddDotSlashOptions})), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.