routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
@typedef {
|
||||
{
|
||||
name?: string,
|
||||
names?: string[],
|
||||
argumentsLength?: number,
|
||||
minimumArguments?: number,
|
||||
maximumArguments?: number,
|
||||
allowSpreadElement?: boolean,
|
||||
optional?: boolean,
|
||||
} | string | string[]
|
||||
} CallOrNewExpressionCheckOptions
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
function create(node, options, types) {
|
||||
if (!types.includes(node?.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof options === 'string') {
|
||||
options = {names: [options]};
|
||||
}
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
options = {names: options};
|
||||
}
|
||||
|
||||
let {
|
||||
name,
|
||||
names,
|
||||
argumentsLength,
|
||||
minimumArguments,
|
||||
maximumArguments,
|
||||
allowSpreadElement,
|
||||
optional,
|
||||
} = {
|
||||
minimumArguments: 0,
|
||||
maximumArguments: Number.POSITIVE_INFINITY,
|
||||
allowSpreadElement: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (name) {
|
||||
names = [name];
|
||||
}
|
||||
|
||||
if (
|
||||
(optional === true && (node.optional !== optional))
|
||||
|| (
|
||||
optional === false
|
||||
// `node.optional` can be `undefined` in some parsers
|
||||
&& node.optional
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof argumentsLength === 'number' && node.arguments.length !== argumentsLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (minimumArguments !== 0 && node.arguments.length < minimumArguments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isFinite(maximumArguments) && node.arguments.length > maximumArguments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!allowSpreadElement) {
|
||||
const maximumArgumentsLength = Number.isFinite(maximumArguments) ? maximumArguments : argumentsLength;
|
||||
if (
|
||||
typeof maximumArgumentsLength === 'number'
|
||||
&& node.arguments.some((node, index) =>
|
||||
node.type === 'SpreadElement'
|
||||
&& index < maximumArgumentsLength)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(names)
|
||||
&& names.length > 0
|
||||
&& (
|
||||
node.callee.type !== 'Identifier'
|
||||
|| !names.includes(node.callee.name)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@param {CallOrNewExpressionCheckOptions} [options]
|
||||
@returns {boolean}
|
||||
*/
|
||||
export const isCallExpression = (node, options) => create(node, options, ['CallExpression']);
|
||||
|
||||
/**
|
||||
@param {CallOrNewExpressionCheckOptions} [options]
|
||||
@returns {boolean}
|
||||
*/
|
||||
export const isNewExpression = (node, options) => {
|
||||
if (typeof options?.optional === 'boolean') {
|
||||
throw new TypeError('Cannot check node.optional in `isNewExpression`.');
|
||||
}
|
||||
|
||||
return create(node, options, ['NewExpression']);
|
||||
};
|
||||
|
||||
/**
|
||||
@param {CallOrNewExpressionCheckOptions} [options]
|
||||
@returns {boolean}
|
||||
*/
|
||||
export const isCallOrNewExpression = (node, options) => create(node, options, ['CallExpression', 'NewExpression']);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
const functionTypes = [
|
||||
'FunctionDeclaration',
|
||||
'FunctionExpression',
|
||||
'ArrowFunctionExpression',
|
||||
];
|
||||
|
||||
export default functionTypes;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
export {
|
||||
isLiteral,
|
||||
isStringLiteral,
|
||||
isNumericLiteral,
|
||||
isBigIntLiteral,
|
||||
isNullLiteral,
|
||||
isRegexLiteral,
|
||||
isEmptyStringLiteral,
|
||||
} from './literal.js';
|
||||
|
||||
export {
|
||||
isNewExpression,
|
||||
isCallExpression,
|
||||
isCallOrNewExpression,
|
||||
} from './call-or-new-expression.js';
|
||||
|
||||
export {default as isArrowFunctionBody} from './is-arrow-function-body.js';
|
||||
export {default as isDirective} from './is-directive.js';
|
||||
export {default as isEmptyNode} from './is-empty-node.js';
|
||||
export {default as isEmptyArrayExpression} from './is-empty-array-expression.js';
|
||||
export {default as isEmptyObjectExpression} from './is-empty-object-expression.js';
|
||||
export {default as isExpressionStatement} from './is-expression-statement.js';
|
||||
export {default as isFunction} from './is-function.js';
|
||||
export {default as isMemberExpression} from './is-member-expression.js';
|
||||
export {default as isMethodCall} from './is-method-call.js';
|
||||
export {default as isNegativeOne} from './is-negative-one.js';
|
||||
export {default as isReferenceIdentifier} from './is-reference-identifier.js';
|
||||
export {default as isStaticRequire} from './is-static-require.js';
|
||||
export {default as isTaggedTemplateLiteral} from './is-tagged-template-literal.js';
|
||||
export {default as isUndefined} from './is-undefined.js';
|
||||
export {default as functionTypes} from './function-types.js';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function isArrowFunctionBody(node) {
|
||||
return node.parent.type === 'ArrowFunctionExpression' && node.parent.body === node;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
const isDirective = node => node.type === 'ExpressionStatement'
|
||||
&& typeof node.directive === 'string';
|
||||
|
||||
export default isDirective;
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
const isEmptyArrayExpression = node =>
|
||||
node.type === 'ArrayExpression'
|
||||
&& node.elements.length === 0;
|
||||
|
||||
export default isEmptyArrayExpression;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export default function isEmptyNode(node, additionalEmpty) {
|
||||
const {type} = node;
|
||||
|
||||
if (type === 'BlockStatement') {
|
||||
return node.body.every(currentNode => isEmptyNode(currentNode, additionalEmpty));
|
||||
}
|
||||
|
||||
if (type === 'EmptyStatement') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (additionalEmpty?.(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
const isEmptyArrayExpression = node =>
|
||||
node.type === 'ObjectExpression'
|
||||
&& node.properties.length === 0;
|
||||
|
||||
export default isEmptyArrayExpression;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export default function isExpressionStatement(node) {
|
||||
return node.type === 'ExpressionStatement'
|
||||
|| (
|
||||
node.type === 'ChainExpression'
|
||||
&& node.parent.type === 'ExpressionStatement'
|
||||
);
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import functionTypes from './function-types.js';
|
||||
|
||||
export default function isFunction(node) {
|
||||
return functionTypes.includes(node.type);
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/* eslint-disable complexity */
|
||||
/**
|
||||
@param {
|
||||
{
|
||||
property?: string,
|
||||
properties?: string[],
|
||||
object?: string,
|
||||
objects?: string[],
|
||||
optional?: boolean,
|
||||
computed?: boolean
|
||||
} | string | string[]
|
||||
} [options]
|
||||
@returns {string}
|
||||
*/
|
||||
export default function isMemberExpression(node, options) {
|
||||
if (node?.type !== 'MemberExpression') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof options === 'string') {
|
||||
options = {properties: [options]};
|
||||
}
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
options = {properties: options};
|
||||
}
|
||||
|
||||
let {
|
||||
property,
|
||||
properties,
|
||||
object,
|
||||
objects,
|
||||
optional,
|
||||
computed,
|
||||
} = {
|
||||
property: '',
|
||||
properties: [],
|
||||
object: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
if (property) {
|
||||
properties = [property];
|
||||
}
|
||||
|
||||
if (object) {
|
||||
objects = [object];
|
||||
}
|
||||
|
||||
if (
|
||||
(optional === true && (node.optional !== optional))
|
||||
|| (
|
||||
optional === false
|
||||
// `node.optional` can be `undefined` in some parsers
|
||||
&& node.optional
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(properties)
|
||||
&& properties.length > 0
|
||||
) {
|
||||
if (
|
||||
node.property.type !== 'Identifier'
|
||||
|| !properties.includes(node.property.name)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
computed ??= false;
|
||||
}
|
||||
|
||||
if (
|
||||
(computed === true && (node.computed !== computed))
|
||||
|| (
|
||||
computed === false
|
||||
// `node.computed` can be `undefined` in some parsers
|
||||
&& node.computed
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(objects)
|
||||
&& objects.length > 0
|
||||
&& (
|
||||
node.object.type !== 'Identifier'
|
||||
|| !objects.includes(node.object.name)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import isMemberExpression from './is-member-expression.js';
|
||||
import {isCallExpression} from './call-or-new-expression.js';
|
||||
|
||||
/**
|
||||
@param {
|
||||
{
|
||||
// `isCallExpression` options
|
||||
argumentsLength?: number,
|
||||
minimumArguments?: number,
|
||||
maximumArguments?: number,
|
||||
optionalCall?: boolean,
|
||||
allowSpreadElement?: boolean,
|
||||
|
||||
// `isMemberExpression` options
|
||||
method?: string,
|
||||
methods?: string[],
|
||||
object?: string,
|
||||
objects?: string[],
|
||||
optionalMember?: boolean,
|
||||
computed?: boolean
|
||||
} | string | string[]
|
||||
} [options]
|
||||
@returns {string}
|
||||
*/
|
||||
export default function isMethodCall(node, options) {
|
||||
if (typeof options === 'string') {
|
||||
options = {methods: [options]};
|
||||
}
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
options = {methods: options};
|
||||
}
|
||||
|
||||
const {
|
||||
optionalCall,
|
||||
optionalMember,
|
||||
method,
|
||||
methods,
|
||||
} = {
|
||||
method: '',
|
||||
methods: [],
|
||||
...options,
|
||||
};
|
||||
|
||||
return (
|
||||
isCallExpression(node, {
|
||||
argumentsLength: options.argumentsLength,
|
||||
minimumArguments: options.minimumArguments,
|
||||
maximumArguments: options.maximumArguments,
|
||||
allowSpreadElement: options.allowSpreadElement,
|
||||
optional: optionalCall,
|
||||
})
|
||||
&& isMemberExpression(node.callee, {
|
||||
object: options.object,
|
||||
objects: options.objects,
|
||||
computed: options.computed,
|
||||
property: method,
|
||||
properties: methods,
|
||||
optional: optionalMember,
|
||||
})
|
||||
);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import {isNumericLiteral} from './literal.js';
|
||||
|
||||
export default function isNegativeOne(node) {
|
||||
return node?.type === 'UnaryExpression'
|
||||
&& node.operator === '-'
|
||||
&& isNumericLiteral(node.argument)
|
||||
&& node.argument.value === 1;
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
// eslint-disable-next-line complexity
|
||||
function isNotReference(node) {
|
||||
const {parent} = node;
|
||||
|
||||
switch (parent.type) {
|
||||
// `foo.Identifier`
|
||||
case 'MemberExpression': {
|
||||
return !parent.computed && parent.property === node;
|
||||
}
|
||||
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression': {
|
||||
return (
|
||||
// `function foo(Identifier) {}`
|
||||
// `const foo = function(Identifier) {}`
|
||||
parent.params.includes(node)
|
||||
// `function Identifier() {}`
|
||||
// `const foo = function Identifier() {}`
|
||||
|| parent.id === node
|
||||
);
|
||||
}
|
||||
|
||||
case 'ArrowFunctionExpression': {
|
||||
// `const foo = (Identifier) => {}`
|
||||
return parent.params.includes(node);
|
||||
}
|
||||
|
||||
// `class Identifier() {}`
|
||||
// `const foo = class Identifier() {}`
|
||||
// `const Identifier = 1`
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassExpression':
|
||||
case 'VariableDeclarator': {
|
||||
return parent.id === node;
|
||||
}
|
||||
|
||||
// `class Foo {Identifier = 1}`
|
||||
// `class Foo {Identifier() {}}`
|
||||
case 'PropertyDefinition':
|
||||
case 'MethodDefinition': {
|
||||
return !parent.computed && parent.key === node;
|
||||
}
|
||||
|
||||
// `const foo = {Identifier: 1}`
|
||||
// `const {Identifier} = {}`
|
||||
// `const {Identifier: foo} = {}`
|
||||
// `const {Identifier} = {}`
|
||||
// `const {foo: Identifier} = {}`
|
||||
case 'Property': {
|
||||
return (
|
||||
(
|
||||
!parent.computed
|
||||
&& parent.key === node
|
||||
&& (
|
||||
(parent.parent.type === 'ObjectExpression' || parent.parent.type === 'ObjectPattern')
|
||||
&& parent.parent.properties.includes(parent)
|
||||
)
|
||||
)
|
||||
|| (
|
||||
parent.value === node
|
||||
&& parent.parent.type === 'ObjectPattern'
|
||||
&& parent.parent.properties.includes(parent)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// `const [Identifier] = []`
|
||||
case 'ArrayPattern': {
|
||||
return parent.elements.includes(node);
|
||||
}
|
||||
|
||||
/*
|
||||
```
|
||||
Identifier: for (const foo of bar) {
|
||||
continue Identifier;
|
||||
break Identifier;
|
||||
}
|
||||
```
|
||||
*/
|
||||
case 'LabeledStatement':
|
||||
case 'ContinueStatement':
|
||||
case 'BreakStatement': {
|
||||
return parent.label === node;
|
||||
}
|
||||
|
||||
// `import * as Identifier from 'foo'`
|
||||
// `import Identifier from 'foo'`
|
||||
case 'ImportDefaultSpecifier':
|
||||
case 'ImportNamespaceSpecifier': {
|
||||
return parent.local === node;
|
||||
}
|
||||
|
||||
// `export * as Identifier from 'foo'`
|
||||
case 'ExportAllDeclaration': {
|
||||
return parent.exported === node;
|
||||
}
|
||||
|
||||
// `import {foo as Identifier} from 'foo'`
|
||||
// `import {Identifier as foo} from 'foo'`
|
||||
case 'ImportSpecifier': {
|
||||
return (parent.local === node || parent.imported === node);
|
||||
}
|
||||
|
||||
// `export {foo as Identifier}`
|
||||
// `export {Identifier as foo}`
|
||||
case 'ExportSpecifier': {
|
||||
return (parent.local === node || parent.exported === node);
|
||||
}
|
||||
|
||||
// TypeScript
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSEnumMember': {
|
||||
return parent.id === node;
|
||||
}
|
||||
|
||||
// `type Foo = { [Identifier: string]: string }`
|
||||
case 'TSIndexSignature': {
|
||||
return parent.parameters.includes(node);
|
||||
}
|
||||
|
||||
// `@typescript-eslint/parse` v7
|
||||
// `type Foo = { [Identifier in keyof string]: number; };`
|
||||
case 'TSTypeParameter': {
|
||||
return parent.name === node;
|
||||
}
|
||||
|
||||
// `@typescript-eslint/parse` v8
|
||||
// `type Foo = { [Identifier in keyof string]: number; };`
|
||||
case 'TSMappedType': {
|
||||
return parent.key === node;
|
||||
}
|
||||
|
||||
// `type Identifier = Foo`
|
||||
case 'TSTypeAliasDeclaration': {
|
||||
return parent.id === node;
|
||||
}
|
||||
|
||||
case 'TSPropertySignature': {
|
||||
return parent.key === node;
|
||||
}
|
||||
|
||||
// No default
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function isReferenceIdentifier(node, nameOrNames = []) {
|
||||
if (node.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames];
|
||||
if (names.length > 0 && !names.includes(node.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isNotReference(node);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import {isStringLiteral} from './literal.js';
|
||||
import {isCallExpression} from './call-or-new-expression.js';
|
||||
|
||||
const isStaticRequire = node => isCallExpression(node, {
|
||||
name: 'require',
|
||||
argumentsLength: 1,
|
||||
optional: false,
|
||||
}) && isStringLiteral(node.arguments[0]);
|
||||
|
||||
export default isStaticRequire;
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import {isNodeMatches} from '../utils/is-node-matches.js';
|
||||
|
||||
/**
|
||||
Check if the given node is a tagged template literal.
|
||||
|
||||
@param {Node} node - The AST node to check.
|
||||
@param {string[]} tags - The object name or key paths.
|
||||
@returns {boolean}
|
||||
*/
|
||||
export default function isTaggedTemplateLiteral(node, tags) {
|
||||
if (
|
||||
node.type !== 'TemplateLiteral'
|
||||
|| node.parent.type !== 'TaggedTemplateExpression'
|
||||
|| node.parent.quasi !== node
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
return isNodeMatches(node.parent.tag, tags);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function isUndefined(node) {
|
||||
return node?.type === 'Identifier' && node.name === 'undefined';
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export function isLiteral(node, value) {
|
||||
if (node?.type !== 'Literal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.value === value;
|
||||
}
|
||||
|
||||
export const isStringLiteral = node => node?.type === 'Literal' && typeof node.value === 'string';
|
||||
|
||||
export const isNumericLiteral = node => node.type === 'Literal' && typeof node.value === 'number';
|
||||
|
||||
export const isRegexLiteral = node => node.type === 'Literal' && Boolean(node.regex);
|
||||
|
||||
export const isNullLiteral = node => node?.type === 'Literal' && node.raw === 'null';
|
||||
|
||||
export const isBigIntLiteral = node => node.type === 'Literal' && Boolean(node.bigint);
|
||||
|
||||
export const isEmptyStringLiteral = node => isLiteral(node, '');
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import cleanRegexp from 'clean-regexp';
|
||||
import regexpTree from 'regexp-tree';
|
||||
import escapeString from './utils/escape-string.js';
|
||||
import {isStringLiteral, isNewExpression, isRegexLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'better-regex';
|
||||
const MESSAGE_ID_PARSE_ERROR = 'better-regex/parse-error';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: '{{original}} can be optimized to {{optimized}}.',
|
||||
[MESSAGE_ID_PARSE_ERROR]: 'Problem parsing {{original}}: {{error}}',
|
||||
};
|
||||
|
||||
// `regexp-tree` can optimize `/|/` into `//`, which is not valid JavaScript syntax.
|
||||
// Normalize to an explicit empty alternative so the autofix always stays parseable.
|
||||
const normalizeOptimizedRegexLiteral = optimizedRegexLiteral => (
|
||||
optimizedRegexLiteral.startsWith('//')
|
||||
? `/(?:)/${optimizedRegexLiteral.slice(2)}`
|
||||
: optimizedRegexLiteral
|
||||
);
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sortCharacterClasses} = context.options[0];
|
||||
|
||||
const ignoreList = [];
|
||||
|
||||
if (sortCharacterClasses === false) {
|
||||
ignoreList.push('charClassClassrangesMerge');
|
||||
}
|
||||
|
||||
context.on('Literal', node => {
|
||||
if (!isRegexLiteral(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {raw: original, regex} = node;
|
||||
// Regular Expressions with `u` and `v` flag are not well handled by `regexp-tree`
|
||||
// https://github.com/DmitrySoshnikov/regexp-tree/issues/162
|
||||
if (regex.flags.includes('u') || regex.flags.includes('v')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let optimized = original;
|
||||
|
||||
try {
|
||||
optimized = regexpTree.optimize(original, undefined, {blacklist: ignoreList}).toString();
|
||||
optimized = normalizeOptimizedRegexLiteral(optimized);
|
||||
} catch (error) {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_PARSE_ERROR,
|
||||
data: {
|
||||
original,
|
||||
error: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (original === optimized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
original,
|
||||
optimized,
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
node.parent.type === 'MemberExpression'
|
||||
&& node.parent.object === node
|
||||
&& !node.parent.optional
|
||||
&& !node.parent.computed
|
||||
&& node.parent.property.type === 'Identifier'
|
||||
&& (
|
||||
node.parent.property.name === 'toString'
|
||||
|| node.parent.property.name === 'source'
|
||||
)
|
||||
) {
|
||||
return problem;
|
||||
}
|
||||
|
||||
return Object.assign(problem, {
|
||||
fix: fixer => fixer.replaceText(node, optimized),
|
||||
});
|
||||
});
|
||||
|
||||
context.on('NewExpression', node => {
|
||||
if (!isNewExpression(node, {name: 'RegExp', minimumArguments: 1})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [patternNode, flagsNode] = node.arguments;
|
||||
|
||||
if (!isStringLiteral(patternNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPattern = patternNode.value;
|
||||
const flags = isStringLiteral(flagsNode)
|
||||
? flagsNode.value
|
||||
: '';
|
||||
|
||||
const newPattern = cleanRegexp(oldPattern, flags);
|
||||
|
||||
if (oldPattern !== newPattern) {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
original: oldPattern,
|
||||
optimized: newPattern,
|
||||
},
|
||||
fix: fixer => fixer.replaceText(
|
||||
patternNode,
|
||||
escapeString(newPattern, patternNode.raw.charAt(0)),
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
sortCharacterClasses: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to sort character classes.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Improve regexes by making them shorter, consistent, and safer.',
|
||||
recommended: false,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema,
|
||||
defaultOptions: [{sortCharacterClasses: true}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import {isRegExp} from 'node:util/types';
|
||||
import {findVariable} from '@eslint-community/eslint-utils';
|
||||
import {getAvailableVariableName, upperFirst} from './utils/index.js';
|
||||
import {renameVariable} from './fix/index.js';
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'catch-error-name';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'The catch parameter `{{originalName}}` should be named `{{fixedName}}`.',
|
||||
};
|
||||
|
||||
// - `promise.then(…, foo => {})`
|
||||
// - `promise.then(…, function(foo) {})`
|
||||
// - `promise.catch(foo => {})`
|
||||
// - `promise.catch(function(foo) {})`
|
||||
const isPromiseCatchParameter = node =>
|
||||
(node.parent.type === 'FunctionExpression' || node.parent.type === 'ArrowFunctionExpression')
|
||||
&& node.parent.params[0] === node
|
||||
&& (
|
||||
isMethodCall(node.parent.parent, {
|
||||
method: 'then',
|
||||
argumentsLength: 2,
|
||||
optionalCall: false,
|
||||
})
|
||||
|| isMethodCall(node.parent.parent, {
|
||||
method: 'catch',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
})
|
||||
)
|
||||
&& node.parent.parent.arguments.at(-1) === node.parent;
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const options = context.options[0];
|
||||
const {name: expectedName} = options;
|
||||
const ignore = options.ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'));
|
||||
const isNameAllowed = name =>
|
||||
name === expectedName
|
||||
|| ignore.some(regexp => regexp.test(name))
|
||||
|| name.endsWith(expectedName)
|
||||
|| name.endsWith(upperFirst(expectedName));
|
||||
|
||||
context.on('Identifier', node => {
|
||||
if (
|
||||
!(node.parent.type === 'CatchClause' && node.parent.param === node)
|
||||
&& !isPromiseCatchParameter(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalName = node.name;
|
||||
|
||||
if (
|
||||
isNameAllowed(originalName)
|
||||
|| isNameAllowed(originalName.replaceAll(/_+$/g, ''))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = context.sourceCode.getScope(node);
|
||||
const variable = findVariable(scope, node);
|
||||
|
||||
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768072967
|
||||
// But can't reproduce, just ignore this case
|
||||
/* c8 ignore next 3 */
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (originalName === '_' && variable.references.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scopes = [
|
||||
variable.scope,
|
||||
...variable.references.map(({from}) => from),
|
||||
];
|
||||
const fixedName = getAvailableVariableName(expectedName, scopes);
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
originalName,
|
||||
fixedName: fixedName || expectedName,
|
||||
},
|
||||
};
|
||||
|
||||
if (fixedName) {
|
||||
problem.fix = fixer => renameVariable(variable, fixedName, context, fixer);
|
||||
}
|
||||
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The expected name for the error variable.',
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
description: 'Patterns to ignore.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce a specific parameter name in catch clauses.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema,
|
||||
defaultOptions: [{name: 'error', ignore: []}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
const MESSAGE_ID_ERROR = 'consistent-assert/error';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Prefer `{{name}}.ok(…)` over `{{name}}(…)`.',
|
||||
};
|
||||
|
||||
/**
|
||||
@param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier | import('estree').ImportSpecifier | import('estree').ImportDeclaration} node
|
||||
*/
|
||||
const isValueImport = node => !node.importKind || node.importKind === 'value';
|
||||
|
||||
/**
|
||||
Check if a specifier is `assert` function.
|
||||
|
||||
@param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier} specifier
|
||||
@param {string} moduleName
|
||||
*/
|
||||
const isAssertFunction = (specifier, moduleName) =>
|
||||
// `import assert from 'node:assert';`
|
||||
// `import assert from 'node:assert/strict';`
|
||||
specifier.type === 'ImportDefaultSpecifier'
|
||||
// `import {default as assert} from 'node:assert';`
|
||||
// `import {default as assert} from 'node:assert/strict';`
|
||||
|| (
|
||||
specifier.type === 'ImportSpecifier'
|
||||
&& specifier.imported.type === 'Identifier'
|
||||
&& specifier.imported.name === 'default'
|
||||
)
|
||||
// `import {strict as assert} from 'node:assert';`
|
||||
|| (
|
||||
moduleName === 'assert'
|
||||
&& specifier.type === 'ImportSpecifier'
|
||||
&& specifier.imported.type === 'Identifier'
|
||||
&& specifier.imported.name === 'strict'
|
||||
);
|
||||
|
||||
const NODE_PROTOCOL = 'node:';
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule['create']} */
|
||||
const create = context => {
|
||||
context.on('ImportDeclaration', function * (importDeclaration) {
|
||||
if (!isValueImport(importDeclaration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let moduleName = importDeclaration.source.value;
|
||||
|
||||
if (moduleName.startsWith(NODE_PROTOCOL)) {
|
||||
moduleName = moduleName.slice(NODE_PROTOCOL.length);
|
||||
}
|
||||
|
||||
if (moduleName !== 'assert' && moduleName !== 'assert/strict') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const specifier of importDeclaration.specifiers) {
|
||||
if (!isValueImport(specifier) || !isAssertFunction(specifier, moduleName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variables = context.sourceCode.getDeclaredVariables(specifier);
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
if (!Array.isArray(variables) && variables.length === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [variable] = variables;
|
||||
|
||||
for (const {identifier} of variable.references) {
|
||||
if (!(identifier.parent.type === 'CallExpression' && identifier.parent.callee === identifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield {
|
||||
node: identifier,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {name: identifier.name},
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
fix: fixer => fixer.insertTextAfter(identifier, '.ok'),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce consistent assertion style with `node:assert`.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import {isMethodCall, isNewExpression} from './ast/index.js';
|
||||
import {removeMethodCall} from './fix/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'consistent-date-clone/error';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Unnecessary `.getTime()` call.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('NewExpression', newExpression => {
|
||||
if (!isNewExpression(newExpression, {name: 'Date', argumentsLength: 1})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [callExpression] = newExpression.arguments;
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
method: 'getTime',
|
||||
argumentsLength: 0,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
return {
|
||||
node: callExpression,
|
||||
loc: {
|
||||
start: sourceCode.getLoc(callExpression.callee.property).start,
|
||||
end: sourceCode.getLoc(callExpression).end,
|
||||
},
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
fix: fixer => removeMethodCall(fixer, callExpression, context),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer passing `Date` directly to the constructor when cloning.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
import {findVariable} from '@eslint-community/eslint-utils';
|
||||
import {getAvailableVariableName, isLeftHandSide} from './utils/index.js';
|
||||
import {isCallOrNewExpression} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'consistentDestructuring';
|
||||
const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest';
|
||||
|
||||
const isSimpleExpression = expression => {
|
||||
while (expression) {
|
||||
if (expression.computed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expression.type !== 'MemberExpression') {
|
||||
break;
|
||||
}
|
||||
|
||||
expression = expression.object;
|
||||
}
|
||||
|
||||
return expression.type === 'Identifier'
|
||||
|| expression.type === 'ThisExpression';
|
||||
};
|
||||
|
||||
const isChildInParentScope = (child, parent) => {
|
||||
while (child) {
|
||||
if (child === parent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
child = child.upper;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getRootIdentifier = expression => {
|
||||
while (expression?.type === 'MemberExpression') {
|
||||
expression = expression.object;
|
||||
}
|
||||
|
||||
return expression?.type === 'Identifier' ? expression : undefined;
|
||||
};
|
||||
|
||||
const hasRestElement = pattern => {
|
||||
switch (pattern?.type) {
|
||||
case 'RestElement': {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'AssignmentPattern': {
|
||||
return hasRestElement(pattern.left);
|
||||
}
|
||||
|
||||
case 'ObjectPattern': {
|
||||
return pattern.properties.some(property =>
|
||||
hasRestElement(property.type === 'Property' ? property.value : property));
|
||||
}
|
||||
|
||||
case 'ArrayPattern': {
|
||||
return pattern.elements.some(element =>
|
||||
hasRestElement(element));
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isIdentifierProperty = property =>
|
||||
property.type === 'Property'
|
||||
&& property.key.type === 'Identifier';
|
||||
|
||||
const hasNestedRestElement = pattern => {
|
||||
switch (pattern?.type) {
|
||||
case 'AssignmentPattern': {
|
||||
return hasNestedRestElement(pattern.left);
|
||||
}
|
||||
|
||||
case 'ObjectPattern': {
|
||||
return pattern.properties.some(property => {
|
||||
if (property.type === 'RestElement') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasRestElement(property.value);
|
||||
});
|
||||
}
|
||||
|
||||
case 'ArrayPattern': {
|
||||
return pattern.elements.some(element => {
|
||||
if (!element || element.type === 'RestElement') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasRestElement(element);
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isMemberDestructuredInNestedPatternWithRest = (objectPattern, memberName) =>
|
||||
objectPattern.properties.some(property =>
|
||||
isIdentifierProperty(property)
|
||||
&& property.key.name === memberName
|
||||
&& property.value.type !== 'Identifier'
|
||||
&& hasNestedRestElement(property.value));
|
||||
|
||||
const isRootVariableReassigned = (declaration, memberExpressionNode, memberScope, sourceCode) => {
|
||||
if (!declaration.rootVariable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, declarationEnd] = sourceCode.getRange(declaration.object);
|
||||
const [memberStart] = sourceCode.getRange(memberExpressionNode);
|
||||
|
||||
return declaration.rootVariable.references.some(reference => {
|
||||
if (!reference.isWrite()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [referenceStart] = sourceCode.getRange(reference.identifier);
|
||||
if (referenceStart < declarationEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Be conservative: writes from other variable scopes may run before this read via calls/closures.
|
||||
if (reference.from.variableScope !== memberScope.variableScope) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return referenceStart <= memberStart;
|
||||
});
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
const declarations = new Map();
|
||||
|
||||
context.on('VariableDeclarator', node => {
|
||||
if (!(
|
||||
node.id.type === 'ObjectPattern'
|
||||
&& node.init
|
||||
&& node.init.type !== 'Literal'
|
||||
// Ignore any complex expressions (e.g. arrays, functions)
|
||||
&& isSimpleExpression(node.init)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootIdentifier = getRootIdentifier(node.init);
|
||||
declarations.set(sourceCode.getText(node.init), {
|
||||
scope: sourceCode.getScope(node),
|
||||
object: node.init,
|
||||
rootIdentifierName: rootIdentifier?.name,
|
||||
rootVariable: rootIdentifier && findVariable(sourceCode.getScope(node), rootIdentifier),
|
||||
objectPattern: node.id,
|
||||
});
|
||||
});
|
||||
|
||||
context.on('MemberExpression', node => {
|
||||
if (
|
||||
node.computed
|
||||
|| (
|
||||
isCallOrNewExpression(node.parent)
|
||||
&& node.parent.callee === node
|
||||
)
|
||||
|| isLeftHandSide(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const declaration = declarations.get(sourceCode.getText(node.object));
|
||||
|
||||
if (!declaration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memberScope = sourceCode.getScope(node);
|
||||
const memberRootIdentifier = getRootIdentifier(node.object);
|
||||
const memberRootVariable = memberRootIdentifier && findVariable(memberScope, memberRootIdentifier);
|
||||
if (
|
||||
declaration.rootIdentifierName
|
||||
&& memberRootIdentifier?.name === declaration.rootIdentifierName
|
||||
&& memberRootVariable !== declaration.rootVariable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRootVariableReassigned(declaration, node, memberScope, sourceCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {scope, objectPattern} = declaration;
|
||||
|
||||
// Property is destructured outside the current scope
|
||||
if (!isChildInParentScope(memberScope, scope)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = sourceCode.getText(node.property);
|
||||
const memberDestructuredInNestedPattern = isMemberDestructuredInNestedPatternWithRest(objectPattern, member);
|
||||
|
||||
const destructuredProperties = objectPattern.properties.filter(property =>
|
||||
isIdentifierProperty(property)
|
||||
&& property.value.type === 'Identifier');
|
||||
|
||||
const lastProperty = objectPattern.properties.at(-1);
|
||||
const hasRest = lastProperty?.type === 'RestElement';
|
||||
const expression = sourceCode.getText(node);
|
||||
|
||||
// Member might already be destructured
|
||||
const destructuredMember = destructuredProperties.find(property =>
|
||||
property.key.name === member);
|
||||
|
||||
if (!destructuredMember) {
|
||||
if (memberDestructuredInNestedPattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't destructure additional members when rest is used
|
||||
if (hasRest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destructured member collides with an existing identifier
|
||||
if (getAvailableVariableName(member, [memberScope]) !== member) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't try to fix nested member expressions
|
||||
if (node.parent.type === 'MemberExpression') {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
}
|
||||
|
||||
const newMember = destructuredMember ? destructuredMember.value.name : member;
|
||||
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
suggest: [{
|
||||
messageId: MESSAGE_ID_SUGGEST,
|
||||
data: {
|
||||
expression,
|
||||
property: newMember,
|
||||
},
|
||||
* fix(fixer) {
|
||||
const {properties} = objectPattern;
|
||||
const lastProperty = properties.at(-1);
|
||||
|
||||
yield fixer.replaceText(node, newMember);
|
||||
|
||||
if (!destructuredMember) {
|
||||
yield lastProperty
|
||||
? fixer.insertTextAfter(lastProperty, `, ${newMember}`)
|
||||
: fixer.replaceText(objectPattern, `{${newMember}}`);
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Use destructured variables over properties.',
|
||||
recommended: false,
|
||||
},
|
||||
hasSuggestions: true,
|
||||
messages: {
|
||||
[MESSAGE_ID]: 'Use destructured variables over properties.',
|
||||
[MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+123
@@ -0,0 +1,123 @@
|
||||
import {getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isEmptyArrayExpression,
|
||||
isEmptyStringLiteral,
|
||||
} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'consistent-empty-array-spread';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Prefer using empty {{replacementDescription}} since the {{anotherNodePosition}} is {{anotherNodeDescription}}.',
|
||||
};
|
||||
|
||||
const isString = (node, context) => {
|
||||
const staticValueResult = getStaticValue(node, context.sourceCode.getScope(node));
|
||||
return typeof staticValueResult?.value === 'string';
|
||||
};
|
||||
|
||||
const isArray = (node, context) => {
|
||||
if (node.type === 'ArrayExpression') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const staticValueResult = getStaticValue(node, context.sourceCode.getScope(node));
|
||||
return Array.isArray(staticValueResult?.value);
|
||||
};
|
||||
|
||||
const cases = [
|
||||
{
|
||||
oneSidePredicate: isEmptyStringLiteral,
|
||||
anotherSidePredicate: isArray,
|
||||
anotherNodeDescription: 'an array',
|
||||
replacementDescription: 'array',
|
||||
replacementCode: '[]',
|
||||
},
|
||||
{
|
||||
oneSidePredicate: isEmptyArrayExpression,
|
||||
anotherSidePredicate: isString,
|
||||
anotherNodeDescription: 'a string',
|
||||
replacementDescription: 'string',
|
||||
replacementCode: '\'\'',
|
||||
},
|
||||
];
|
||||
|
||||
function createProblem({
|
||||
problemNode,
|
||||
anotherNodePosition,
|
||||
anotherNodeDescription,
|
||||
replacementDescription,
|
||||
replacementCode,
|
||||
}) {
|
||||
return {
|
||||
node: problemNode,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
replacementDescription,
|
||||
anotherNodePosition,
|
||||
anotherNodeDescription,
|
||||
},
|
||||
fix: fixer => fixer.replaceText(problemNode, replacementCode),
|
||||
};
|
||||
}
|
||||
|
||||
function getProblem(conditionalExpression, context) {
|
||||
const {
|
||||
consequent,
|
||||
alternate,
|
||||
} = conditionalExpression;
|
||||
|
||||
for (const problemCase of cases) {
|
||||
const {
|
||||
oneSidePredicate,
|
||||
anotherSidePredicate,
|
||||
} = problemCase;
|
||||
|
||||
if (oneSidePredicate(consequent, context) && anotherSidePredicate(alternate, context)) {
|
||||
return createProblem({
|
||||
...problemCase,
|
||||
problemNode: consequent,
|
||||
anotherNodePosition: 'alternate',
|
||||
});
|
||||
}
|
||||
|
||||
if (oneSidePredicate(alternate, context) && anotherSidePredicate(consequent, context)) {
|
||||
return createProblem({
|
||||
...problemCase,
|
||||
problemNode: alternate,
|
||||
anotherNodePosition: 'consequent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('ArrayExpression', function * (arrayExpression) {
|
||||
for (const element of arrayExpression.elements) {
|
||||
if (
|
||||
element?.type !== 'SpreadElement'
|
||||
|| element.argument.type !== 'ConditionalExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield getProblem(element.argument, context);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer consistent types when spreading a ternary in an array literal.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+136
@@ -0,0 +1,136 @@
|
||||
import toLocation from './utils/to-location.js';
|
||||
import {isMethodCall, isNegativeOne, isNumericLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'consistent-existence-index-check';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Prefer `{{replacementOperator}} {{replacementValue}}` over `{{originalOperator}} {{originalValue}}` to check {{existenceOrNonExistence}}.',
|
||||
};
|
||||
|
||||
const isZero = node => isNumericLiteral(node) && node.value === 0;
|
||||
|
||||
/**
|
||||
@param {parent: import('estree').BinaryExpression} binaryExpression
|
||||
@returns {{
|
||||
replacementOperator: string,
|
||||
replacementValue: string,
|
||||
originalOperator: string,
|
||||
originalValue: string,
|
||||
} | undefined}
|
||||
*/
|
||||
function getReplacement(binaryExpression) {
|
||||
const {operator, right} = binaryExpression;
|
||||
|
||||
if (operator === '<' && isZero(right)) {
|
||||
return {
|
||||
replacementOperator: '===',
|
||||
replacementValue: '-1',
|
||||
originalOperator: operator,
|
||||
originalValue: '0',
|
||||
};
|
||||
}
|
||||
|
||||
if (operator === '>' && isNegativeOne(right)) {
|
||||
return {
|
||||
replacementOperator: '!==',
|
||||
replacementValue: '-1',
|
||||
originalOperator: operator,
|
||||
originalValue: '-1',
|
||||
};
|
||||
}
|
||||
|
||||
if (operator === '>=' && isZero(right)) {
|
||||
return {
|
||||
replacementOperator: '!==',
|
||||
replacementValue: '-1',
|
||||
originalOperator: operator,
|
||||
originalValue: '0',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('VariableDeclarator', /** @param {import('estree').VariableDeclarator} variableDeclarator */ function * (variableDeclarator) {
|
||||
if (!(
|
||||
variableDeclarator.parent.type === 'VariableDeclaration'
|
||||
&& variableDeclarator.parent.kind === 'const'
|
||||
&& variableDeclarator.id.type === 'Identifier'
|
||||
&& isMethodCall(variableDeclarator.init, {methods: ['indexOf', 'lastIndexOf', 'findIndex', 'findLastIndex']})
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variableIdentifier = variableDeclarator.id;
|
||||
const variables = context.sourceCode.getDeclaredVariables(variableDeclarator);
|
||||
const [variable] = variables;
|
||||
|
||||
// Just for safety
|
||||
if (
|
||||
variables.length !== 1
|
||||
|| variable.identifiers.length !== 1
|
||||
|| variable.identifiers[0] !== variableIdentifier
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const {identifier} of variable.references) {
|
||||
/** @type {{parent: import('estree').BinaryExpression}} */
|
||||
const binaryExpression = identifier.parent;
|
||||
|
||||
if (binaryExpression.type !== 'BinaryExpression' || binaryExpression.left !== identifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const replacement = getReplacement(binaryExpression);
|
||||
|
||||
if (!replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {left, operator, right} = binaryExpression;
|
||||
const {sourceCode} = context;
|
||||
|
||||
const operatorToken = sourceCode.getTokenAfter(
|
||||
left,
|
||||
token => token.type === 'Punctuator' && token.value === operator,
|
||||
);
|
||||
|
||||
const [start] = sourceCode.getRange(operatorToken);
|
||||
const [, end] = sourceCode.getRange(right);
|
||||
|
||||
yield {
|
||||
node: binaryExpression,
|
||||
loc: toLocation([start, end], context),
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
...replacement,
|
||||
existenceOrNonExistence: `${replacement.replacementOperator === '===' ? 'non-' : ''}existence`,
|
||||
},
|
||||
* fix(fixer) {
|
||||
yield fixer.replaceText(operatorToken, replacement.replacementOperator);
|
||||
|
||||
if (replacement.replacementValue !== replacement.originalValue) {
|
||||
yield fixer.replaceText(right, replacement.replacementValue);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
import {getFunctionHeadLocation, getFunctionNameWithKind} from '@eslint-community/eslint-utils';
|
||||
import {getReferences, isNodeContainsLexicalThis, isNodeMatches} from './utils/index.js';
|
||||
import {functionTypes} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'consistent-function-scoping';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Move {{functionNameWithKind}} to the outer scope.',
|
||||
};
|
||||
|
||||
const isSameScope = (scope1, scope2) =>
|
||||
scope1 && scope2 && (scope1 === scope2 || scope1.block === scope2.block);
|
||||
|
||||
function checkReferences(scope, parent, scopeManager) {
|
||||
const hitReference = references => references.some(reference => {
|
||||
if (isSameScope(parent, reference.from)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {resolved} = reference;
|
||||
const [definition] = resolved.defs;
|
||||
|
||||
// Skip recursive function name
|
||||
if (definition?.type === 'FunctionName' && resolved.name === definition.name.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isSameScope(parent, resolved.scope);
|
||||
});
|
||||
|
||||
const hitDefinitions = definitions => definitions.some(definition => {
|
||||
const scope = scopeManager.acquire(definition.node);
|
||||
return isSameScope(parent, scope);
|
||||
});
|
||||
|
||||
// This check looks for neighboring function definitions
|
||||
const hitIdentifier = identifiers => identifiers.some(identifier => {
|
||||
// Only look at identifiers that live in a FunctionDeclaration
|
||||
if (
|
||||
!identifier.parent
|
||||
|| identifier.parent.type !== 'FunctionDeclaration'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const identifierScope = scopeManager.acquire(identifier);
|
||||
|
||||
// If we have a scope, the earlier checks should have worked so ignore them here
|
||||
/* c8 ignore next 3 */
|
||||
if (identifierScope) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const identifierParentScope = scopeManager.acquire(identifier.parent);
|
||||
/* c8 ignore next 3 */
|
||||
if (!identifierParentScope) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore identifiers from our own scope
|
||||
if (isSameScope(scope, identifierParentScope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look at the scope above the function definition to see if it lives
|
||||
// next to the reference being checked
|
||||
return isSameScope(parent, identifierParentScope.upper);
|
||||
});
|
||||
|
||||
return getReferences(scope)
|
||||
.map(({resolved}) => resolved)
|
||||
.filter(Boolean)
|
||||
.some(variable =>
|
||||
hitReference(variable.references)
|
||||
|| hitDefinitions(variable.defs)
|
||||
|| hitIdentifier(variable.identifiers));
|
||||
}
|
||||
|
||||
// https://reactjs.org/docs/hooks-reference.html
|
||||
const reactHooks = [
|
||||
'useState',
|
||||
'useEffect',
|
||||
'useContext',
|
||||
'useReducer',
|
||||
'useCallback',
|
||||
'useMemo',
|
||||
'useRef',
|
||||
'useImperativeHandle',
|
||||
'useLayoutEffect',
|
||||
'useDebugValue',
|
||||
].flatMap(hookName => [hookName, `React.${hookName}`]);
|
||||
|
||||
const isReactHook = scope =>
|
||||
scope.block?.parent?.callee
|
||||
&& isNodeMatches(scope.block.parent.callee, reactHooks);
|
||||
|
||||
const isArrowFunctionNodeWithThis = (node, visitorKeys) =>
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
// We avoid `scope.thisFound` because parser scope metadata differs; AST lexical checks are consistent.
|
||||
// Include both params and body, because parameter defaults can reference lexical `this`.
|
||||
&& isNodeContainsLexicalThis(node, visitorKeys);
|
||||
|
||||
const iifeFunctionTypes = new Set([
|
||||
'FunctionExpression',
|
||||
'ArrowFunctionExpression',
|
||||
]);
|
||||
const isIife = node =>
|
||||
iifeFunctionTypes.has(node.type)
|
||||
&& node.parent.type === 'CallExpression'
|
||||
&& node.parent.callee === node;
|
||||
|
||||
// Helper to walk up the chain to find the first non-arrow ancestor
|
||||
function getNonArrowAncestor(node) {
|
||||
let ancestor = node;
|
||||
while (ancestor && ancestor.type === 'ArrowFunctionExpression') {
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
return ancestor;
|
||||
}
|
||||
|
||||
// Helper to skip over a chain of ArrowFunctionExpression nodes
|
||||
function skipArrowFunctionChain(node) {
|
||||
let current = node;
|
||||
while (current.type === 'ArrowFunctionExpression') {
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
function handleNestedArrowFunctions(parentNode, node) {
|
||||
// Skip over arrow function expressions when they are parents and we came from a ReturnStatement
|
||||
// This handles nested arrow functions: return next => action => { ... }
|
||||
// But only when we're in a return statement context
|
||||
if (parentNode.type === 'ArrowFunctionExpression' && node.type === 'ArrowFunctionExpression') {
|
||||
const ancestor = getNonArrowAncestor(parentNode);
|
||||
if (ancestor && ancestor.type === 'ReturnStatement') {
|
||||
parentNode = skipArrowFunctionChain(parentNode);
|
||||
if (parentNode.type === 'ReturnStatement') {
|
||||
parentNode = parentNode.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
function checkNode(node, scopeManager, sourceCode) {
|
||||
const scope = scopeManager.acquire(node);
|
||||
|
||||
if (
|
||||
!scope
|
||||
|| isArrowFunctionNodeWithThis(node, sourceCode.visitorKeys)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let parentNode = node.parent;
|
||||
|
||||
// Skip over junk like the block statement inside of a function declaration
|
||||
// or the various pieces of an arrow function.
|
||||
|
||||
if (parentNode.type === 'VariableDeclarator') {
|
||||
parentNode = parentNode.parent;
|
||||
}
|
||||
|
||||
if (parentNode.type === 'VariableDeclaration') {
|
||||
parentNode = parentNode.parent;
|
||||
}
|
||||
|
||||
// Only skip ReturnStatement for arrow functions
|
||||
// Regular function expressions have different semantics and shouldn't be moved
|
||||
if (parentNode?.type === 'ReturnStatement' && node.type === 'ArrowFunctionExpression') {
|
||||
parentNode = parentNode.parent;
|
||||
}
|
||||
|
||||
parentNode = handleNestedArrowFunctions(parentNode, node);
|
||||
|
||||
if (parentNode?.type === 'BlockStatement') {
|
||||
parentNode = parentNode.parent;
|
||||
}
|
||||
|
||||
const parentScope = scopeManager.acquire(parentNode);
|
||||
if (
|
||||
!parentScope
|
||||
|| parentScope.type === 'global'
|
||||
|| isReactHook(parentScope)
|
||||
|| isIife(parentNode)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkReferences(scope, parentScope, scopeManager);
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {checkArrowFunctions} = context.options[0];
|
||||
const {sourceCode} = context;
|
||||
const {scopeManager} = sourceCode;
|
||||
|
||||
const functions = [];
|
||||
|
||||
context.on(functionTypes, () => {
|
||||
functions.push(false);
|
||||
});
|
||||
|
||||
context.on('JSXElement', () => {
|
||||
// Turn off this rule if we see a JSX element because scope
|
||||
// references does not include JSXElement nodes.
|
||||
if (functions.length > 0) {
|
||||
functions[functions.length - 1] = true;
|
||||
}
|
||||
});
|
||||
|
||||
context.onExit(functionTypes, node => {
|
||||
const currentFunctionHasJsx = functions.pop();
|
||||
if (currentFunctionHasJsx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'ArrowFunctionExpression' && !checkArrowFunctions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkNode(node, scopeManager, sourceCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
loc: getFunctionHeadLocation(node, sourceCode),
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
checkArrowFunctions: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check arrow functions.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Move function definitions to the highest possible scope.',
|
||||
recommended: true,
|
||||
},
|
||||
schema,
|
||||
defaultOptions: [{checkArrowFunctions: true}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import {replaceTemplateElement} from './fix/index.js';
|
||||
import {isTaggedTemplateLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'consistent-template-literal-escape';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Use `\\${` instead of `$\\{` to escape in template literals.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('TemplateElement', node => {
|
||||
if (isTaggedTemplateLiteral(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {raw} = node.value;
|
||||
|
||||
// Match `$\{` or `\$\{` and replace with `\${`.
|
||||
// The `\\?` makes the leading backslash optional to handle both patterns.
|
||||
// The lookbehind ensures an even number of preceding backslashes (including zero).
|
||||
const fixedRaw = raw.replaceAll(
|
||||
/(?<=(?:^|[^\\])(?:\\\\)*)\\?\$\\{/g,
|
||||
String.raw`\${`,
|
||||
);
|
||||
|
||||
if (raw !== fixedRaw) {
|
||||
const problem = {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
fix: fixer => replaceTemplateElement(node, fixedRaw, context, fixer),
|
||||
};
|
||||
|
||||
return problem;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce consistent style for escaping `${` in template literals.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import {
|
||||
upperFirst,
|
||||
getParenthesizedText,
|
||||
isNodeMatchesNameOrPath,
|
||||
} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
|
||||
const messages = {
|
||||
[MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class',
|
||||
};
|
||||
|
||||
const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
|
||||
|
||||
const getClassName = name => upperFirst(name).replace(/(?:error|)$/i, 'Error');
|
||||
|
||||
const getNameProperty = className => `
|
||||
name = '${className}';
|
||||
`;
|
||||
|
||||
const getSuperClassName = superClass => {
|
||||
if (superClass?.type === 'Identifier') {
|
||||
return superClass.name;
|
||||
}
|
||||
|
||||
if (
|
||||
superClass?.type === 'MemberExpression'
|
||||
&& !superClass.computed
|
||||
&& superClass.property.type === 'Identifier'
|
||||
) {
|
||||
return superClass.property.name;
|
||||
}
|
||||
};
|
||||
|
||||
const hasValidSuperClass = node => {
|
||||
const superClassName = getSuperClassName(node.superClass);
|
||||
return Boolean(superClassName) && nameRegexp.test(superClassName);
|
||||
};
|
||||
|
||||
const isSuperExpression = node =>
|
||||
node.type === 'ExpressionStatement'
|
||||
&& node.expression.type === 'CallExpression'
|
||||
&& isNodeMatchesNameOrPath(node.expression.callee, 'super');
|
||||
|
||||
const isAssignmentExpression = (node, name) =>
|
||||
node.type === 'ExpressionStatement'
|
||||
&& node.expression.type === 'AssignmentExpression'
|
||||
&& isNodeMatchesNameOrPath(node.expression.left, `this.${name}`);
|
||||
|
||||
const createInvalidNameError = (node, name) => ({
|
||||
node,
|
||||
message: `The \`name\` property should be set to \`${name}\`.`,
|
||||
});
|
||||
|
||||
const isPropertyDefinition = (node, name) =>
|
||||
node.type === 'PropertyDefinition'
|
||||
&& !node.static
|
||||
&& !node.computed
|
||||
&& node.key.type === 'Identifier'
|
||||
&& node.key.name === name;
|
||||
|
||||
const isValidNameProperty = (nameProperty, className) =>
|
||||
nameProperty?.value
|
||||
&& nameProperty.value.value === className;
|
||||
|
||||
function * checkConstructorBody(context, constructor, name, nameProperty) {
|
||||
const {sourceCode} = context;
|
||||
const constructorBodyNode = constructor.value.body;
|
||||
|
||||
// Verify the constructor has a body (TypeScript)
|
||||
if (!constructorBodyNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const constructorBody = constructorBodyNode.body;
|
||||
|
||||
const superExpression = constructorBody.find(bodyNode => isSuperExpression(bodyNode));
|
||||
const messageExpressionIndex = constructorBody.findIndex(bodyNode => isAssignmentExpression(bodyNode, 'message'));
|
||||
|
||||
if (!superExpression) {
|
||||
yield {
|
||||
node: constructorBodyNode,
|
||||
message: 'Missing call to `super()` in constructor.',
|
||||
};
|
||||
} else if (messageExpressionIndex !== -1) {
|
||||
const expression = constructorBody[messageExpressionIndex];
|
||||
|
||||
yield {
|
||||
node: superExpression,
|
||||
message: 'Pass the error message to `super()` instead of setting `this.message`.',
|
||||
* fix(fixer) {
|
||||
if (superExpression.expression.arguments.length === 0) {
|
||||
const rhs = expression.expression.right;
|
||||
const [start] = sourceCode.getRange(superExpression);
|
||||
// This part crashes on ESLint 10, but it's still not correct.
|
||||
// There can be spaces, comments after `super`
|
||||
yield fixer.insertTextAfterRange(
|
||||
[start, start + 6],
|
||||
getParenthesizedText(rhs, context),
|
||||
);
|
||||
}
|
||||
|
||||
const start = messageExpressionIndex === 0
|
||||
? sourceCode.getRange(constructorBodyNode)[0]
|
||||
: sourceCode.getRange(constructorBody[messageExpressionIndex - 1])[1];
|
||||
const [, end] = sourceCode.getRange(expression);
|
||||
yield fixer.removeRange([start, end]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const nameExpression = constructorBody.find(bodyNode => isAssignmentExpression(bodyNode, 'name'));
|
||||
if (!nameExpression) {
|
||||
if (!isValidNameProperty(nameProperty, name)) {
|
||||
yield createInvalidNameError(nameProperty?.value ?? constructorBodyNode, name);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
nameExpression.expression.right.type !== 'Literal'
|
||||
|| nameExpression.expression.right.value !== name
|
||||
) {
|
||||
yield createInvalidNameError(nameExpression.expression.right ?? constructorBodyNode, name);
|
||||
}
|
||||
}
|
||||
|
||||
function * customErrorDefinition(context, node) {
|
||||
if (!hasValidSuperClass(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {name} = node.id;
|
||||
const className = getClassName(name);
|
||||
|
||||
if (name !== className) {
|
||||
yield {
|
||||
node: node.id,
|
||||
message: `Invalid class name, use \`${className}\`.`,
|
||||
};
|
||||
}
|
||||
|
||||
const {body} = node.body;
|
||||
const {sourceCode} = context;
|
||||
const constructor = body.find(x => x.kind === 'constructor');
|
||||
const nameProperty = body.find(classNode => isPropertyDefinition(classNode, 'name'));
|
||||
|
||||
if (!constructor) {
|
||||
if (isValidNameProperty(nameProperty, name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = sourceCode.getRange(node.body);
|
||||
yield {
|
||||
...createInvalidNameError(nameProperty?.value ?? node, name),
|
||||
fix(fixer) {
|
||||
if (nameProperty?.value) {
|
||||
return fixer.replaceText(nameProperty.value, `'${name}'`);
|
||||
}
|
||||
|
||||
if (nameProperty) {
|
||||
return fixer.replaceText(nameProperty, getNameProperty(name).trim());
|
||||
}
|
||||
|
||||
return fixer.insertTextAfterRange([
|
||||
range[0],
|
||||
range[0] + 1,
|
||||
], getNameProperty(name));
|
||||
},
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
yield * checkConstructorBody(context, constructor, name, nameProperty);
|
||||
}
|
||||
|
||||
const customErrorExport = (context, node) => {
|
||||
const exportsName = node.left.property.name;
|
||||
|
||||
const maybeError = node.right;
|
||||
|
||||
if (maybeError.type !== 'ClassExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasValidSuperClass(maybeError)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!maybeError.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume rule has already fixed the error name
|
||||
const errorName = maybeError.id.name;
|
||||
|
||||
if (exportsName === errorName) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: node.left.property,
|
||||
messageId: MESSAGE_ID_INVALID_EXPORT,
|
||||
fix: fixer => fixer.replaceText(node.left.property, errorName),
|
||||
};
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('ClassDeclaration', node => customErrorDefinition(context, node));
|
||||
context.on('AssignmentExpression', node => {
|
||||
if (node.right.type === 'ClassExpression') {
|
||||
return customErrorDefinition(context, node.right);
|
||||
}
|
||||
});
|
||||
context.on('AssignmentExpression', node => {
|
||||
if (
|
||||
node.left.type === 'MemberExpression'
|
||||
&& node.left.object.type === 'Identifier'
|
||||
&& node.left.object.name === 'exports'
|
||||
) {
|
||||
return customErrorExport(context, node);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce correct `Error` subclassing.',
|
||||
recommended: false,
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import {isOpeningBraceToken} from '@eslint-community/eslint-utils';
|
||||
|
||||
const MESSAGE_ID = 'empty-brace-spaces';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not add spaces between braces.',
|
||||
};
|
||||
|
||||
const getProblem = (node, context) => {
|
||||
const {sourceCode} = context;
|
||||
const openingBrace = sourceCode.getFirstToken(node, {filter: isOpeningBraceToken});
|
||||
const closingBrace = sourceCode.getLastToken(node);
|
||||
const [, start] = sourceCode.getRange(openingBrace);
|
||||
const [end] = sourceCode.getRange(closingBrace);
|
||||
const textBetween = sourceCode.text.slice(start, end);
|
||||
|
||||
if (!/^\s+$/.test(textBetween)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
loc: {
|
||||
start: sourceCode.getLoc(openingBrace).end,
|
||||
end: sourceCode.getLoc(closingBrace).start,
|
||||
},
|
||||
messageId: MESSAGE_ID,
|
||||
fix: fixer => fixer.removeRange([start, end]),
|
||||
};
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on([
|
||||
'BlockStatement',
|
||||
'ClassBody',
|
||||
'StaticBlock',
|
||||
'ObjectExpression',
|
||||
], node => {
|
||||
const children = node.type === 'ObjectExpression' ? node.properties : node.body;
|
||||
|
||||
if (children.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem(node, context);
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'layout',
|
||||
docs: {
|
||||
description: 'Enforce no spaces between braces.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import {getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {isCallOrNewExpression} from './ast/index.js';
|
||||
import builtinErrors from './shared/builtin-errors.js';
|
||||
|
||||
const MESSAGE_ID_MISSING_MESSAGE = 'missing-message';
|
||||
const MESSAGE_ID_EMPTY_MESSAGE = 'message-is-empty-string';
|
||||
const MESSAGE_ID_NOT_STRING = 'message-is-not-a-string';
|
||||
const messages = {
|
||||
[MESSAGE_ID_MISSING_MESSAGE]: 'Pass a message to the `{{constructorName}}` constructor.',
|
||||
[MESSAGE_ID_EMPTY_MESSAGE]: 'Error message should not be an empty string.',
|
||||
[MESSAGE_ID_NOT_STRING]: 'Error message should be a string.',
|
||||
};
|
||||
|
||||
const messageArgumentIndexes = new Map([
|
||||
['AggregateError', 1],
|
||||
['SuppressedError', 2],
|
||||
]);
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on(['CallExpression', 'NewExpression'], expression => {
|
||||
if (!(
|
||||
isCallOrNewExpression(expression, {
|
||||
names: builtinErrors,
|
||||
optional: false,
|
||||
})
|
||||
&& context.sourceCode.isGlobalReference(expression.callee)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const constructorName = expression.callee.name;
|
||||
const messageArgumentIndex = messageArgumentIndexes.has(constructorName)
|
||||
? messageArgumentIndexes.get(constructorName)
|
||||
: 0;
|
||||
const callArguments = expression.arguments;
|
||||
|
||||
// If message is `SpreadElement` or there is `SpreadElement` before message
|
||||
if (callArguments.some((node, index) => index <= messageArgumentIndex && node.type === 'SpreadElement')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = callArguments[messageArgumentIndex];
|
||||
if (!node) {
|
||||
return {
|
||||
node: expression,
|
||||
messageId: MESSAGE_ID_MISSING_MESSAGE,
|
||||
data: {constructorName},
|
||||
};
|
||||
}
|
||||
|
||||
// These types can't be string, and `getStaticValue` may don't know the value
|
||||
// Add more types, if issue reported
|
||||
if (node.type === 'ArrayExpression' || node.type === 'ObjectExpression') {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_NOT_STRING,
|
||||
};
|
||||
}
|
||||
|
||||
const staticResult = getStaticValue(node, context.sourceCode.getScope(node));
|
||||
|
||||
// We don't know the value of `message`
|
||||
if (!staticResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {value} = staticResult;
|
||||
if (typeof value !== 'string') {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_NOT_STRING,
|
||||
};
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_EMPTY_MESSAGE,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce passing a `message` value when creating a built-in error.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import {replaceTemplateElement} from './fix/index.js';
|
||||
import {isRegexLiteral, isStringLiteral, isTaggedTemplateLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID_UPPERCASE = 'escape-uppercase';
|
||||
const MESSAGE_ID_LOWERCASE = 'escape-lowercase';
|
||||
const messages = {
|
||||
[MESSAGE_ID_UPPERCASE]: 'Use uppercase characters for the value of the escape sequence.',
|
||||
[MESSAGE_ID_LOWERCASE]: 'Use lowercase characters for the value of the escape sequence.',
|
||||
};
|
||||
|
||||
const escapeCase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?<data>x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+})/g;
|
||||
const escapePatternCase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?<data>x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+}|c[A-Za-z])/g;
|
||||
const getProblem = ({node, original, regex = escapeCase, lowercase, fix}) => {
|
||||
const fixed = original.replace(regex, data => data[0] + data.slice(1)[lowercase ? 'toLowerCase' : 'toUpperCase']());
|
||||
|
||||
if (fixed !== original) {
|
||||
return {
|
||||
node,
|
||||
messageId: lowercase ? MESSAGE_ID_LOWERCASE : MESSAGE_ID_UPPERCASE,
|
||||
fix: fixer => fix ? fix(fixer, fixed) : fixer.replaceText(node, fixed),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const lowercase = context.options[0] === 'lowercase';
|
||||
|
||||
context.on('Literal', node => {
|
||||
if (isStringLiteral(node)) {
|
||||
return getProblem({
|
||||
node,
|
||||
original: node.raw,
|
||||
lowercase,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
context.on('Literal', node => {
|
||||
if (isRegexLiteral(node)) {
|
||||
return getProblem({
|
||||
node,
|
||||
original: node.raw,
|
||||
regex: escapePatternCase,
|
||||
lowercase,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
context.on('TemplateElement', node => {
|
||||
if (isTaggedTemplateLiteral(node.parent, ['String.raw'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem({
|
||||
node,
|
||||
original: node.value.raw,
|
||||
lowercase,
|
||||
fix: (fixer, fixed) => replaceTemplateElement(node, fixed, context, fixer),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
enum: ['uppercase', 'lowercase'],
|
||||
description: 'The case style for escape sequences.',
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Require escape sequences to use uppercase or lowercase values.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema,
|
||||
defaultOptions: ['uppercase'],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+577
@@ -0,0 +1,577 @@
|
||||
import path from 'node:path';
|
||||
import {isRegExp} from 'node:util/types';
|
||||
import semver from 'semver';
|
||||
import * as ci from 'ci-info';
|
||||
import {
|
||||
isEslintDisableOrEnableDirective,
|
||||
getBuiltinRule,
|
||||
} from './utils/index.js';
|
||||
import {readPackageJson} from './shared/package-json.js';
|
||||
|
||||
const baseRule = getBuiltinRule('no-warning-comments');
|
||||
|
||||
// `unicorn/` prefix is added to avoid conflicts with core rule
|
||||
const MESSAGE_ID_AVOID_MULTIPLE_DATES = 'unicorn/avoidMultipleDates';
|
||||
const MESSAGE_ID_EXPIRED_TODO = 'unicorn/expiredTodo';
|
||||
const MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS
|
||||
= 'unicorn/avoidMultiplePackageVersions';
|
||||
const MESSAGE_ID_REACHED_PACKAGE_VERSION = 'unicorn/reachedPackageVersion';
|
||||
const MESSAGE_ID_HAVE_PACKAGE = 'unicorn/havePackage';
|
||||
const MESSAGE_ID_DONT_HAVE_PACKAGE = 'unicorn/dontHavePackage';
|
||||
const MESSAGE_ID_VERSION_MATCHES = 'unicorn/versionMatches';
|
||||
const MESSAGE_ID_ENGINE_MATCHES = 'unicorn/engineMatches';
|
||||
const MESSAGE_ID_REMOVE_WHITESPACE = 'unicorn/removeWhitespaces';
|
||||
const MESSAGE_ID_MISSING_AT_SYMBOL = 'unicorn/missingAtSymbol';
|
||||
|
||||
// Override of core rule message with a more specific one - no prefix
|
||||
const MESSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT = 'unexpectedComment';
|
||||
const messages = {
|
||||
[MESSAGE_ID_AVOID_MULTIPLE_DATES]:
|
||||
'Avoid using multiple expiration dates in TODO: {{expirationDates}}. {{message}}',
|
||||
[MESSAGE_ID_EXPIRED_TODO]:
|
||||
'There is a TODO that is past due date: {{expirationDate}}. {{message}}',
|
||||
[MESSAGE_ID_REACHED_PACKAGE_VERSION]:
|
||||
'There is a TODO that is past due package version: {{comparison}}. {{message}}',
|
||||
[MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS]:
|
||||
'Avoid using multiple package versions in TODO: {{versions}}. {{message}}',
|
||||
[MESSAGE_ID_HAVE_PACKAGE]:
|
||||
'There is a TODO that is deprecated since you installed: {{package}}. {{message}}',
|
||||
[MESSAGE_ID_DONT_HAVE_PACKAGE]:
|
||||
'There is a TODO that is deprecated since you uninstalled: {{package}}. {{message}}',
|
||||
[MESSAGE_ID_VERSION_MATCHES]:
|
||||
'There is a TODO match for package version: {{comparison}}. {{message}}',
|
||||
[MESSAGE_ID_ENGINE_MATCHES]:
|
||||
'There is a TODO match for Node.js version: {{comparison}}. {{message}}',
|
||||
[MESSAGE_ID_REMOVE_WHITESPACE]:
|
||||
'Avoid using whitespace on TODO argument. On \'{{original}}\' use \'{{fix}}\'. {{message}}',
|
||||
[MESSAGE_ID_MISSING_AT_SYMBOL]:
|
||||
'Missing \'@\' on TODO argument. On \'{{original}}\' use \'{{fix}}\'. {{message}}',
|
||||
...baseRule.meta.messages,
|
||||
[MESSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT]:
|
||||
'Unexpected \'{{matchedTerm}}\' comment without any conditions: \'{{comment}}\'.',
|
||||
};
|
||||
|
||||
/** @param {string} dirname */
|
||||
function getPackageHelpers(dirname) {
|
||||
const packageJsonResult = readPackageJson(dirname);
|
||||
const packageJson = packageJsonResult?.packageJson ?? {};
|
||||
const hasPackage = Boolean(packageJsonResult);
|
||||
|
||||
const packageDependencies = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
|
||||
function parseTodoWithArguments(string, {terms}) {
|
||||
const lowerCaseString = string.toLowerCase();
|
||||
const lowerCaseTerms = terms.map(term => term.toLowerCase());
|
||||
const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term));
|
||||
|
||||
if (!hasTerm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i;
|
||||
const result = TODO_ARGUMENT_RE.exec(string);
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {rawArguments} = result.groups;
|
||||
|
||||
const parsedArguments = rawArguments
|
||||
.split(',')
|
||||
.map(argument => parseArgument(argument.trim()));
|
||||
|
||||
return createArgumentGroup(parsedArguments);
|
||||
}
|
||||
|
||||
function parseArgument(argumentString, dirname) {
|
||||
const {hasPackage} = getPackageHelpers(dirname);
|
||||
if (ISO8601_DATE.test(argumentString)) {
|
||||
return {
|
||||
type: 'dates',
|
||||
value: argumentString,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) {
|
||||
const condition = argumentString[0] === '+' ? 'in' : 'out';
|
||||
const name = argumentString.slice(1).trim();
|
||||
|
||||
return {
|
||||
type: 'dependencies',
|
||||
value: {
|
||||
name,
|
||||
condition,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (hasPackage && VERSION_COMPARISON_RE.test(argumentString)) {
|
||||
const {groups} = VERSION_COMPARISON_RE.exec(argumentString);
|
||||
const name = groups.name.trim();
|
||||
const condition = groups.condition.trim();
|
||||
const version = groups.version.trim();
|
||||
|
||||
const hasEngineKeyword = name.indexOf('engine:') === 0;
|
||||
const isNodeEngine = hasEngineKeyword && name === 'engine:node';
|
||||
|
||||
if (hasEngineKeyword && isNodeEngine) {
|
||||
return {
|
||||
type: 'engines',
|
||||
value: {
|
||||
condition,
|
||||
version,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasEngineKeyword) {
|
||||
return {
|
||||
type: 'dependencies',
|
||||
value: {
|
||||
name,
|
||||
condition,
|
||||
version,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPackage && PKG_VERSION_RE.test(argumentString)) {
|
||||
const result = PKG_VERSION_RE.exec(argumentString);
|
||||
const {condition, version} = result.groups;
|
||||
|
||||
return {
|
||||
type: 'packageVersions',
|
||||
value: {
|
||||
condition: condition.trim(),
|
||||
version: version.trim(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Currently being ignored as integration tests pointed
|
||||
// some TODO comments have `[random data like this]`
|
||||
return {
|
||||
type: 'unknowns',
|
||||
value: argumentString,
|
||||
};
|
||||
}
|
||||
|
||||
function parseTodoMessage(todoString) {
|
||||
// @example "TODO [...]: message here"
|
||||
// @example "TODO [...] message here"
|
||||
const argumentsEnd = todoString.indexOf(']');
|
||||
|
||||
const afterArguments = todoString.slice(argumentsEnd + 1).trim();
|
||||
|
||||
// Check if have to skip colon
|
||||
// @example "TODO [...]: message here"
|
||||
const dropColon = afterArguments[0] === ':';
|
||||
if (dropColon) {
|
||||
return afterArguments.slice(1).trim();
|
||||
}
|
||||
|
||||
return afterArguments;
|
||||
}
|
||||
|
||||
return {
|
||||
packageResult: packageJsonResult,
|
||||
hasPackage,
|
||||
packageJson,
|
||||
packageDependencies,
|
||||
parseArgument,
|
||||
parseTodoMessage,
|
||||
parseTodoWithArguments,
|
||||
};
|
||||
}
|
||||
|
||||
const DEPENDENCY_INCLUSION_RE = /^[+-]\s*@?\S+\/?\S+/;
|
||||
const VERSION_COMPARISON_RE = /^(?<name>@?\S\/?\S+)@(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i;
|
||||
const PKG_VERSION_RE = /^(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)\s*$/;
|
||||
const ISO8601_DATE = /\d{4}-\d{2}-\d{2}/;
|
||||
|
||||
function createArgumentGroup(arguments_) {
|
||||
const groups = {};
|
||||
for (const {value, type} of arguments_) {
|
||||
groups[type] ??= [];
|
||||
groups[type].push(value);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function reachedDate(past, now) {
|
||||
return Date.parse(past) < Date.parse(now);
|
||||
}
|
||||
|
||||
function tryToCoerceVersion(rawVersion) {
|
||||
// `version` in `package.json` and comment can't be empty
|
||||
/* c8 ignore next 3 */
|
||||
if (!rawVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let version = String(rawVersion);
|
||||
|
||||
// Remove leading things like `^1.0.0`, `>1.0.0`
|
||||
const leadingNoises = [
|
||||
'>=',
|
||||
'<=',
|
||||
'>',
|
||||
'<',
|
||||
'~',
|
||||
'^',
|
||||
];
|
||||
const foundTrailingNoise = leadingNoises.find(noise => version.startsWith(noise));
|
||||
if (foundTrailingNoise) {
|
||||
version = version.slice(foundTrailingNoise.length);
|
||||
}
|
||||
|
||||
// Get only the first member for cases such as `1.0.0 - 2.9999.9999`
|
||||
const parts = version.split(' ');
|
||||
// We don't have this `package.json` to test
|
||||
/* c8 ignore next 3 */
|
||||
if (parts.length > 1) {
|
||||
version = parts[0];
|
||||
}
|
||||
|
||||
// We don't have this `package.json` to test
|
||||
/* c8 ignore next 3 */
|
||||
if (semver.valid(version)) {
|
||||
return version;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to semver.parse a perfect match while semver.coerce tries to fix errors
|
||||
// But coerce can't parse pre-releases.
|
||||
return semver.parse(version) || semver.coerce(version);
|
||||
} catch {
|
||||
// We don't have this `package.json` to test
|
||||
/* c8 ignore next 3 */
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function satisfiesRange(version, condition, range) {
|
||||
return semver.satisfies(version, `${condition}${range}`, {includePrerelease: true});
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
terms: ['todo', 'fixme', 'xxx'],
|
||||
ignore: [],
|
||||
ignoreDates: false,
|
||||
ignoreDatesOnPullRequests: true,
|
||||
allowWarningComments: true,
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const options = {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
...context.options[0],
|
||||
};
|
||||
|
||||
const ignoreRegexes = options.ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'));
|
||||
|
||||
const dirname = path.dirname(context.filename);
|
||||
const {packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments} = getPackageHelpers(dirname);
|
||||
|
||||
const {sourceCode} = context;
|
||||
const comments = sourceCode.getAllComments();
|
||||
const unusedComments = comments
|
||||
.filter(comment => comment.type !== 'Shebang' && !isEslintDisableOrEnableDirective(context, comment))
|
||||
// Block comments come as one.
|
||||
// Split for situations like this:
|
||||
// /*
|
||||
// * TODO [2999-01-01]: Validate this
|
||||
// * TODO [2999-01-01]: And this
|
||||
// * TODO [2999-01-01]: Also this
|
||||
// */
|
||||
.flatMap(comment =>
|
||||
comment.value.split('\n').map(line => ({
|
||||
...comment,
|
||||
value: line,
|
||||
}))).filter(comment => processComment(comment));
|
||||
|
||||
// This is highly dependable on ESLint's `no-warning-comments` implementation.
|
||||
// What we do is patch the parts we know the rule will use, `getAllComments`.
|
||||
// Since we have priority, we leave only the comments that we didn't use.
|
||||
const fakeContext = new Proxy(context, {
|
||||
get(target, property, receiver) {
|
||||
if (property === 'sourceCode') {
|
||||
return {
|
||||
...sourceCode,
|
||||
getAllComments: () => options.allowWarningComments ? [] : unusedComments,
|
||||
};
|
||||
}
|
||||
|
||||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
});
|
||||
const rules = baseRule.create(fakeContext);
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function processComment(comment) {
|
||||
if (ignoreRegexes.some(ignore => ignore.test(comment.value))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseTodoWithArguments(comment.value, options);
|
||||
|
||||
if (!parsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Count if there are valid properties.
|
||||
// Otherwise, it's a useless TODO and falls back to `no-warning-comments`.
|
||||
let uses = 0;
|
||||
|
||||
const {
|
||||
packageVersions = [],
|
||||
dates = [],
|
||||
dependencies = [],
|
||||
engines = [],
|
||||
unknowns = [],
|
||||
} = parsed;
|
||||
|
||||
if (dates.length > 1) {
|
||||
uses++;
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_AVOID_MULTIPLE_DATES,
|
||||
data: {
|
||||
expirationDates: dates.join(', '),
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
} else if (dates.length === 1) {
|
||||
uses++;
|
||||
const [expirationDate] = dates;
|
||||
|
||||
const shouldIgnore = options.ignoreDates || (options.ignoreDatesOnPullRequests && ci.isPR);
|
||||
if (!shouldIgnore && reachedDate(expirationDate, options.date)) {
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_EXPIRED_TODO,
|
||||
data: {
|
||||
expirationDate,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (packageVersions.length > 1) {
|
||||
uses++;
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS,
|
||||
data: {
|
||||
versions: packageVersions
|
||||
.map(({condition, version}) => `${condition}${version}`)
|
||||
.join(', '),
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
} else if (packageVersions.length === 1) {
|
||||
uses++;
|
||||
const [{condition, version}] = packageVersions;
|
||||
|
||||
const packageVersion = tryToCoerceVersion(packageJson.version);
|
||||
if (packageVersion && satisfiesRange(packageVersion, condition, version)) {
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_REACHED_PACKAGE_VERSION,
|
||||
data: {
|
||||
comparison: `${condition}${version}`,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Inclusion: 'in', 'out'
|
||||
// Comparison: '>', '>='
|
||||
for (const dependency of dependencies) {
|
||||
uses++;
|
||||
const targetPackageRawVersion = packageDependencies[dependency.name];
|
||||
const hasTargetPackage = Boolean(targetPackageRawVersion);
|
||||
|
||||
const isInclusion = ['in', 'out'].includes(dependency.condition);
|
||||
if (isInclusion) {
|
||||
const [trigger, messageId]
|
||||
= dependency.condition === 'in'
|
||||
? [hasTargetPackage, MESSAGE_ID_HAVE_PACKAGE]
|
||||
: [!hasTargetPackage, MESSAGE_ID_DONT_HAVE_PACKAGE];
|
||||
|
||||
if (trigger) {
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId,
|
||||
data: {
|
||||
package: dependency.name,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPackageVersion = tryToCoerceVersion(targetPackageRawVersion);
|
||||
|
||||
/* c8 ignore start */
|
||||
if (!hasTargetPackage || !targetPackageVersion) {
|
||||
// Can't compare `¯\_(ツ)_/¯`
|
||||
continue;
|
||||
}
|
||||
/* c8 ignore end */
|
||||
|
||||
if (satisfiesRange(targetPackageVersion, dependency.condition, dependency.version)) {
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_VERSION_MATCHES,
|
||||
data: {
|
||||
comparison: `${dependency.name} ${dependency.condition} ${dependency.version}`,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const packageEngines = packageJson.engines || {};
|
||||
|
||||
for (const engine of engines) {
|
||||
uses++;
|
||||
|
||||
const targetPackageRawEngineVersion = packageEngines.node;
|
||||
const hasTargetEngine = Boolean(targetPackageRawEngineVersion);
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
if (!hasTargetEngine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPackageEngineVersion = tryToCoerceVersion(targetPackageRawEngineVersion);
|
||||
|
||||
if (targetPackageEngineVersion && satisfiesRange(targetPackageEngineVersion, engine.condition, engine.version)) {
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_ENGINE_MATCHES,
|
||||
data: {
|
||||
comparison: `node${engine.condition}${engine.version}`,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const unknown of unknowns) {
|
||||
// In this case, check if there's just an '@' missing before a '>' or '>='.
|
||||
const hasAt = unknown.includes('@');
|
||||
const comparisonIndex = unknown.indexOf('>');
|
||||
|
||||
if (!hasAt && comparisonIndex !== -1) {
|
||||
const testString = `${unknown.slice(
|
||||
0,
|
||||
comparisonIndex,
|
||||
)}@${unknown.slice(comparisonIndex)}`;
|
||||
|
||||
if (parseArgument(testString).type !== 'unknowns') {
|
||||
uses++;
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_MISSING_AT_SYMBOL,
|
||||
data: {
|
||||
original: unknown,
|
||||
fix: testString,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const withoutWhitespace = unknown.replaceAll(' ', '');
|
||||
|
||||
if (parseArgument(withoutWhitespace).type !== 'unknowns') {
|
||||
uses++;
|
||||
context.report({
|
||||
loc: sourceCode.getLoc(comment),
|
||||
messageId: MESSAGE_ID_REMOVE_WHITESPACE,
|
||||
data: {
|
||||
original: unknown,
|
||||
fix: withoutWhitespace,
|
||||
message: parseTodoMessage(comment.value),
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return uses === 0;
|
||||
}
|
||||
|
||||
context.on('Program', () => {
|
||||
rules.Program(); // eslint-disable-line new-cap
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
terms: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: 'Comment terms to check.',
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
description: 'Patterns to ignore.',
|
||||
},
|
||||
ignoreDates: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to ignore expiration dates.',
|
||||
},
|
||||
ignoreDatesOnPullRequests: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to ignore expiration dates on pull requests.',
|
||||
},
|
||||
allowWarningComments: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to allow warning comments.',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
description: 'The reference date.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Add expiration conditions to TODO comments.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
schema,
|
||||
defaultOptions: [{...DEFAULT_OPTIONS}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
import {getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isParenthesized,
|
||||
checkVueTemplate,
|
||||
isLogicalExpression,
|
||||
isBooleanExpression,
|
||||
isControlFlowTest,
|
||||
getBooleanAncestor,
|
||||
} from './utils/index.js';
|
||||
import {fixSpaceAroundKeyword} from './fix/index.js';
|
||||
import {isLiteral, isMemberExpression} from './ast/index.js';
|
||||
|
||||
const TYPE_NON_ZERO = 'non-zero';
|
||||
const TYPE_ZERO = 'zero';
|
||||
const MESSAGE_ID_SUGGESTION = 'suggestion';
|
||||
const messages = {
|
||||
[TYPE_NON_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is not zero.',
|
||||
[TYPE_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is zero.',
|
||||
[MESSAGE_ID_SUGGESTION]: 'Replace `.{{property}}` with `.{{property}} {{code}}`.',
|
||||
};
|
||||
|
||||
const isCompareRight = (node, operator, value) =>
|
||||
node.type === 'BinaryExpression'
|
||||
&& node.operator === operator
|
||||
&& isLiteral(node.right, value);
|
||||
const isCompareLeft = (node, operator, value) =>
|
||||
node.type === 'BinaryExpression'
|
||||
&& node.operator === operator
|
||||
&& isLiteral(node.left, value);
|
||||
const nonZeroStyles = new Map([
|
||||
[
|
||||
'greater-than',
|
||||
{
|
||||
code: '> 0',
|
||||
test: node => isCompareRight(node, '>', 0),
|
||||
},
|
||||
],
|
||||
[
|
||||
'not-equal',
|
||||
{
|
||||
code: '!== 0',
|
||||
test: node => isCompareRight(node, '!==', 0),
|
||||
},
|
||||
],
|
||||
]);
|
||||
const zeroStyle = {
|
||||
code: '=== 0',
|
||||
test: node => isCompareRight(node, '===', 0),
|
||||
};
|
||||
|
||||
function getLengthCheckNode(node) {
|
||||
node = node.parent;
|
||||
|
||||
// Zero length check
|
||||
if (
|
||||
// `foo.length === 0`
|
||||
isCompareRight(node, '===', 0)
|
||||
// `foo.length == 0`
|
||||
|| isCompareRight(node, '==', 0)
|
||||
// `foo.length < 1`
|
||||
|| isCompareRight(node, '<', 1)
|
||||
// `0 === foo.length`
|
||||
|| isCompareLeft(node, '===', 0)
|
||||
// `0 == foo.length`
|
||||
|| isCompareLeft(node, '==', 0)
|
||||
// `1 > foo.length`
|
||||
|| isCompareLeft(node, '>', 1)
|
||||
) {
|
||||
return {isZeroLengthCheck: true, node};
|
||||
}
|
||||
|
||||
// Non-Zero length check
|
||||
if (
|
||||
// `foo.length !== 0`
|
||||
isCompareRight(node, '!==', 0)
|
||||
// `foo.length != 0`
|
||||
|| isCompareRight(node, '!=', 0)
|
||||
// `foo.length > 0`
|
||||
|| isCompareRight(node, '>', 0)
|
||||
// `foo.length >= 1`
|
||||
|| isCompareRight(node, '>=', 1)
|
||||
// `0 !== foo.length`
|
||||
|| isCompareLeft(node, '!==', 0)
|
||||
// `0 !== foo.length`
|
||||
|| isCompareLeft(node, '!=', 0)
|
||||
// `0 < foo.length`
|
||||
|| isCompareLeft(node, '<', 0)
|
||||
// `1 <= foo.length`
|
||||
|| isCompareLeft(node, '<=', 1)
|
||||
) {
|
||||
return {isZeroLengthCheck: false, node};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function create(context) {
|
||||
const options = context.options[0];
|
||||
const nonZeroStyle = nonZeroStyles.get(options['non-zero']);
|
||||
const {sourceCode} = context;
|
||||
|
||||
function getProblem({node, isZeroLengthCheck, lengthNode, autoFix, shouldSuggest = true}) {
|
||||
const {code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle;
|
||||
if (test(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fixed = `${sourceCode.getText(lengthNode)} ${code}`;
|
||||
if (
|
||||
!isParenthesized(node, context)
|
||||
&& node.type === 'UnaryExpression'
|
||||
&& (node.parent.type === 'UnaryExpression' || node.parent.type === 'AwaitExpression')
|
||||
) {
|
||||
fixed = `(${fixed})`;
|
||||
}
|
||||
|
||||
const fix = function * (fixer) {
|
||||
yield fixer.replaceText(node, fixed);
|
||||
yield fixSpaceAroundKeyword(fixer, node, context);
|
||||
};
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: isZeroLengthCheck ? TYPE_ZERO : TYPE_NON_ZERO,
|
||||
data: {code, property: lengthNode.property.name},
|
||||
};
|
||||
|
||||
if (autoFix) {
|
||||
problem.fix = fix;
|
||||
} else if (shouldSuggest) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION,
|
||||
fix,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
context.on('MemberExpression', memberExpression => {
|
||||
if (
|
||||
!isMemberExpression(memberExpression, {
|
||||
properties: ['length', 'size'],
|
||||
optional: false,
|
||||
})
|
||||
|| memberExpression.object.type === 'ThisExpression'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lengthNode = memberExpression;
|
||||
const staticValue = getStaticValue(lengthNode, sourceCode.getScope(lengthNode));
|
||||
if (staticValue && (!Number.isInteger(staticValue.value) || staticValue.value < 0)) {
|
||||
// Ignore known, non-positive-integer length properties.
|
||||
return;
|
||||
}
|
||||
|
||||
let node;
|
||||
let autoFix = true;
|
||||
let {isZeroLengthCheck, node: lengthCheckNode} = getLengthCheckNode(lengthNode);
|
||||
if (lengthCheckNode) {
|
||||
const {isNegative, node: ancestor} = getBooleanAncestor(lengthCheckNode);
|
||||
node = ancestor;
|
||||
if (isNegative) {
|
||||
isZeroLengthCheck = !isZeroLengthCheck;
|
||||
}
|
||||
} else {
|
||||
const {isNegative, node: ancestor} = getBooleanAncestor(lengthNode);
|
||||
if (isBooleanExpression(ancestor) || isControlFlowTest(ancestor)) {
|
||||
isZeroLengthCheck = isNegative;
|
||||
node = ancestor;
|
||||
} else if (isLogicalExpression(lengthNode.parent) && lengthNode.parent.operator === '&&') {
|
||||
isZeroLengthCheck = isNegative;
|
||||
node = lengthNode;
|
||||
autoFix = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (node) {
|
||||
const isUnsafeNegationInBinaryExpression = node.type === 'UnaryExpression'
|
||||
&& node.operator === '!'
|
||||
&& node.parent.type === 'BinaryExpression'
|
||||
&& node.parent.left === node;
|
||||
|
||||
return getProblem({
|
||||
node,
|
||||
isZeroLengthCheck,
|
||||
lengthNode,
|
||||
autoFix: autoFix && !isUnsafeNegationInBinaryExpression,
|
||||
shouldSuggest: !isUnsafeNegationInBinaryExpression,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
'non-zero': {
|
||||
enum: [...nonZeroStyles.keys()],
|
||||
default: 'greater-than',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create: checkVueTemplate(create),
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce explicitly comparing the `length` or `size` property of a value.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema,
|
||||
defaultOptions: [{'non-zero': 'greater-than'}],
|
||||
messages,
|
||||
hasSuggestions: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
import path from 'node:path';
|
||||
import {isRegExp} from 'node:util/types';
|
||||
import {
|
||||
camelCase,
|
||||
kebabCase,
|
||||
snakeCase,
|
||||
pascalCase,
|
||||
} from 'change-case';
|
||||
import cartesianProductSamples from './utils/cartesian-product-samples.js';
|
||||
|
||||
const MESSAGE_ID = 'filename-case';
|
||||
const MESSAGE_ID_EXTENSION = 'filename-extension';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Filename is not in {{chosenCases}}. Rename it to {{renamedFilenames}}.',
|
||||
[MESSAGE_ID_EXTENSION]: 'File extension `{{extension}}` is not in lowercase. Rename it to `{{filename}}`.',
|
||||
};
|
||||
|
||||
const isIgnoredChar = char => !/^[a-z\d-_]$/i.test(char);
|
||||
const ignoredByDefault = new Set(['index.js', 'index.mjs', 'index.cjs', 'index.ts', 'index.tsx', 'index.vue']);
|
||||
const isLowerCase = string => string === string.toLowerCase();
|
||||
|
||||
const cases = {
|
||||
camelCase: {
|
||||
fn: camelCase,
|
||||
name: 'camel case',
|
||||
},
|
||||
kebabCase: {
|
||||
fn: kebabCase,
|
||||
name: 'kebab case',
|
||||
},
|
||||
snakeCase: {
|
||||
fn: snakeCase,
|
||||
name: 'snake case',
|
||||
},
|
||||
pascalCase: {
|
||||
fn: pascalCase,
|
||||
name: 'pascal case',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
Get the cases specified by the option.
|
||||
|
||||
@param {object} options
|
||||
@returns {string[]} The chosen cases.
|
||||
*/
|
||||
function getChosenCases(options) {
|
||||
if (options.case) {
|
||||
return [options.case];
|
||||
}
|
||||
|
||||
if (options.cases) {
|
||||
const cases = Object.keys(options.cases)
|
||||
.filter(cases => options.cases[cases]);
|
||||
|
||||
return cases.length > 0 ? cases : ['kebabCase'];
|
||||
}
|
||||
|
||||
return ['kebabCase'];
|
||||
}
|
||||
|
||||
function validateFilename(words, caseFunctions) {
|
||||
return words
|
||||
.filter(({ignored}) => !ignored)
|
||||
.every(({word}) => caseFunctions.some(caseFunction => caseFunction(word) === word));
|
||||
}
|
||||
|
||||
function fixFilename(words, caseFunctions, {leading, trailing}) {
|
||||
const replacements = words
|
||||
.map(({word, ignored}) => ignored ? [word] : caseFunctions.map(caseFunction => caseFunction(word)));
|
||||
|
||||
const {
|
||||
samples: combinations,
|
||||
} = cartesianProductSamples(replacements);
|
||||
|
||||
return [...new Set(combinations.map(parts => `${leading}${parts.join('')}${trailing}`))];
|
||||
}
|
||||
|
||||
function getFilenameParts(filenameWithExtension, {multipleFileExtensions}) {
|
||||
const extension = path.extname(filenameWithExtension);
|
||||
const filename = path.basename(filenameWithExtension, extension);
|
||||
const basename = filename + extension;
|
||||
|
||||
const parts = {
|
||||
basename,
|
||||
filename,
|
||||
middle: '',
|
||||
extension,
|
||||
};
|
||||
|
||||
if (multipleFileExtensions) {
|
||||
const [firstPart] = filename.split('.');
|
||||
Object.assign(parts, {
|
||||
filename: firstPart,
|
||||
middle: filename.slice(firstPart.length),
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
const leadingUnderscoresRegex = /^(?<leading>_+)(?<tailing>.*)$/;
|
||||
function splitFilename(filename) {
|
||||
const result = leadingUnderscoresRegex.exec(filename) || {groups: {}};
|
||||
const {leading = '', tailing = filename} = result.groups;
|
||||
|
||||
const words = [];
|
||||
|
||||
let lastWord;
|
||||
for (const char of tailing) {
|
||||
const isIgnored = isIgnoredChar(char);
|
||||
|
||||
if (lastWord?.ignored === isIgnored) {
|
||||
lastWord.word += char;
|
||||
} else {
|
||||
lastWord = {
|
||||
word: char,
|
||||
ignored: isIgnored,
|
||||
};
|
||||
words.push(lastWord);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
leading,
|
||||
words,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
Turns `[a, b, c]` into `a, b, or c`.
|
||||
|
||||
@param {string[]} words
|
||||
@returns {string}
|
||||
*/
|
||||
const englishishJoinWords = words => new Intl.ListFormat('en-US', {type: 'disjunction'}).format(words);
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const options = context.options[0] || {};
|
||||
const chosenCases = getChosenCases(options);
|
||||
const ignore = (options.ignore || []).map(item => {
|
||||
if (isRegExp(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return new RegExp(item, 'u');
|
||||
});
|
||||
const multipleFileExtensions = options.multipleFileExtensions !== false;
|
||||
const chosenCasesFunctions = chosenCases.map(case_ => cases[case_].fn);
|
||||
const filenameWithExtension = context.physicalFilename;
|
||||
|
||||
if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.on('Program', () => {
|
||||
const {
|
||||
basename,
|
||||
filename,
|
||||
middle,
|
||||
extension,
|
||||
} = getFilenameParts(filenameWithExtension, {multipleFileExtensions});
|
||||
|
||||
if (ignoredByDefault.has(basename) || ignore.some(regexp => regexp.test(basename))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {leading, words} = splitFilename(filename);
|
||||
const isValid = validateFilename(words, chosenCasesFunctions);
|
||||
|
||||
if (isValid) {
|
||||
if (!isLowerCase(extension)) {
|
||||
return {
|
||||
loc: {column: 0, line: 1},
|
||||
messageId: MESSAGE_ID_EXTENSION,
|
||||
data: {filename: filename + middle + extension.toLowerCase(), extension},
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const renamedFilenames = fixFilename(words, chosenCasesFunctions, {
|
||||
leading,
|
||||
trailing: middle + extension.toLowerCase(),
|
||||
});
|
||||
|
||||
return {
|
||||
// Report on first character like `unicode-bom` rule
|
||||
// https://github.com/eslint/eslint/blob/8a77b661bc921c3408bae01b3aa41579edfc6e58/lib/rules/unicode-bom.js#L46
|
||||
loc: {column: 0, line: 1},
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
chosenCases: englishishJoinWords(chosenCases.map(x => cases[x].name)),
|
||||
renamedFilenames: englishishJoinWords(renamedFilenames.map(x => `\`${x}\``)),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
properties: {
|
||||
case: {
|
||||
enum: [
|
||||
'camelCase',
|
||||
'snakeCase',
|
||||
'kebabCase',
|
||||
'pascalCase',
|
||||
],
|
||||
description: 'The filename case style.',
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
description: 'Patterns to ignore.',
|
||||
},
|
||||
multipleFileExtensions: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to treat additional, dot-separated parts of a filename as file extensions.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
cases: {
|
||||
properties: {
|
||||
camelCase: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to allow camelCase filenames.',
|
||||
},
|
||||
snakeCase: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to allow snake_case filenames.',
|
||||
},
|
||||
kebabCase: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to allow kebab-case filenames.',
|
||||
},
|
||||
pascalCase: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to allow PascalCase filenames.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
description: 'The allowed filename case styles.',
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
description: 'Patterns to ignore.',
|
||||
},
|
||||
multipleFileExtensions: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to treat additional, dot-separated parts of a filename as file extensions.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce a case style for filenames.',
|
||||
recommended: true,
|
||||
},
|
||||
schema,
|
||||
// eslint-disable-next-line eslint-plugin/require-meta-default-options
|
||||
defaultOptions: [],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import {isSemicolonToken} from '@eslint-community/eslint-utils';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESTree.ReturnStatement | ESTree.ThrowStatement} node
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * addParenthesizesToReturnOrThrowExpression(fixer, node, context) {
|
||||
if (node.type !== 'ReturnStatement' && node.type !== 'ThrowStatement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const returnOrThrowToken = sourceCode.getFirstToken(node);
|
||||
yield fixer.insertTextAfter(returnOrThrowToken, ' (');
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
if (!isSemicolonToken(lastToken)) {
|
||||
yield fixer.insertTextAfter(node, ')');
|
||||
return;
|
||||
}
|
||||
|
||||
yield fixer.insertTextBefore(lastToken, ')');
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import {isCommaToken} from '@eslint-community/eslint-utils';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESTree.CallExpression} node
|
||||
@param {string} text
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function appendArgument(fixer, node, text, context) {
|
||||
// This function should also work for `NewExpression`
|
||||
// But parentheses of `NewExpression` could be omitted, add this check to prevent accidental use on it
|
||||
/* c8 ignore next 3 */
|
||||
if (node.type !== 'CallExpression') {
|
||||
throw new Error(`Unexpected node "${node.type}".`);
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2);
|
||||
if (node.arguments.length > 0) {
|
||||
text = isCommaToken(penultimateToken) ? ` ${text},` : `, ${text}`;
|
||||
}
|
||||
|
||||
return fixer.insertTextBefore(lastToken, text);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
Extend fix range to prevent changes from other rules.
|
||||
https://github.com/eslint/eslint/pull/13748/files#diff-c692f3fde09eda7c89f1802c908511a3fb59f5d207fe95eb009cb52e46a99e84R348
|
||||
|
||||
@param {ruleFixer} fixer - The fixer to fix.
|
||||
@param {int[]} range - The extended range node.
|
||||
*/
|
||||
export default function * extendFixRange(fixer, range) {
|
||||
yield fixer.insertTextBeforeRange(range, '');
|
||||
yield fixer.insertTextAfterRange(range, '');
|
||||
}
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import {getParenthesizedRange} from '../utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
const isProblematicToken = ({type, value}) => (
|
||||
(type === 'Keyword' && /^[a-z]*$/.test(value))
|
||||
// ForOfStatement
|
||||
|| (type === 'Identifier' && value === 'of')
|
||||
// AwaitExpression
|
||||
|| (type === 'Identifier' && value === 'await')
|
||||
);
|
||||
|
||||
/**
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESTree.Node} node
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
*/
|
||||
export default function * fixSpaceAroundKeyword(fixer, node, context) {
|
||||
const {sourceCode} = context;
|
||||
const range = getParenthesizedRange(node, context);
|
||||
const tokenBefore = sourceCode.getTokenBefore({range}, {includeComments: true});
|
||||
|
||||
if (
|
||||
tokenBefore
|
||||
&& range[0] === sourceCode.getRange(tokenBefore)[1]
|
||||
&& isProblematicToken(tokenBefore)
|
||||
) {
|
||||
yield fixer.insertTextAfter(tokenBefore, ' ');
|
||||
}
|
||||
|
||||
const tokenAfter = sourceCode.getTokenAfter({range}, {includeComments: true});
|
||||
|
||||
if (
|
||||
tokenAfter
|
||||
&& range[1] === sourceCode.getRange(tokenAfter)[0]
|
||||
&& isProblematicToken(tokenAfter)
|
||||
) {
|
||||
yield fixer.insertTextBefore(tokenAfter, ' ');
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
export {default as extendFixRange} from './extend-fix-range.js';
|
||||
export {default as removeParentheses} from './remove-parentheses.js';
|
||||
export {default as appendArgument} from './append-argument.js';
|
||||
export {default as removeArgument} from './remove-argument.js';
|
||||
export {default as replaceArgument} from './replace-argument.js';
|
||||
export {default as switchNewExpressionToCallExpression} from './switch-new-expression-to-call-expression.js';
|
||||
export {default as switchCallExpressionToNewExpression} from './switch-call-expression-to-new-expression.js';
|
||||
export {
|
||||
replaceMemberExpressionProperty,
|
||||
removeMemberExpressionProperty,
|
||||
} from './replace-member-expression-property.js';
|
||||
export {default as removeMethodCall} from './remove-method-call.js';
|
||||
export {default as removeExpressionStatement} from './remove-expression-statement.js';
|
||||
export {default as removeSpacesAfter} from './remove-spaces-after.js';
|
||||
export {default as removeSpecifier} from './remove-specifier.js';
|
||||
export {default as removeObjectProperty} from './remove-object-property.js';
|
||||
export {default as renameVariable} from './rename-variable.js';
|
||||
export {default as replaceTemplateElement} from './replace-template-element.js';
|
||||
export {default as replaceReferenceIdentifier} from './replace-reference-identifier.js';
|
||||
export {default as replaceNodeOrTokenAndSpacesBefore} from './replace-node-or-token-and-spaces-before.js';
|
||||
export {default as fixSpaceAroundKeyword} from './fix-space-around-keywords.js';
|
||||
export {default as replaceStringRaw} from './replace-string-raw.js';
|
||||
export {default as addParenthesizesToReturnOrThrowExpression} from './add-parenthesizes-to-return-or-throw-expression.js';
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import {isCommaToken} from '@eslint-community/eslint-utils';
|
||||
import {getParentheses} from '../utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESTree.NewExpression | ESTree.CallExpression} node
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function removeArgument(fixer, node, context) {
|
||||
const callOrNewExpression = node.parent;
|
||||
const index = callOrNewExpression.arguments.indexOf(node);
|
||||
const parentheses = getParentheses(node, context);
|
||||
const firstToken = parentheses[0] || node;
|
||||
const lastToken = parentheses.at(-1) || node;
|
||||
const {sourceCode} = context;
|
||||
|
||||
let [start] = sourceCode.getRange(firstToken);
|
||||
let [, end] = sourceCode.getRange(lastToken);
|
||||
|
||||
if (index !== 0) {
|
||||
const commaToken = sourceCode.getTokenBefore(firstToken);
|
||||
[start] = sourceCode.getRange(commaToken);
|
||||
}
|
||||
|
||||
// If the removed argument is the only argument, the trailing comma must be removed too
|
||||
if (callOrNewExpression.arguments.length === 1) {
|
||||
const tokenAfter = sourceCode.getTokenAfter(lastToken);
|
||||
if (isCommaToken(tokenAfter)) {
|
||||
[, end] = sourceCode.getRange(tokenAfter);
|
||||
}
|
||||
}
|
||||
|
||||
return fixer.removeRange([start, end]);
|
||||
}
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
import {isSemicolonToken} from '@eslint-community/eslint-utils';
|
||||
|
||||
const isWhitespaceOnly = text => /^\s*$/.test(text);
|
||||
|
||||
function removeExpressionStatement(expressionStatement, context, fixer, preserveSemiColon = false) {
|
||||
const {sourceCode} = context;
|
||||
const {lines} = sourceCode;
|
||||
let endToken = expressionStatement;
|
||||
|
||||
if (preserveSemiColon) {
|
||||
const [penultimateToken, lastToken] = sourceCode.getLastTokens(expressionStatement, 2);
|
||||
|
||||
if (isSemicolonToken(lastToken)) {
|
||||
endToken = penultimateToken;
|
||||
}
|
||||
}
|
||||
|
||||
const startLocation = sourceCode.getLoc(expressionStatement).start;
|
||||
const endLocation = sourceCode.getLoc(endToken).end;
|
||||
|
||||
const textBefore = lines[startLocation.line - 1].slice(0, startLocation.column);
|
||||
const textAfter = lines[endLocation.line - 1].slice(endLocation.column);
|
||||
|
||||
let [start] = sourceCode.getRange(expressionStatement);
|
||||
let [, end] = sourceCode.getRange(endToken);
|
||||
|
||||
if (isWhitespaceOnly(textBefore) && isWhitespaceOnly(textAfter)) {
|
||||
start = Math.max(0, start - textBefore.length - 1);
|
||||
end += textAfter.length;
|
||||
}
|
||||
|
||||
return fixer.removeRange([start, end]);
|
||||
}
|
||||
|
||||
export default removeExpressionStatement;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import {getParenthesizedRange} from '../utils/index.js';
|
||||
import {removeMemberExpressionProperty} from './replace-member-expression-property.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESTree.CallExpression} callExpression
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * removeMethodCall(fixer, callExpression, context) {
|
||||
const memberExpression = callExpression.callee;
|
||||
|
||||
// `(( (( foo )).bar ))()`
|
||||
// ^^^^
|
||||
yield removeMemberExpressionProperty(fixer, memberExpression, context);
|
||||
|
||||
// `(( (( foo )).bar ))()`
|
||||
// ^^
|
||||
const [, start] = getParenthesizedRange(memberExpression, context);
|
||||
const [, end] = context.sourceCode.getRange(callExpression);
|
||||
|
||||
yield fixer.removeRange([start, end]);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import {isCommaToken} from '@eslint-community/eslint-utils';
|
||||
|
||||
export default function * removeObjectProperty(fixer, property, context) {
|
||||
const {sourceCode} = context;
|
||||
for (const token of sourceCode.getTokens(property)) {
|
||||
yield fixer.remove(token);
|
||||
}
|
||||
|
||||
const tokenAfter = sourceCode.getTokenAfter(property);
|
||||
if (isCommaToken(tokenAfter)) {
|
||||
yield fixer.remove(tokenAfter);
|
||||
} else {
|
||||
// If the property is the last one and there is no trailing comma
|
||||
// remove the previous comma
|
||||
const {properties} = property.parent;
|
||||
if (properties.length > 1 && properties.at(-1) === property) {
|
||||
const commaTokenBefore = sourceCode.getTokenBefore(property);
|
||||
yield fixer.remove(commaTokenBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {getParentheses} from '../utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESTree.Node} node
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * removeParentheses(node, fixer, context) {
|
||||
const parentheses = getParentheses(node, context);
|
||||
for (const token of parentheses) {
|
||||
yield fixer.remove(token);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESTree.Node | ESTree.Token | number} indexOrNodeOrToken
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
|
||||
export default function removeSpacesAfter(indexOrNodeOrToken, context, fixer) {
|
||||
let index = indexOrNodeOrToken;
|
||||
if (typeof indexOrNodeOrToken === 'object') {
|
||||
index = context.sourceCode.getRange(indexOrNodeOrToken)[1];
|
||||
}
|
||||
|
||||
const textAfter = context.sourceCode.text.slice(index);
|
||||
const [leadingSpaces] = textAfter.match(/^\s*/);
|
||||
return fixer.removeRange([index, index + leadingSpaces.length]);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import {isCommaToken, isOpeningBraceToken} from '@eslint-community/eslint-utils';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESTree.ImportSpecifier | ESTree.ExportSpecifier | ESTree.ImportDefaultSpecifier | ESTree.ImportNamespaceSpecifier} specifier
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@param {boolean} [keepDeclaration = false]
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * removeSpecifier(specifier, fixer, context, keepDeclaration = false) {
|
||||
const declaration = specifier.parent;
|
||||
const {specifiers} = declaration;
|
||||
|
||||
if (specifiers.length === 1 && !keepDeclaration) {
|
||||
yield fixer.remove(declaration);
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
switch (specifier.type) {
|
||||
case 'ImportSpecifier': {
|
||||
const isTheOnlyNamedImport = specifiers.every(node => specifier === node || specifier.type !== node.type);
|
||||
if (isTheOnlyNamedImport) {
|
||||
const fromToken = sourceCode.getTokenAfter(specifier, token => token.type === 'Identifier' && token.value === 'from');
|
||||
|
||||
const hasDefaultImport = specifiers.some(node => node.type === 'ImportDefaultSpecifier');
|
||||
const startToken = sourceCode.getTokenBefore(specifier, hasDefaultImport ? isCommaToken : isOpeningBraceToken);
|
||||
const [start] = sourceCode.getRange(startToken);
|
||||
const [end] = sourceCode.getRange(fromToken);
|
||||
const tokenBefore = sourceCode.getTokenBefore(startToken);
|
||||
const shouldInsertSpace = sourceCode.getRange(tokenBefore)[1] === start;
|
||||
|
||||
yield fixer.replaceTextRange([start, end], shouldInsertSpace ? ' ' : '');
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
}
|
||||
|
||||
case 'ExportSpecifier':
|
||||
case 'ImportNamespaceSpecifier':
|
||||
case 'ImportDefaultSpecifier': {
|
||||
yield fixer.remove(specifier);
|
||||
|
||||
const tokenAfter = sourceCode.getTokenAfter(specifier);
|
||||
if (isCommaToken(tokenAfter)) {
|
||||
yield fixer.remove(tokenAfter);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// No default
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import getVariableIdentifiers from '../utils/get-variable-identifiers.js';
|
||||
import replaceReferenceIdentifier from './replace-reference-identifier.js';
|
||||
|
||||
const renameVariable = (variable, name, context, fixer) =>
|
||||
getVariableIdentifiers(variable)
|
||||
.map(identifier => replaceReferenceIdentifier(identifier, name, context, fixer));
|
||||
|
||||
export default renameVariable;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import {getParenthesizedRange} from '../utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESTree.CallExpression | ESTree.NewExpression} node
|
||||
@param {string} text
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function replaceArgument(fixer, node, text, context) {
|
||||
return fixer.replaceTextRange(getParenthesizedRange(node, context), text);
|
||||
}
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import {getParenthesizedRange} from '../utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESTree.MemberExpression} memberExpression - The `MemberExpression` to fix.
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@param {string} text
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export function replaceMemberExpressionProperty(fixer, memberExpression, context, text) {
|
||||
const [, start] = getParenthesizedRange(memberExpression.object, context);
|
||||
const [, end] = context.sourceCode.getRange(memberExpression);
|
||||
return fixer.replaceTextRange([start, end], text);
|
||||
}
|
||||
|
||||
/**
|
||||
@param {ESTree.MemberExpression} memberExpression - The `MemberExpression` to fix.
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export const removeMemberExpressionProperty = (fixer, memberExpression, context) => replaceMemberExpressionProperty(fixer, memberExpression, context, '');
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
import {getParentheses} from '../utils/index.js';
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESTree.Node | ESTree.Token} nodeOrToken
|
||||
@param {string} replacement
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@param {ESLint.SourceCode} [tokenStore]
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * replaceNodeOrTokenAndSpacesBefore(nodeOrToken, replacement, fixer, context, tokenStore) {
|
||||
const tokens = getParentheses(nodeOrToken, tokenStore ? {sourceCode: tokenStore} : context);
|
||||
|
||||
for (const token of tokens) {
|
||||
yield replaceNodeOrTokenAndSpacesBefore(token, '', fixer, context, tokenStore);
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
let [start, end] = sourceCode.getRange(nodeOrToken);
|
||||
|
||||
const textBefore = sourceCode.text.slice(0, start);
|
||||
const [trailingSpaces] = textBefore.match(/\s*$/);
|
||||
const [lineBreak] = trailingSpaces.match(/(?:\r?\n|\r){0,1}/);
|
||||
start -= trailingSpaces.length;
|
||||
|
||||
yield fixer.replaceTextRange([start, end], `${lineBreak}${replacement}`);
|
||||
}
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
import isShorthandPropertyValue from '../utils/is-shorthand-property-value.js';
|
||||
import isShorthandPropertyAssignmentPatternLeft from '../utils/is-shorthand-property-assignment-pattern-left.js';
|
||||
import isShorthandImportLocal from '../utils/is-shorthand-import-local.js';
|
||||
import isShorthandExportLocal from '../utils/is-shorthand-export-local.js';
|
||||
|
||||
export default function replaceReferenceIdentifier(identifier, replacement, context, fixer) {
|
||||
if (
|
||||
isShorthandPropertyValue(identifier)
|
||||
|| isShorthandPropertyAssignmentPatternLeft(identifier)
|
||||
) {
|
||||
return fixer.replaceText(identifier, `${identifier.name}: ${replacement}`);
|
||||
}
|
||||
|
||||
if (isShorthandImportLocal(identifier, context)) {
|
||||
return fixer.replaceText(identifier, `${identifier.name} as ${replacement}`);
|
||||
}
|
||||
|
||||
if (isShorthandExportLocal(identifier, context)) {
|
||||
return fixer.replaceText(identifier, `${replacement} as ${identifier.name}`);
|
||||
}
|
||||
|
||||
// `typeAnnotation`
|
||||
if (identifier.typeAnnotation) {
|
||||
const {sourceCode} = context;
|
||||
return fixer.replaceTextRange(
|
||||
[sourceCode.getRange(identifier)[0], sourceCode.getRange(identifier.typeAnnotation)[0]],
|
||||
`${replacement}${identifier.optional ? '?' : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
return fixer.replaceText(identifier, replacement);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Replace `StringLiteral` or `TemplateLiteral` node with raw text
|
||||
const replaceStringRaw = (node, raw, context, fixer) =>
|
||||
fixer.replaceTextRange(
|
||||
// Ignore quotes and backticks
|
||||
[
|
||||
context.sourceCode.getRange(node)[0] + 1,
|
||||
context.sourceCode.getRange(node)[1] - 1,
|
||||
],
|
||||
raw,
|
||||
);
|
||||
|
||||
export default replaceStringRaw;
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
const replaceTemplateElement = (node, replacement, context, fixer) => {
|
||||
const {tail} = node;
|
||||
const [start, end] = context.sourceCode.getRange(node);
|
||||
return fixer.replaceTextRange(
|
||||
[start + 1, end - (tail ? 1 : 2)],
|
||||
replacement,
|
||||
);
|
||||
};
|
||||
|
||||
export default replaceTemplateElement;
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
isParenthesized,
|
||||
shouldAddParenthesesToNewExpressionCallee,
|
||||
} from '../utils/index.js';
|
||||
import fixSpaceAroundKeyword from './fix-space-around-keywords.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@param {ESTree.CallExpression} node
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * switchCallExpressionToNewExpression(node, context, fixer) {
|
||||
yield fixSpaceAroundKeyword(fixer, node, context);
|
||||
yield fixer.insertTextBefore(node, 'new ');
|
||||
|
||||
const {callee} = node;
|
||||
if (
|
||||
!isParenthesized(callee, context)
|
||||
&& shouldAddParenthesesToNewExpressionCallee(callee)
|
||||
) {
|
||||
yield fixer.insertTextBefore(callee, '(');
|
||||
yield fixer.insertTextAfter(callee, ')');
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
isNewExpressionWithParentheses,
|
||||
isParenthesized,
|
||||
isOnSameLine,
|
||||
} from '../utils/index.js';
|
||||
import addParenthesizesToReturnOrThrowExpression from './add-parenthesizes-to-return-or-throw-expression.js';
|
||||
import removeSpaceAfter from './remove-spaces-after.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {ESTree.NewExpression} newExpression
|
||||
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
||||
@param {ESLint.Rule.RuleFixer} fixer
|
||||
@returns {ESLint.Rule.ReportFixer}
|
||||
*/
|
||||
export default function * switchNewExpressionToCallExpression(newExpression, context, fixer) {
|
||||
const newToken = context.sourceCode.getFirstToken(newExpression);
|
||||
yield fixer.remove(newToken);
|
||||
yield removeSpaceAfter(newToken, context, fixer);
|
||||
|
||||
if (!isNewExpressionWithParentheses(newExpression, context)) {
|
||||
yield fixer.insertTextAfter(newExpression, '()');
|
||||
}
|
||||
|
||||
/*
|
||||
Remove `new` from this code will makes the function return `undefined`
|
||||
|
||||
```js
|
||||
() => {
|
||||
return new // comment
|
||||
Foo()
|
||||
}
|
||||
```
|
||||
*/
|
||||
if (!isOnSameLine(newToken, newExpression.callee, context) && !isParenthesized(newExpression, context)) {
|
||||
// Ideally, we should use first parenthesis of the `callee`, and should check spaces after the `new` token
|
||||
// But adding extra parentheses is harmless, no need to be too complicated
|
||||
yield addParenthesizesToReturnOrThrowExpression(fixer, newExpression.parent, context);
|
||||
}
|
||||
}
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
import {getStringIfConstant} from '@eslint-community/eslint-utils';
|
||||
import {isCallExpression} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'importStyle';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.',
|
||||
};
|
||||
|
||||
const getActualImportDeclarationStyles = importDeclaration => {
|
||||
const {specifiers} = importDeclaration;
|
||||
|
||||
if (specifiers.length === 0) {
|
||||
return ['unassigned'];
|
||||
}
|
||||
|
||||
const styles = new Set();
|
||||
|
||||
for (const specifier of specifiers) {
|
||||
if (specifier.type === 'ImportDefaultSpecifier') {
|
||||
styles.add('default');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (specifier.type === 'ImportNamespaceSpecifier') {
|
||||
styles.add('namespace');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (specifier.type === 'ImportSpecifier') {
|
||||
if (specifier.imported.type === 'Identifier' && specifier.imported.name === 'default') {
|
||||
styles.add('default');
|
||||
continue;
|
||||
}
|
||||
|
||||
styles.add('named');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return [...styles];
|
||||
};
|
||||
|
||||
const getActualExportDeclarationStyles = exportDeclaration => {
|
||||
const {specifiers} = exportDeclaration;
|
||||
|
||||
if (specifiers.length === 0) {
|
||||
return ['unassigned'];
|
||||
}
|
||||
|
||||
const styles = new Set();
|
||||
|
||||
for (const specifier of specifiers) {
|
||||
if (specifier.type === 'ExportSpecifier') {
|
||||
if (specifier.exported.type === 'Identifier' && specifier.exported.name === 'default') {
|
||||
styles.add('default');
|
||||
continue;
|
||||
}
|
||||
|
||||
styles.add('named');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return [...styles];
|
||||
};
|
||||
|
||||
const getActualAssignmentTargetImportStyles = assignmentTarget => {
|
||||
if (assignmentTarget.type === 'Identifier' || assignmentTarget.type === 'ArrayPattern') {
|
||||
return ['namespace'];
|
||||
}
|
||||
|
||||
if (assignmentTarget.type === 'ObjectPattern') {
|
||||
if (assignmentTarget.properties.length === 0) {
|
||||
return ['unassigned'];
|
||||
}
|
||||
|
||||
const styles = new Set();
|
||||
|
||||
for (const property of assignmentTarget.properties) {
|
||||
if (property.type === 'RestElement') {
|
||||
styles.add('named');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.key.type === 'Identifier') {
|
||||
if (property.key.name === 'default') {
|
||||
styles.add('default');
|
||||
} else {
|
||||
styles.add('named');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...styles];
|
||||
}
|
||||
|
||||
// Next line is not test-coverable until unforceable changes to the language
|
||||
// like an addition of new AST node types usable in `const __HERE__ = foo;`.
|
||||
// An exotic custom parser or a bug in one could cover it too.
|
||||
/* c8 ignore next */
|
||||
return [];
|
||||
};
|
||||
|
||||
const isAssignedDynamicImport = node =>
|
||||
node.parent.type === 'AwaitExpression'
|
||||
&& node.parent.argument === node
|
||||
&& node.parent.parent.type === 'VariableDeclarator'
|
||||
&& node.parent.parent.init === node.parent;
|
||||
|
||||
// Keep this alphabetically sorted for easier maintenance
|
||||
const defaultStyles = {
|
||||
chalk: {
|
||||
default: true,
|
||||
},
|
||||
path: {
|
||||
default: true,
|
||||
},
|
||||
'node:path': {
|
||||
default: true,
|
||||
},
|
||||
util: {
|
||||
named: true,
|
||||
},
|
||||
'node:util': {
|
||||
named: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
let [
|
||||
{
|
||||
styles = {},
|
||||
extendDefaultStyles = true,
|
||||
checkImport = true,
|
||||
checkDynamicImport = true,
|
||||
checkExportFrom = false,
|
||||
checkRequire = true,
|
||||
} = {},
|
||||
] = context.options;
|
||||
|
||||
styles = extendDefaultStyles
|
||||
? Object.fromEntries([...Object.keys(defaultStyles), ...Object.keys(styles)]
|
||||
.map(name => [name, styles[name] === false ? {} : {...defaultStyles[name], ...styles[name]}]))
|
||||
: styles;
|
||||
|
||||
styles = new Map(Object.entries(styles).map(([moduleName, styles]) =>
|
||||
[moduleName, new Set(Object.entries(styles).filter(([, isAllowed]) => isAllowed).map(([style]) => style))]));
|
||||
|
||||
const {sourceCode} = context;
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => {
|
||||
if (!allowedImportStyles || allowedImportStyles.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let effectiveAllowedImportStyles = allowedImportStyles;
|
||||
|
||||
// For `require`, `'default'` style allows both `x = require('x')` (`'namespace'` style) and
|
||||
// `{default: x} = require('x')` (`'default'` style) since we don't know in advance
|
||||
// whether `'x'` is a compiled ES6 module (with `default` key) or a CommonJS module and `require`
|
||||
// does not provide any automatic interop for this, so the user may have to use either of these.
|
||||
if (isRequire && allowedImportStyles.has('default') && !allowedImportStyles.has('namespace')) {
|
||||
effectiveAllowedImportStyles = new Set(allowedImportStyles);
|
||||
effectiveAllowedImportStyles.add('namespace');
|
||||
}
|
||||
|
||||
if (actualImportStyles.every(style => effectiveAllowedImportStyles.has(style))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
allowedStyles: new Intl.ListFormat('en-US', {type: 'disjunction'}).format([...allowedImportStyles.keys()]),
|
||||
moduleName,
|
||||
};
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
if (checkImport) {
|
||||
context.on('ImportDeclaration', node => {
|
||||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
|
||||
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = getActualImportDeclarationStyles(node);
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles);
|
||||
});
|
||||
}
|
||||
|
||||
if (checkDynamicImport) {
|
||||
context.on('ImportExpression', node => {
|
||||
if (isAssignedDynamicImport(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = ['unassigned'];
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles);
|
||||
});
|
||||
|
||||
context.on('VariableDeclarator', node => {
|
||||
if (!(
|
||||
node.init?.type === 'AwaitExpression'
|
||||
&& node.init.argument.type === 'ImportExpression'
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assignmentTargetNode = node.id;
|
||||
const moduleNameNode = node.init.argument.source;
|
||||
const moduleName = getStringIfConstant(moduleNameNode, sourceCode.getScope(moduleNameNode));
|
||||
|
||||
if (!moduleName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles);
|
||||
});
|
||||
}
|
||||
|
||||
if (checkExportFrom) {
|
||||
context.on('ExportAllDeclaration', node => {
|
||||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
|
||||
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = ['namespace'];
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles);
|
||||
});
|
||||
|
||||
context.on('ExportNamedDeclaration', node => {
|
||||
if (!node.source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
|
||||
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = getActualExportDeclarationStyles(node);
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles);
|
||||
});
|
||||
}
|
||||
|
||||
if (checkRequire) {
|
||||
context.on('CallExpression', node => {
|
||||
if (!(
|
||||
isCallExpression(node, {
|
||||
name: 'require',
|
||||
argumentsLength: 1,
|
||||
optional: false,
|
||||
})
|
||||
&& (node.parent.type === 'ExpressionStatement' && node.parent.expression === node)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = getStringIfConstant(node.arguments[0], sourceCode.getScope(node.arguments[0]));
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = ['unassigned'];
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles, true);
|
||||
});
|
||||
|
||||
context.on('VariableDeclarator', node => {
|
||||
if (!(
|
||||
node.init?.type === 'CallExpression'
|
||||
&& node.init.callee.type === 'Identifier'
|
||||
&& node.init.callee.name === 'require'
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assignmentTargetNode = node.id;
|
||||
const moduleNameNode = node.init.arguments[0];
|
||||
const moduleName = getStringIfConstant(moduleNameNode, sourceCode.getScope(moduleNameNode));
|
||||
|
||||
if (!moduleName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedImportStyles = styles.get(moduleName);
|
||||
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
|
||||
|
||||
report(node, moduleName, actualImportStyles, allowedImportStyles, true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const schema = {
|
||||
type: 'array',
|
||||
additionalItems: false,
|
||||
items: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
checkImport: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check `import` statements.',
|
||||
},
|
||||
checkDynamicImport: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check dynamic `import()` expressions.',
|
||||
},
|
||||
checkExportFrom: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check `export … from` statements.',
|
||||
},
|
||||
checkRequire: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check `require()` calls.',
|
||||
},
|
||||
extendDefaultStyles: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to extend the default styles.',
|
||||
},
|
||||
styles: {
|
||||
$ref: '#/definitions/moduleStyles',
|
||||
description: 'Module import styles.',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
definitions: {
|
||||
moduleStyles: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
$ref: '#/definitions/styles',
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
{
|
||||
$ref: '#/definitions/booleanObject',
|
||||
},
|
||||
],
|
||||
},
|
||||
booleanObject: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce specific import styles per module.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
schema,
|
||||
defaultOptions: [{}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Generated file, DO NOT edit
|
||||
|
||||
export {default as 'better-regex'} from './better-regex.js';
|
||||
export {default as 'catch-error-name'} from './catch-error-name.js';
|
||||
export {default as 'consistent-assert'} from './consistent-assert.js';
|
||||
export {default as 'consistent-date-clone'} from './consistent-date-clone.js';
|
||||
export {default as 'consistent-destructuring'} from './consistent-destructuring.js';
|
||||
export {default as 'consistent-empty-array-spread'} from './consistent-empty-array-spread.js';
|
||||
export {default as 'consistent-existence-index-check'} from './consistent-existence-index-check.js';
|
||||
export {default as 'consistent-function-scoping'} from './consistent-function-scoping.js';
|
||||
export {default as 'consistent-template-literal-escape'} from './consistent-template-literal-escape.js';
|
||||
export {default as 'custom-error-definition'} from './custom-error-definition.js';
|
||||
export {default as 'empty-brace-spaces'} from './empty-brace-spaces.js';
|
||||
export {default as 'error-message'} from './error-message.js';
|
||||
export {default as 'escape-case'} from './escape-case.js';
|
||||
export {default as 'expiring-todo-comments'} from './expiring-todo-comments.js';
|
||||
export {default as 'explicit-length-check'} from './explicit-length-check.js';
|
||||
export {default as 'filename-case'} from './filename-case.js';
|
||||
export {default as 'import-style'} from './import-style.js';
|
||||
export {default as 'isolated-functions'} from './isolated-functions.js';
|
||||
export {default as 'new-for-builtins'} from './new-for-builtins.js';
|
||||
export {default as 'no-abusive-eslint-disable'} from './no-abusive-eslint-disable.js';
|
||||
export {default as 'no-accessor-recursion'} from './no-accessor-recursion.js';
|
||||
export {default as 'no-anonymous-default-export'} from './no-anonymous-default-export.js';
|
||||
export {default as 'no-array-callback-reference'} from './no-array-callback-reference.js';
|
||||
export {default as 'no-array-for-each'} from './no-array-for-each.js';
|
||||
export {default as 'no-array-method-this-argument'} from './no-array-method-this-argument.js';
|
||||
export {default as 'no-array-reduce'} from './no-array-reduce.js';
|
||||
export {default as 'no-array-reverse'} from './no-array-reverse.js';
|
||||
export {default as 'no-array-sort'} from './no-array-sort.js';
|
||||
export {default as 'no-await-expression-member'} from './no-await-expression-member.js';
|
||||
export {default as 'no-await-in-promise-methods'} from './no-await-in-promise-methods.js';
|
||||
export {default as 'no-console-spaces'} from './no-console-spaces.js';
|
||||
export {default as 'no-document-cookie'} from './no-document-cookie.js';
|
||||
export {default as 'no-empty-file'} from './no-empty-file.js';
|
||||
export {default as 'no-for-loop'} from './no-for-loop.js';
|
||||
export {default as 'no-hex-escape'} from './no-hex-escape.js';
|
||||
export {default as 'no-immediate-mutation'} from './no-immediate-mutation.js';
|
||||
export {default as 'no-instanceof-builtins'} from './no-instanceof-builtins.js';
|
||||
export {default as 'no-invalid-fetch-options'} from './no-invalid-fetch-options.js';
|
||||
export {default as 'no-invalid-remove-event-listener'} from './no-invalid-remove-event-listener.js';
|
||||
export {default as 'no-keyword-prefix'} from './no-keyword-prefix.js';
|
||||
export {default as 'no-lonely-if'} from './no-lonely-if.js';
|
||||
export {default as 'no-magic-array-flat-depth'} from './no-magic-array-flat-depth.js';
|
||||
export {default as 'no-named-default'} from './no-named-default.js';
|
||||
export {default as 'no-negated-condition'} from './no-negated-condition.js';
|
||||
export {default as 'no-negation-in-equality-check'} from './no-negation-in-equality-check.js';
|
||||
export {default as 'no-nested-ternary'} from './no-nested-ternary.js';
|
||||
export {default as 'no-new-array'} from './no-new-array.js';
|
||||
export {default as 'no-new-buffer'} from './no-new-buffer.js';
|
||||
export {default as 'no-null'} from './no-null.js';
|
||||
export {default as 'no-object-as-default-parameter'} from './no-object-as-default-parameter.js';
|
||||
export {default as 'no-process-exit'} from './no-process-exit.js';
|
||||
export {default as 'no-single-promise-in-promise-methods'} from './no-single-promise-in-promise-methods.js';
|
||||
export {default as 'no-static-only-class'} from './no-static-only-class.js';
|
||||
export {default as 'no-thenable'} from './no-thenable.js';
|
||||
export {default as 'no-this-assignment'} from './no-this-assignment.js';
|
||||
export {default as 'no-typeof-undefined'} from './no-typeof-undefined.js';
|
||||
export {default as 'no-unnecessary-array-flat-depth'} from './no-unnecessary-array-flat-depth.js';
|
||||
export {default as 'no-unnecessary-array-splice-count'} from './no-unnecessary-array-splice-count.js';
|
||||
export {default as 'no-unnecessary-await'} from './no-unnecessary-await.js';
|
||||
export {default as 'no-unnecessary-polyfills'} from './no-unnecessary-polyfills.js';
|
||||
export {default as 'no-unnecessary-slice-end'} from './no-unnecessary-slice-end.js';
|
||||
export {default as 'no-unreadable-array-destructuring'} from './no-unreadable-array-destructuring.js';
|
||||
export {default as 'no-unreadable-iife'} from './no-unreadable-iife.js';
|
||||
export {default as 'no-unused-properties'} from './no-unused-properties.js';
|
||||
export {default as 'no-useless-collection-argument'} from './no-useless-collection-argument.js';
|
||||
export {default as 'no-useless-error-capture-stack-trace'} from './no-useless-error-capture-stack-trace.js';
|
||||
export {default as 'no-useless-fallback-in-spread'} from './no-useless-fallback-in-spread.js';
|
||||
export {default as 'no-useless-iterator-to-array'} from './no-useless-iterator-to-array.js';
|
||||
export {default as 'no-useless-length-check'} from './no-useless-length-check.js';
|
||||
export {default as 'no-useless-promise-resolve-reject'} from './no-useless-promise-resolve-reject.js';
|
||||
export {default as 'no-useless-spread'} from './no-useless-spread.js';
|
||||
export {default as 'no-useless-switch-case'} from './no-useless-switch-case.js';
|
||||
export {default as 'no-useless-undefined'} from './no-useless-undefined.js';
|
||||
export {default as 'no-zero-fractions'} from './no-zero-fractions.js';
|
||||
export {default as 'number-literal-case'} from './number-literal-case.js';
|
||||
export {default as 'numeric-separators-style'} from './numeric-separators-style.js';
|
||||
export {default as 'prefer-add-event-listener'} from './prefer-add-event-listener.js';
|
||||
export {default as 'prefer-array-find'} from './prefer-array-find.js';
|
||||
export {default as 'prefer-array-flat-map'} from './prefer-array-flat-map.js';
|
||||
export {default as 'prefer-array-flat'} from './prefer-array-flat.js';
|
||||
export {default as 'prefer-array-index-of'} from './prefer-array-index-of.js';
|
||||
export {default as 'prefer-array-some'} from './prefer-array-some.js';
|
||||
export {default as 'prefer-at'} from './prefer-at.js';
|
||||
export {default as 'prefer-bigint-literals'} from './prefer-bigint-literals.js';
|
||||
export {default as 'prefer-blob-reading-methods'} from './prefer-blob-reading-methods.js';
|
||||
export {default as 'prefer-class-fields'} from './prefer-class-fields.js';
|
||||
export {default as 'prefer-classlist-toggle'} from './prefer-classlist-toggle.js';
|
||||
export {default as 'prefer-code-point'} from './prefer-code-point.js';
|
||||
export {default as 'prefer-date-now'} from './prefer-date-now.js';
|
||||
export {default as 'prefer-default-parameters'} from './prefer-default-parameters.js';
|
||||
export {default as 'prefer-dom-node-append'} from './prefer-dom-node-append.js';
|
||||
export {default as 'prefer-dom-node-dataset'} from './prefer-dom-node-dataset.js';
|
||||
export {default as 'prefer-dom-node-remove'} from './prefer-dom-node-remove.js';
|
||||
export {default as 'prefer-dom-node-text-content'} from './prefer-dom-node-text-content.js';
|
||||
export {default as 'prefer-event-target'} from './prefer-event-target.js';
|
||||
export {default as 'prefer-export-from'} from './prefer-export-from.js';
|
||||
export {default as 'prefer-global-this'} from './prefer-global-this.js';
|
||||
export {default as 'prefer-import-meta-properties'} from './prefer-import-meta-properties.js';
|
||||
export {default as 'prefer-includes'} from './prefer-includes.js';
|
||||
export {default as 'prefer-json-parse-buffer'} from './prefer-json-parse-buffer.js';
|
||||
export {default as 'prefer-keyboard-event-key'} from './prefer-keyboard-event-key.js';
|
||||
export {default as 'prefer-logical-operator-over-ternary'} from './prefer-logical-operator-over-ternary.js';
|
||||
export {default as 'prefer-math-min-max'} from './prefer-math-min-max.js';
|
||||
export {default as 'prefer-math-trunc'} from './prefer-math-trunc.js';
|
||||
export {default as 'prefer-modern-dom-apis'} from './prefer-modern-dom-apis.js';
|
||||
export {default as 'prefer-modern-math-apis'} from './prefer-modern-math-apis.js';
|
||||
export {default as 'prefer-module'} from './prefer-module.js';
|
||||
export {default as 'prefer-native-coercion-functions'} from './prefer-native-coercion-functions.js';
|
||||
export {default as 'prefer-negative-index'} from './prefer-negative-index.js';
|
||||
export {default as 'prefer-node-protocol'} from './prefer-node-protocol.js';
|
||||
export {default as 'prefer-number-properties'} from './prefer-number-properties.js';
|
||||
export {default as 'prefer-object-from-entries'} from './prefer-object-from-entries.js';
|
||||
export {default as 'prefer-optional-catch-binding'} from './prefer-optional-catch-binding.js';
|
||||
export {default as 'prefer-prototype-methods'} from './prefer-prototype-methods.js';
|
||||
export {default as 'prefer-query-selector'} from './prefer-query-selector.js';
|
||||
export {default as 'prefer-reflect-apply'} from './prefer-reflect-apply.js';
|
||||
export {default as 'prefer-regexp-test'} from './prefer-regexp-test.js';
|
||||
export {default as 'prefer-response-static-json'} from './prefer-response-static-json.js';
|
||||
export {default as 'prefer-set-has'} from './prefer-set-has.js';
|
||||
export {default as 'prefer-set-size'} from './prefer-set-size.js';
|
||||
export {default as 'prefer-simple-condition-first'} from './prefer-simple-condition-first.js';
|
||||
export {default as 'prefer-single-call'} from './prefer-single-call.js';
|
||||
export {default as 'prefer-spread'} from './prefer-spread.js';
|
||||
export {default as 'prefer-string-raw'} from './prefer-string-raw.js';
|
||||
export {default as 'prefer-string-replace-all'} from './prefer-string-replace-all.js';
|
||||
export {default as 'prefer-string-slice'} from './prefer-string-slice.js';
|
||||
export {default as 'prefer-string-starts-ends-with'} from './prefer-string-starts-ends-with.js';
|
||||
export {default as 'prefer-string-trim-start-end'} from './prefer-string-trim-start-end.js';
|
||||
export {default as 'prefer-structured-clone'} from './prefer-structured-clone.js';
|
||||
export {default as 'prefer-switch'} from './prefer-switch.js';
|
||||
export {default as 'prefer-ternary'} from './prefer-ternary.js';
|
||||
export {default as 'prefer-top-level-await'} from './prefer-top-level-await.js';
|
||||
export {default as 'prefer-type-error'} from './prefer-type-error.js';
|
||||
export {default as 'prevent-abbreviations'} from './prevent-abbreviations.js';
|
||||
export {default as 'relative-url-style'} from './relative-url-style.js';
|
||||
export {default as 'require-array-join-separator'} from './require-array-join-separator.js';
|
||||
export {default as 'require-module-attributes'} from './require-module-attributes.js';
|
||||
export {default as 'require-module-specifiers'} from './require-module-specifiers.js';
|
||||
export {default as 'require-number-to-fixed-digits-argument'} from './require-number-to-fixed-digits-argument.js';
|
||||
export {default as 'require-post-message-target-origin'} from './require-post-message-target-origin.js';
|
||||
export {default as 'string-content'} from './string-content.js';
|
||||
export {default as 'switch-case-braces'} from './switch-case-braces.js';
|
||||
export {default as 'switch-case-break-position'} from './switch-case-break-position.js';
|
||||
export {default as 'template-indent'} from './template-indent.js';
|
||||
export {default as 'text-encoding-identifier-case'} from './text-encoding-identifier-case.js';
|
||||
export {default as 'throw-new-error'} from './throw-new-error.js';
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
import globals from 'globals';
|
||||
import {functionTypes} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE = 'externally-scoped-variable';
|
||||
const messages = {
|
||||
[MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE]: 'Variable {{name}} not defined in scope of isolated function. Function is isolated because: {{reason}}.',
|
||||
};
|
||||
|
||||
/** @type {{functions: string[], selectors: string[], comments: string[], overrideGlobals?: import('eslint').Linter.Globals}} */
|
||||
const defaultOptions = {
|
||||
functions: ['makeSynchronous'],
|
||||
selectors: [],
|
||||
comments: ['@isolated'],
|
||||
overrideGlobals: {},
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
/** @type {typeof defaultOptions} */
|
||||
const options = {...context.options[0]};
|
||||
|
||||
options.comments = options.comments.map(comment => comment.toLowerCase());
|
||||
|
||||
const allowedGlobals = {
|
||||
...(globals[`es${context.languageOptions.ecmaVersion}`] ?? globals.builtins),
|
||||
...context.languageOptions.globals,
|
||||
...options.overrideGlobals,
|
||||
};
|
||||
const checked = new WeakSet();
|
||||
|
||||
/** @param {import('estree').Node} node */
|
||||
const checkForExternallyScopedVariables = (node, reason) => {
|
||||
if (checked.has(node) || !functionTypes.includes(node.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checked.add(node);
|
||||
|
||||
const nodeScope = sourceCode.getScope(node);
|
||||
|
||||
// `through`: "The array of references which could not be resolved in this scope" https://eslint.org/docs/latest/extend/scope-manager-interface#scope-interface
|
||||
for (const reference of nodeScope.through) {
|
||||
const {identifier} = reference;
|
||||
|
||||
if (identifier.parent.type === 'TSTypeReference' || identifier.parent.type === 'TSTypeQuery') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (identifier.name in allowedGlobals && allowedGlobals[identifier.name] !== 'off') {
|
||||
if (reference.isReadOnly()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const globalsValue = allowedGlobals[identifier.name];
|
||||
const isGlobalWritable = globalsValue === true || globalsValue === 'writable' || globalsValue === 'writeable';
|
||||
if (isGlobalWritable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
reason += ' (global variable is not writable)';
|
||||
}
|
||||
|
||||
// Could consider checking for typeof operator here, like in no-undef?
|
||||
|
||||
context.report({
|
||||
node: identifier,
|
||||
messageId: MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE,
|
||||
data: {name: identifier.name, reason},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isComment = token => token?.type === 'Block' || token?.type === 'Line';
|
||||
|
||||
/**
|
||||
Find a comment on this node or its parent, in cases where the node passed is part of a variable or export declaration.
|
||||
@param {import('estree').Node} node
|
||||
*/
|
||||
const findComment = node => {
|
||||
let previousToken = sourceCode.getTokenBefore(node, {includeComments: true});
|
||||
let commentableNode = node;
|
||||
while (
|
||||
!isComment(previousToken)
|
||||
&& (commentableNode.parent.type === 'VariableDeclarator'
|
||||
|| commentableNode.parent.type === 'VariableDeclaration'
|
||||
|| commentableNode.parent.type === 'ExportNamedDeclaration'
|
||||
|| commentableNode.parent.type === 'ExportDefaultDeclaration')
|
||||
) {
|
||||
commentableNode = commentableNode.parent;
|
||||
previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true});
|
||||
}
|
||||
|
||||
if (isComment(previousToken)) {
|
||||
return previousToken.value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
Find the string "reason" that a function (node) should be considered isolated. For passing in to `context.report(...)` when out-of-scope variables are found. Returns undefined if the function should not be considered isolated.
|
||||
@param {import('estree').Node & {parent?: import('estree').Node}} node
|
||||
*/
|
||||
const reasonForBeingIsolatedFunction = node => {
|
||||
if (options.comments.length > 0) {
|
||||
let previousComment = findComment(node);
|
||||
|
||||
if (previousComment) {
|
||||
previousComment = previousComment
|
||||
.replace(/(?:\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated`
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const match = options.comments.find(comment => previousComment === comment || previousComment.startsWith(`${comment} - `) || previousComment.startsWith(`${comment} -- `));
|
||||
if (match) {
|
||||
return `follows comment ${JSON.stringify(match)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options.functions.length > 0
|
||||
&& node.parent.type === 'CallExpression'
|
||||
&& node.parent.arguments.includes(node)
|
||||
&& node.parent.callee.type === 'Identifier'
|
||||
&& options.functions.includes(node.parent.callee.name)
|
||||
) {
|
||||
return `callee of function named ${JSON.stringify(node.parent.callee.name)}`;
|
||||
}
|
||||
};
|
||||
|
||||
context.onExit(
|
||||
functionTypes,
|
||||
node => {
|
||||
const reason = reasonForBeingIsolatedFunction(node);
|
||||
if (!reason) {
|
||||
return;
|
||||
}
|
||||
|
||||
return checkForExternallyScopedVariables(node, reason);
|
||||
},
|
||||
);
|
||||
|
||||
for (const selector of options.selectors) {
|
||||
context.onExit(
|
||||
selector,
|
||||
node => {
|
||||
const reason = `matches selector ${JSON.stringify(selector)}`;
|
||||
return checkForExternallyScopedVariables(node, reason);
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('json-schema').JSONSchema7[]} */
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
overrideGlobals: {
|
||||
additionalProperties: {
|
||||
anyOf: [{type: 'boolean'}, {type: 'string', enum: ['readonly', 'writable', 'writeable', 'off']}],
|
||||
},
|
||||
description: 'Override which global variables are allowed inside isolated scopes.',
|
||||
},
|
||||
functions: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: 'Function names that mark a scope as isolated.',
|
||||
},
|
||||
selectors: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: 'AST selectors that mark a scope as isolated.',
|
||||
},
|
||||
comments: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: 'Comment patterns that mark a scope as isolated.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export default {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Prevent usage of variables from outside the scope of isolated functions.',
|
||||
recommended: true,
|
||||
},
|
||||
schema,
|
||||
defaultOptions: [defaultOptions],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import {GlobalReferenceTracker} from './utils/global-reference-tracker.js';
|
||||
import * as builtins from './utils/builtins.js';
|
||||
import {
|
||||
switchCallExpressionToNewExpression,
|
||||
switchNewExpressionToCallExpression,
|
||||
fixSpaceAroundKeyword,
|
||||
} from './fix/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR_DATE = 'error-date';
|
||||
const MESSAGE_ID_SUGGESTION_DATE = 'suggestion-date';
|
||||
|
||||
const messages = {
|
||||
enforce: 'Use `new {{name}}()` instead of `{{name}}()`.',
|
||||
disallow: 'Use `{{name}}()` instead of `new {{name}}()`.',
|
||||
[MESSAGE_ID_ERROR_DATE]: 'Use `String(new Date())` instead of `Date()`.',
|
||||
[MESSAGE_ID_SUGGESTION_DATE]: 'Switch to `String(new Date())`.',
|
||||
};
|
||||
|
||||
function enforceNewExpression({node, path: [name]}, context) {
|
||||
if (name === 'Object') {
|
||||
const {parent} = node;
|
||||
if (
|
||||
parent.type === 'BinaryExpression'
|
||||
&& (parent.operator === '===' || parent.operator === '!==')
|
||||
&& (parent.left === node || parent.right === node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// `Date()` returns a string representation of the current date and time, exactly as `new Date().toString()` does.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#return_value
|
||||
if (name === 'Date') {
|
||||
function * fix(fixer) {
|
||||
yield fixer.replaceText(node, 'String(new Date())');
|
||||
yield fixSpaceAroundKeyword(fixer, node, context);
|
||||
}
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: MESSAGE_ID_ERROR_DATE,
|
||||
};
|
||||
|
||||
if (context.sourceCode.getCommentsInside(node).length === 0 && node.arguments.length === 0) {
|
||||
problem.fix = fix;
|
||||
} else {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_DATE,
|
||||
fix,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
messageId: 'enforce',
|
||||
data: {name},
|
||||
fix: fixer => switchCallExpressionToNewExpression(node, context, fixer),
|
||||
};
|
||||
}
|
||||
|
||||
function enforceCallExpression({node, path: [name]}, context) {
|
||||
const problem = {
|
||||
node,
|
||||
messageId: 'disallow',
|
||||
data: {name},
|
||||
};
|
||||
|
||||
if (name !== 'String' && name !== 'Boolean' && name !== 'Number') {
|
||||
problem.fix = fixer => switchNewExpressionToCallExpression(node, context, fixer);
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
const newExpressionTracker = new GlobalReferenceTracker({
|
||||
objects: builtins.disallowNew,
|
||||
type: GlobalReferenceTracker.CONSTRUCT,
|
||||
handle: enforceCallExpression,
|
||||
});
|
||||
const callExpressionTracker = new GlobalReferenceTracker({
|
||||
objects: builtins.enforceNew,
|
||||
type: GlobalReferenceTracker.CALL,
|
||||
handle: enforceNewExpression,
|
||||
});
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
newExpressionTracker.listen({context});
|
||||
callExpressionTracker.listen({context});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
getEslintDisableDirectives,
|
||||
} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-abusive-eslint-disable';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Specify the rules you want to disable.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('Program', function * () {
|
||||
for (const directive of getEslintDisableDirectives(context)) {
|
||||
if (directive.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const {start, end} = context.sourceCode.getLoc(directive.node);
|
||||
|
||||
yield {
|
||||
// Can't set it at the given location as the warning
|
||||
// will be ignored due to the disable comment
|
||||
loc: {
|
||||
start: {
|
||||
...start,
|
||||
column: -1,
|
||||
},
|
||||
end,
|
||||
},
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce specifying rules to disable in `eslint-disable` comments.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
const MESSAGE_ID_ERROR = 'no-accessor-recursion/error';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Disallow recursive access to `this` within {{kind}}ters.',
|
||||
};
|
||||
|
||||
/**
|
||||
Get the closest non-arrow function scope.
|
||||
|
||||
@param {import('eslint').SourceCode} sourceCode
|
||||
@param {import('estree').Node} node
|
||||
@return {import('eslint').Scope.Scope | undefined}
|
||||
*/
|
||||
const getClosestFunctionScope = (sourceCode, node) => {
|
||||
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
|
||||
if (scope.type === 'class') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope.type === 'function' && scope.block.type !== 'ArrowFunctionExpression') {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {import('estree').Identifier | import('estree').PrivateIdentifier} node */
|
||||
const isIdentifier = node => node.type === 'Identifier' || node.type === 'PrivateIdentifier';
|
||||
|
||||
/** @param {import('estree').ThisExpression} node */
|
||||
const isDotNotationAccess = node =>
|
||||
node.parent.type === 'MemberExpression'
|
||||
&& node.parent.object === node
|
||||
&& !node.parent.computed
|
||||
&& isIdentifier(node.parent.property);
|
||||
|
||||
/**
|
||||
Check if a property is a valid getter or setter.
|
||||
|
||||
@param {import('estree').Property | import('estree').MethodDefinition} property
|
||||
*/
|
||||
const isValidProperty = property =>
|
||||
['Property', 'MethodDefinition'].includes(property?.type)
|
||||
&& !property.computed
|
||||
&& ['set', 'get'].includes(property.kind)
|
||||
&& isIdentifier(property.key);
|
||||
|
||||
/**
|
||||
Check if two property keys are the same.
|
||||
|
||||
@param {import('estree').Property['key']} keyLeft
|
||||
@param {import('estree').Property['key']} keyRight
|
||||
*/
|
||||
const isSameKey = (keyLeft, keyRight) => ['type', 'name'].every(key => keyLeft[key] === keyRight[key]);
|
||||
|
||||
/**
|
||||
Check if `this` is accessed recursively within a getter or setter.
|
||||
|
||||
@param {import('estree').ThisExpression} node
|
||||
@param {import('estree').Property | import('estree').MethodDefinition} property
|
||||
*/
|
||||
const isMemberAccess = (node, property) =>
|
||||
isDotNotationAccess(node)
|
||||
&& isSameKey(node.parent.property, property.key);
|
||||
|
||||
/**
|
||||
Check if `this` is accessed recursively within a destructuring assignment.
|
||||
|
||||
@param {import('estree').ThisExpression} node
|
||||
@param {import('estree').Property | import('estree').MethodDefinition} property
|
||||
*/
|
||||
const isRecursiveDestructuringAccess = (node, property) =>
|
||||
node.parent.type === 'VariableDeclarator'
|
||||
&& node.parent.init === node
|
||||
&& node.parent.id.type === 'ObjectPattern'
|
||||
&& node.parent.id.properties.some(declaratorProperty =>
|
||||
declaratorProperty.type === 'Property'
|
||||
&& !declaratorProperty.computed
|
||||
&& isSameKey(declaratorProperty.key, property.key));
|
||||
|
||||
const isPropertyRead = (thisExpression, property) =>
|
||||
isMemberAccess(thisExpression, property)
|
||||
|| isRecursiveDestructuringAccess(thisExpression, property);
|
||||
|
||||
const isPropertyWrite = (thisExpression, property) => {
|
||||
if (!isMemberAccess(thisExpression, property)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const memberExpression = thisExpression.parent;
|
||||
const {parent} = memberExpression;
|
||||
|
||||
// This part is similar to `isLeftHandSide`, try to DRY in future
|
||||
return (
|
||||
// `this.foo = …`
|
||||
// `[this.foo = …] = …`
|
||||
// `({property: this.foo = …] = …)`
|
||||
(
|
||||
(parent.type === 'AssignmentExpression' || parent.type === 'AssignmentPattern')
|
||||
&& parent.left === memberExpression
|
||||
)
|
||||
// `++ this.foo`
|
||||
|| (parent.type === 'UpdateExpression' && parent.argument === memberExpression)
|
||||
// `[this.foo] = …`
|
||||
|| (parent.type === 'ArrayPattern' && parent.elements.includes(memberExpression))
|
||||
// `({property: this.foo} = …)`
|
||||
|| (
|
||||
parent.type === 'Property'
|
||||
&& parent.value === memberExpression
|
||||
&& parent.parent.type === 'ObjectPattern'
|
||||
&& parent.parent.properties.includes(memberExpression.parent)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
|
||||
context.on('ThisExpression', /** @param {import('estree').ThisExpression} thisExpression */ thisExpression => {
|
||||
const scope = getClosestFunctionScope(sourceCode, thisExpression);
|
||||
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {import('estree').Property | import('estree').MethodDefinition} */
|
||||
const property = scope.block.parent;
|
||||
|
||||
if (!isValidProperty(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.kind === 'get' && isPropertyRead(thisExpression, property)) {
|
||||
return {node: thisExpression.parent, messageId: MESSAGE_ID_ERROR, data: {kind: property.kind}};
|
||||
}
|
||||
|
||||
if (property.kind === 'set' && isPropertyWrite(thisExpression, property)) {
|
||||
return {node: thisExpression.parent, messageId: MESSAGE_ID_ERROR, data: {kind: property.kind}};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow recursive access to `this` within getters and setters.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
defaultOptions: [],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
import path from 'node:path';
|
||||
import {getFunctionHeadLocation, getFunctionNameWithKind, isOpeningParenToken} from '@eslint-community/eslint-utils';
|
||||
import helperValidatorIdentifier from '@babel/helper-validator-identifier';
|
||||
import {camelCase} from 'change-case';
|
||||
import {
|
||||
getClassHeadLocation,
|
||||
getParenthesizedRange,
|
||||
getScopes,
|
||||
getAvailableVariableName,
|
||||
upperFirst,
|
||||
} from './utils/index.js';
|
||||
import {isMemberExpression} from './ast/index.js';
|
||||
|
||||
const {isIdentifierName} = helperValidatorIdentifier;
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-anonymous-default-export/error';
|
||||
const MESSAGE_ID_SUGGESTION = 'no-anonymous-default-export/suggestion';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'The {{description}} should be named.',
|
||||
[MESSAGE_ID_SUGGESTION]: 'Name it as `{{name}}`.',
|
||||
};
|
||||
|
||||
const isClassKeywordToken = token => token.type === 'Keyword' && token.value === 'class';
|
||||
const isAnonymousClassOrFunction = node =>
|
||||
(
|
||||
(
|
||||
node.type === 'FunctionDeclaration'
|
||||
|| node.type === 'FunctionExpression'
|
||||
|| node.type === 'ClassDeclaration'
|
||||
|| node.type === 'ClassExpression'
|
||||
)
|
||||
&& !node.id
|
||||
)
|
||||
|| node.type === 'ArrowFunctionExpression';
|
||||
|
||||
function getSuggestionName(node, filename, sourceCode) {
|
||||
if (filename === '<input>' || filename === '<text>') {
|
||||
return;
|
||||
}
|
||||
|
||||
let [name] = path.basename(filename).split('.');
|
||||
name = camelCase(name);
|
||||
|
||||
if (!isIdentifierName(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
name = node.type === 'ClassDeclaration' || node.type === 'ClassExpression' ? upperFirst(name) : name;
|
||||
name = getAvailableVariableName(name, getScopes(sourceCode.getScope(node)));
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function addName(fixer, node, name, context) {
|
||||
const {sourceCode} = context;
|
||||
switch (node.type) {
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassExpression': {
|
||||
const lastDecorator = node.decorators?.at(-1);
|
||||
const classToken = lastDecorator
|
||||
? sourceCode.getTokenAfter(lastDecorator, isClassKeywordToken)
|
||||
: sourceCode.getFirstToken(node, isClassKeywordToken);
|
||||
return fixer.insertTextAfter(classToken, ` ${name}`);
|
||||
}
|
||||
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression': {
|
||||
const openingParenthesisToken = sourceCode.getFirstToken(
|
||||
node,
|
||||
isOpeningParenToken,
|
||||
);
|
||||
const characterBefore = sourceCode.text.charAt(sourceCode.getRange(openingParenthesisToken)[0] - 1);
|
||||
return fixer.insertTextBefore(
|
||||
openingParenthesisToken,
|
||||
`${characterBefore === ' ' ? '' : ' '}${name} `,
|
||||
);
|
||||
}
|
||||
|
||||
case 'ArrowFunctionExpression': {
|
||||
const [exportDeclarationStart, exportDeclarationEnd]
|
||||
= sourceCode.getRange(node.parent.type === 'ExportDefaultDeclaration'
|
||||
? node.parent
|
||||
: node.parent.parent);
|
||||
const [arrowFunctionStart, arrowFunctionEnd] = getParenthesizedRange(node, context);
|
||||
|
||||
let textBefore = sourceCode.text.slice(exportDeclarationStart, arrowFunctionStart);
|
||||
let textAfter = sourceCode.text.slice(arrowFunctionEnd, exportDeclarationEnd);
|
||||
|
||||
textBefore = `\n${textBefore}`;
|
||||
if (!/\s$/.test(textBefore)) {
|
||||
textBefore = `${textBefore} `;
|
||||
}
|
||||
|
||||
if (!textAfter.endsWith(';')) {
|
||||
textAfter = `${textAfter};`;
|
||||
}
|
||||
|
||||
return [
|
||||
fixer.replaceTextRange(
|
||||
[exportDeclarationStart, arrowFunctionStart],
|
||||
`const ${name} = `,
|
||||
),
|
||||
fixer.replaceTextRange(
|
||||
[arrowFunctionEnd, exportDeclarationEnd],
|
||||
';',
|
||||
),
|
||||
fixer.insertTextAfterRange(
|
||||
[exportDeclarationEnd, exportDeclarationEnd],
|
||||
`${textBefore}${name}${textAfter}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
||||
function getProblem(node, context) {
|
||||
const {sourceCode, physicalFilename} = context;
|
||||
|
||||
const suggestionName = getSuggestionName(node, physicalFilename, sourceCode);
|
||||
|
||||
let loc;
|
||||
let description;
|
||||
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
||||
loc = getClassHeadLocation(node, context);
|
||||
description = 'class';
|
||||
} else {
|
||||
loc = getFunctionHeadLocation(node, sourceCode);
|
||||
// [TODO: @fisker]: Ask `@eslint-community/eslint-utils` to expose `getFunctionKind`
|
||||
const nameWithKind = getFunctionNameWithKind(node);
|
||||
description = nameWithKind.replace(/ '.*?'$/, '');
|
||||
}
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
loc,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
if (!suggestionName) {
|
||||
return problem;
|
||||
}
|
||||
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION,
|
||||
data: {
|
||||
name: suggestionName,
|
||||
},
|
||||
fix: fixer => addName(fixer, node, suggestionName, context),
|
||||
},
|
||||
];
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('ExportDefaultDeclaration', node => {
|
||||
if (!isAnonymousClassOrFunction(node.declaration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem(node.declaration, context);
|
||||
});
|
||||
|
||||
context.on('AssignmentExpression', node => {
|
||||
if (
|
||||
!isAnonymousClassOrFunction(node.right)
|
||||
|| !(
|
||||
node.parent.type === 'ExpressionStatement'
|
||||
&& node.parent.expression === node
|
||||
)
|
||||
|| !(
|
||||
isMemberExpression(node.left, {
|
||||
object: 'module',
|
||||
property: 'exports',
|
||||
computed: false,
|
||||
optional: false,
|
||||
})
|
||||
|| (
|
||||
node.left.type === 'Identifier'
|
||||
&& node.left.name === 'exports'
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem(node.right, context);
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow anonymous functions and classes as the default export.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
import {findVariable} from '@eslint-community/eslint-utils';
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
import {
|
||||
isNodeMatches,
|
||||
isNodeValueNotFunction,
|
||||
isParenthesized,
|
||||
getParenthesizedRange,
|
||||
getParenthesizedText,
|
||||
shouldAddParenthesesToCallExpressionCallee,
|
||||
} from './utils/index.js';
|
||||
|
||||
const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
|
||||
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
|
||||
const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
|
||||
const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
|
||||
const messages = {
|
||||
[ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
|
||||
[ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
|
||||
[REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
|
||||
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.',
|
||||
};
|
||||
|
||||
const isAwaitExpressionArgument = node => node.parent.type === 'AwaitExpression' && node.parent.argument === node;
|
||||
|
||||
const iteratorMethods = new Map([
|
||||
{
|
||||
method: 'every',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'filter',
|
||||
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'Vue'),
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'find',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'findLast',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'findIndex',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'findLastIndex',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'flatMap',
|
||||
},
|
||||
{
|
||||
method: 'forEach',
|
||||
returnsUndefined: true,
|
||||
},
|
||||
{
|
||||
method: 'map',
|
||||
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'types'),
|
||||
ignore: [
|
||||
'String',
|
||||
'Number',
|
||||
'BigInt',
|
||||
'Boolean',
|
||||
'Symbol',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'reduce',
|
||||
parameters: [
|
||||
'accumulator',
|
||||
'element',
|
||||
'index',
|
||||
'array',
|
||||
],
|
||||
minParameters: 2,
|
||||
},
|
||||
{
|
||||
method: 'reduceRight',
|
||||
parameters: [
|
||||
'accumulator',
|
||||
'element',
|
||||
'index',
|
||||
'array',
|
||||
],
|
||||
minParameters: 2,
|
||||
},
|
||||
{
|
||||
method: 'some',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
].map(({
|
||||
method,
|
||||
parameters = ['element', 'index', 'array'],
|
||||
ignore = [],
|
||||
minParameters = 1,
|
||||
returnsUndefined = false,
|
||||
shouldIgnoreCallExpression,
|
||||
}) => [method, {
|
||||
minParameters,
|
||||
parameters,
|
||||
returnsUndefined,
|
||||
shouldIgnoreCallExpression(callExpression) {
|
||||
if (
|
||||
method !== 'reduce'
|
||||
&& method !== 'reduceRight'
|
||||
&& isAwaitExpressionArgument(callExpression)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNodeMatches(callExpression.callee.object, ignoredCallee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
callExpression.callee.object.type === 'CallExpression'
|
||||
&& isNodeMatches(callExpression.callee.object.callee, ignoredCallee)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return shouldIgnoreCallExpression?.(callExpression) ?? false;
|
||||
},
|
||||
shouldIgnoreCallback(callback) {
|
||||
if (callback.type === 'Identifier' && ignore.includes(callback.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}]));
|
||||
|
||||
const ignoredCallee = [
|
||||
// http://bluebirdjs.com/docs/api/promise.map.html
|
||||
'Promise',
|
||||
'React.Children',
|
||||
'Children',
|
||||
'lodash',
|
||||
'underscore',
|
||||
'_',
|
||||
'Async',
|
||||
'async',
|
||||
'this',
|
||||
'$',
|
||||
'jQuery',
|
||||
];
|
||||
|
||||
function getProblem(context, node, method, options) {
|
||||
const {type} = node;
|
||||
|
||||
const name = type === 'Identifier' ? node.name : '';
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
|
||||
data: {
|
||||
name,
|
||||
method,
|
||||
},
|
||||
};
|
||||
|
||||
if (node.type === 'YieldExpression' || node.type === 'AwaitExpression') {
|
||||
return problem;
|
||||
}
|
||||
|
||||
problem.suggest = [];
|
||||
|
||||
const {parameters, minParameters, returnsUndefined} = options;
|
||||
for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
|
||||
const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
|
||||
|
||||
const suggest = {
|
||||
messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
|
||||
data: {
|
||||
name,
|
||||
parameters: suggestionParameters,
|
||||
},
|
||||
fix(fixer) {
|
||||
let text = getParenthesizedText(node, context);
|
||||
|
||||
if (
|
||||
!isParenthesized(node, context)
|
||||
&& shouldAddParenthesesToCallExpressionCallee(node)
|
||||
) {
|
||||
text = `(${text})`;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange(
|
||||
getParenthesizedRange(node, context),
|
||||
returnsUndefined
|
||||
? `(${suggestionParameters}) => { ${text}(${suggestionParameters}); }`
|
||||
: `(${suggestionParameters}) => ${text}(${suggestionParameters})`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
problem.suggest.push(suggest);
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
function * getTernaryConsequentAndALternate(node) {
|
||||
if (node.type === 'ConditionalExpression') {
|
||||
yield * getTernaryConsequentAndALternate(node.consequent);
|
||||
yield * getTernaryConsequentAndALternate(node.alternate);
|
||||
return;
|
||||
}
|
||||
|
||||
yield node;
|
||||
}
|
||||
|
||||
// These methods have dedicated type-predicate overloads in TypeScript's lib files.
|
||||
// Wrapping a type guard can lose narrowing, so direct references should be allowed here.
|
||||
const methodsWithTypePredicateOverloads = new Set([
|
||||
'every',
|
||||
'filter',
|
||||
'find',
|
||||
'findLast',
|
||||
]);
|
||||
|
||||
function hasTypePredicateReturnType(node) {
|
||||
return node.returnType?.typeAnnotation?.type === 'TSTypePredicate';
|
||||
}
|
||||
|
||||
function hasTypePredicateFunctionType(node) {
|
||||
return node.typeAnnotation?.typeAnnotation?.returnType?.typeAnnotation?.type === 'TSTypePredicate';
|
||||
}
|
||||
|
||||
function isTypePredicateCallback(callback, context) {
|
||||
if (callback.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep this local and syntax-based. Imported/member expressions need type-aware linting.
|
||||
const variable = findVariable(context.sourceCode.getScope(callback), callback);
|
||||
const definition = variable?.defs[0];
|
||||
|
||||
if (!definition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.type === 'FunctionName') {
|
||||
return hasTypePredicateReturnType(definition.node);
|
||||
}
|
||||
|
||||
// Imported callbacks may be type guards, but we can't inspect their predicate return
|
||||
// type without type-aware linting. Be conservative on methods with predicate overloads.
|
||||
if (definition.type === 'ImportBinding') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (definition.type === 'Parameter') {
|
||||
return hasTypePredicateFunctionType(definition.name);
|
||||
}
|
||||
|
||||
if (definition.type === 'Variable') {
|
||||
if (hasTypePredicateFunctionType(definition.node.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {init} = definition.node;
|
||||
return init
|
||||
&& (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
|
||||
&& hasTypePredicateReturnType(init);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', function * (callExpression) {
|
||||
if (
|
||||
!isMethodCall(callExpression, {
|
||||
minimumArguments: 1,
|
||||
maximumArguments: 2,
|
||||
optionalCall: false,
|
||||
computed: false,
|
||||
})
|
||||
|| callExpression.callee.property.type !== 'Identifier'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methodNode = callExpression.callee.property;
|
||||
const methodName = methodNode.name;
|
||||
if (!iteratorMethods.has(methodName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = iteratorMethods.get(methodName);
|
||||
if (options.shouldIgnoreCallExpression(callExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const callback of getTernaryConsequentAndALternate(callExpression.arguments[0])) {
|
||||
if (
|
||||
callback.type === 'FunctionExpression'
|
||||
|| callback.type === 'ArrowFunctionExpression'
|
||||
// Ignore all `CallExpression`s, including `function.bind()`
|
||||
|| callback.type === 'CallExpression'
|
||||
|| options.shouldIgnoreCallback(callback)
|
||||
|| isNodeValueNotFunction(callback)
|
||||
|| (methodsWithTypePredicateOverloads.has(methodName) && isTypePredicateCallback(callback, context))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield getProblem(context, callback, methodName, options);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Prevent passing a function reference directly to iterator methods.',
|
||||
recommended: true,
|
||||
},
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
import {
|
||||
isCommaToken,
|
||||
isSemicolonToken,
|
||||
isClosingParenToken,
|
||||
findVariable,
|
||||
hasSideEffect,
|
||||
} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
extendFixRange,
|
||||
fixSpaceAroundKeyword,
|
||||
removeParentheses,
|
||||
} from './fix/index.js';
|
||||
import {
|
||||
isArrowFunctionBody,
|
||||
isMethodCall,
|
||||
isReferenceIdentifier,
|
||||
functionTypes,
|
||||
} from './ast/index.js';
|
||||
import {
|
||||
needsSemicolon,
|
||||
shouldAddParenthesesToExpressionStatementExpression,
|
||||
shouldAddParenthesesToMemberExpressionObject,
|
||||
isParenthesized,
|
||||
getParentheses,
|
||||
getParenthesizedRange,
|
||||
isFunctionSelfUsedInside,
|
||||
isNodeMatches,
|
||||
assertToken,
|
||||
} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-array-for-each/error';
|
||||
const MESSAGE_ID_SUGGESTION = 'no-array-for-each/suggestion';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Use `for…of` instead of `.forEach(…)`.',
|
||||
[MESSAGE_ID_SUGGESTION]: 'Switch to `for…of`.',
|
||||
};
|
||||
|
||||
const continueAbleNodeTypes = new Set([
|
||||
'WhileStatement',
|
||||
'DoWhileStatement',
|
||||
'ForStatement',
|
||||
'ForOfStatement',
|
||||
'ForInStatement',
|
||||
]);
|
||||
|
||||
const stripChainExpression = node =>
|
||||
(node.parent.type === 'ChainExpression' && node.parent.expression === node)
|
||||
? node.parent
|
||||
: node;
|
||||
|
||||
function isReturnStatementInContinueAbleNodes(returnStatement, callbackFunction) {
|
||||
for (let node = returnStatement; node && node !== callbackFunction; node = node.parent) {
|
||||
if (continueAbleNodeTypes.has(node.type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldSwitchReturnStatementToBlockStatement(returnStatement) {
|
||||
const {parent} = returnStatement;
|
||||
|
||||
switch (parent.type) {
|
||||
case 'IfStatement': {
|
||||
return parent.consequent === returnStatement || parent.alternate === returnStatement;
|
||||
}
|
||||
|
||||
// These parent's body need switch to `BlockStatement` too, but since they are "continueAble", won't fix
|
||||
// case 'ForStatement':
|
||||
// case 'ForInStatement':
|
||||
// case 'ForOfStatement':
|
||||
// case 'WhileStatement':
|
||||
// case 'DoWhileStatement':
|
||||
case 'WithStatement': {
|
||||
return parent.body === returnStatement;
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFixFunction(callExpression, functionInfo, context) {
|
||||
const {sourceCode} = context;
|
||||
const [callback] = callExpression.arguments;
|
||||
const parameters = callback.params;
|
||||
const iterableObject = callExpression.callee.object;
|
||||
const {returnStatements} = functionInfo.get(callback);
|
||||
const isOptionalObject = callExpression.callee.optional;
|
||||
const ancestor = stripChainExpression(callExpression).parent;
|
||||
const objectText = sourceCode.getText(iterableObject);
|
||||
|
||||
const getForOfLoopHeadText = () => {
|
||||
const [elementText, indexText] = parameters.map(parameter => sourceCode.getText(parameter));
|
||||
const shouldUseEntries = parameters.length === 2;
|
||||
|
||||
let text = 'for (';
|
||||
text += isFunctionParameterVariableReassigned(callback, sourceCode) ? 'let' : 'const';
|
||||
text += ' ';
|
||||
text += shouldUseEntries ? `[${indexText}, ${elementText}]` : elementText;
|
||||
text += ' of ';
|
||||
|
||||
const shouldAddParenthesesToObject
|
||||
= isParenthesized(iterableObject, context)
|
||||
|| (
|
||||
// `1?.forEach()` -> `(1).entries()`
|
||||
isOptionalObject
|
||||
&& shouldUseEntries
|
||||
&& shouldAddParenthesesToMemberExpressionObject(iterableObject, context)
|
||||
);
|
||||
|
||||
text += shouldAddParenthesesToObject ? `(${objectText})` : objectText;
|
||||
|
||||
if (shouldUseEntries) {
|
||||
text += '.entries()';
|
||||
}
|
||||
|
||||
text += ') ';
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
const getForOfLoopHeadRange = () => {
|
||||
const [start] = sourceCode.getRange(callExpression);
|
||||
const [end] = getParenthesizedRange(callback.body, context);
|
||||
return [start, end];
|
||||
};
|
||||
|
||||
function * replaceReturnStatement(returnStatement, fixer) {
|
||||
const returnToken = sourceCode.getFirstToken(returnStatement);
|
||||
assertToken(returnToken, {
|
||||
expected: 'return',
|
||||
ruleId: 'no-array-for-each',
|
||||
});
|
||||
|
||||
if (!returnStatement.argument) {
|
||||
yield fixer.replaceText(returnToken, 'continue');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove `return`
|
||||
yield fixer.remove(returnToken);
|
||||
|
||||
const previousToken = sourceCode.getTokenBefore(returnToken);
|
||||
const nextToken = sourceCode.getTokenAfter(returnToken);
|
||||
let textBefore = '';
|
||||
let textAfter = '';
|
||||
const shouldAddParentheses
|
||||
= !isParenthesized(returnStatement.argument, context)
|
||||
&& shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument);
|
||||
if (shouldAddParentheses) {
|
||||
textBefore = `(${textBefore}`;
|
||||
textAfter = `${textAfter})`;
|
||||
}
|
||||
|
||||
const insertBraces = shouldSwitchReturnStatementToBlockStatement(returnStatement);
|
||||
if (insertBraces) {
|
||||
textBefore = `{ ${textBefore}`;
|
||||
} else if (needsSemicolon(previousToken, context, shouldAddParentheses ? '(' : nextToken.value)) {
|
||||
textBefore = `;${textBefore}`;
|
||||
}
|
||||
|
||||
if (textBefore) {
|
||||
yield fixer.insertTextBefore(nextToken, textBefore);
|
||||
}
|
||||
|
||||
if (textAfter) {
|
||||
yield fixer.insertTextAfter(returnStatement.argument, textAfter);
|
||||
}
|
||||
|
||||
const returnStatementHasSemicolon = isSemicolonToken(sourceCode.getLastToken(returnStatement));
|
||||
if (!returnStatementHasSemicolon) {
|
||||
yield fixer.insertTextAfter(returnStatement, ';');
|
||||
}
|
||||
|
||||
yield fixer.insertTextAfter(returnStatement, ' continue;');
|
||||
|
||||
if (insertBraces) {
|
||||
yield fixer.insertTextAfter(returnStatement, ' }');
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRemoveExpressionStatementLastToken = token => {
|
||||
if (!isSemicolonToken(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (callback.body.type !== 'BlockStatement') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
function * removeCallbackParentheses(fixer) {
|
||||
// Opening parenthesis tokens already included in `getForOfLoopHeadRange`
|
||||
const closingParenthesisTokens = getParentheses(callback, context)
|
||||
.filter(token => isClosingParenToken(token));
|
||||
|
||||
for (const closingParenthesisToken of closingParenthesisTokens) {
|
||||
yield fixer.remove(closingParenthesisToken);
|
||||
}
|
||||
}
|
||||
|
||||
return function * (fixer) {
|
||||
// `(( foo.forEach(bar => bar) ))`
|
||||
yield removeParentheses(callExpression, fixer, context);
|
||||
|
||||
// Replace these with `for (const … of …) `
|
||||
// foo.forEach(bar => bar)
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^
|
||||
// foo.forEach(bar => (bar))
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^
|
||||
// foo.forEach(bar => {})
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^
|
||||
// foo.forEach(function(bar) {})
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
yield fixer.replaceTextRange(getForOfLoopHeadRange(), getForOfLoopHeadText());
|
||||
|
||||
// Parenthesized callback function
|
||||
// foo.forEach( ((bar => {})) )
|
||||
// ^^
|
||||
yield removeCallbackParentheses(fixer);
|
||||
|
||||
const [
|
||||
penultimateToken,
|
||||
lastToken,
|
||||
] = sourceCode.getLastTokens(callExpression, 2);
|
||||
|
||||
// The possible trailing comma token of `Array#forEach()` CallExpression
|
||||
// foo.forEach(bar => {},)
|
||||
// ^
|
||||
if (isCommaToken(penultimateToken)) {
|
||||
yield fixer.remove(penultimateToken);
|
||||
}
|
||||
|
||||
// The closing parenthesis token of `Array#forEach()` CallExpression
|
||||
// foo.forEach(bar => {})
|
||||
// ^
|
||||
yield fixer.remove(lastToken);
|
||||
|
||||
for (const returnStatement of returnStatements) {
|
||||
yield replaceReturnStatement(returnStatement, fixer);
|
||||
}
|
||||
|
||||
if (ancestor.type === 'ExpressionStatement') {
|
||||
const expressionStatementLastToken = sourceCode.getLastToken(ancestor);
|
||||
// Remove semicolon if it's not needed anymore
|
||||
// foo.forEach(bar => {});
|
||||
// ^
|
||||
if (shouldRemoveExpressionStatementLastToken(expressionStatementLastToken)) {
|
||||
yield fixer.remove(expressionStatementLastToken, fixer);
|
||||
}
|
||||
} else if (ancestor.type === 'ArrowFunctionExpression') {
|
||||
yield fixer.insertTextBefore(callExpression, '{ ');
|
||||
yield fixer.insertTextAfter(callExpression, ' }');
|
||||
}
|
||||
|
||||
yield fixSpaceAroundKeyword(fixer, callExpression.parent, context);
|
||||
|
||||
if (isOptionalObject) {
|
||||
yield fixer.insertTextBefore(callExpression, `if (${objectText}) `);
|
||||
}
|
||||
|
||||
// Prevent possible variable conflicts
|
||||
yield extendFixRange(fixer, sourceCode.getRange(callExpression.parent));
|
||||
};
|
||||
}
|
||||
|
||||
const isChildScope = (child, parent) => {
|
||||
for (let scope = child; scope; scope = scope.upper) {
|
||||
if (scope === parent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
function isFunctionParametersSafeToFix(callbackFunction, {sourceCode, scope, callExpression, allIdentifiers}) {
|
||||
const variables = sourceCode.getDeclaredVariables(callbackFunction);
|
||||
|
||||
for (const variable of variables) {
|
||||
if (variable.defs.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [definition] = variable.defs;
|
||||
if (definition.type !== 'Parameter') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variableName = definition.name.name;
|
||||
const [callExpressionStart, callExpressionEnd] = sourceCode.getRange(callExpression);
|
||||
for (const identifier of allIdentifiers) {
|
||||
const {name} = identifier;
|
||||
const [start, end] = sourceCode.getRange(identifier);
|
||||
if (
|
||||
name !== variableName
|
||||
|| start < callExpressionStart
|
||||
|| end > callExpressionEnd
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variable = findVariable(scope, identifier);
|
||||
if (!variable || variable.scope === scope || isChildScope(scope, variable.scope)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isFunctionParameterVariableReassigned(callbackFunction, sourceCode) {
|
||||
return sourceCode.getDeclaredVariables(callbackFunction)
|
||||
.filter(variable => variable.defs[0].type === 'Parameter')
|
||||
.some(variable =>
|
||||
variable.references.some(reference => !reference.init && reference.isWrite()));
|
||||
}
|
||||
|
||||
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, sourceCode}) {
|
||||
// Check `CallExpression`
|
||||
if (callExpression.optional || callExpression.arguments.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check ancestors, we only fix `ExpressionStatement`
|
||||
const callOrChainExpression = stripChainExpression(callExpression);
|
||||
if (
|
||||
callOrChainExpression.parent.type !== 'ExpressionStatement'
|
||||
&& !isArrowFunctionBody(callOrChainExpression)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check `CallExpression.arguments[0]`;
|
||||
const [callback] = callExpression.arguments;
|
||||
if (
|
||||
// Leave non-function type to `no-array-callback-reference` rule
|
||||
(callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')
|
||||
|| callback.async
|
||||
|| callback.generator
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check `callback.params`
|
||||
const parameters = callback.params;
|
||||
if (
|
||||
!(parameters.length === 1 || parameters.length === 2)
|
||||
// `array.forEach((element = defaultValue) => {})`
|
||||
|| (parameters.length === 1 && parameters[0].type === 'AssignmentPattern')
|
||||
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1814
|
||||
|| (parameters.length === 2 && parameters[1].type !== 'Identifier')
|
||||
|| parameters.some(({type, typeAnnotation}) => type === 'RestElement' || typeAnnotation)
|
||||
|| !isFunctionParametersSafeToFix(callback, {
|
||||
scope,
|
||||
callExpression,
|
||||
allIdentifiers,
|
||||
sourceCode,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check `ReturnStatement`s in `callback`
|
||||
const {returnStatements, scope: callbackScope} = functionInfo.get(callback);
|
||||
if (returnStatements.some(returnStatement => isReturnStatementInContinueAbleNodes(returnStatement, callback))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFunctionSelfUsedInside(callback, callbackScope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const ignoredObjects = [
|
||||
'React.Children',
|
||||
'Children',
|
||||
'R',
|
||||
// https://www.npmjs.com/package/p-iteration
|
||||
'pIteration',
|
||||
// https://www.npmjs.com/package/effect
|
||||
'Effect',
|
||||
];
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const functionStack = [];
|
||||
const callExpressions = [];
|
||||
const allIdentifiers = [];
|
||||
const functionInfo = new Map();
|
||||
const {sourceCode} = context;
|
||||
|
||||
context.on(functionTypes, node => {
|
||||
functionStack.push(node);
|
||||
functionInfo.set(node, {
|
||||
returnStatements: [],
|
||||
scope: sourceCode.getScope(node),
|
||||
});
|
||||
});
|
||||
|
||||
context.onExit(functionTypes, () => {
|
||||
functionStack.pop();
|
||||
});
|
||||
|
||||
context.on('Identifier', node => {
|
||||
if (isReferenceIdentifier(node)) {
|
||||
allIdentifiers.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
context.on('ReturnStatement', node => {
|
||||
const currentFunction = functionStack.at(-1);
|
||||
if (!currentFunction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {returnStatements} = functionInfo.get(currentFunction);
|
||||
returnStatements.push(node);
|
||||
});
|
||||
|
||||
context.on('CallExpression', node => {
|
||||
if (
|
||||
!isMethodCall(node, {
|
||||
method: 'forEach',
|
||||
})
|
||||
|| isNodeMatches(node.callee.object, ignoredObjects)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
callExpressions.push({
|
||||
node,
|
||||
scope: sourceCode.getScope(node),
|
||||
});
|
||||
});
|
||||
|
||||
context.onExit('Program', function * () {
|
||||
for (const {node, scope} of callExpressions) {
|
||||
const iterable = node.callee;
|
||||
|
||||
const problem = {
|
||||
node: iterable.property,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
};
|
||||
|
||||
if (!isFixable(node, {
|
||||
scope,
|
||||
allIdentifiers,
|
||||
functionInfo,
|
||||
sourceCode,
|
||||
})) {
|
||||
yield problem;
|
||||
continue;
|
||||
}
|
||||
|
||||
const shouldUseSuggestion = iterable.optional && hasSideEffect(iterable, sourceCode);
|
||||
const fix = getFixFunction(node, functionInfo, context);
|
||||
|
||||
if (shouldUseSuggestion) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION,
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
yield problem;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer `for…of` over the `forEach` method.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+226
@@ -0,0 +1,226 @@
|
||||
import {hasSideEffect} from '@eslint-community/eslint-utils';
|
||||
import {removeArgument} from './fix/index.js';
|
||||
import {
|
||||
getParentheses,
|
||||
getParenthesizedText,
|
||||
shouldAddParenthesesToMemberExpressionObject,
|
||||
isNodeMatches,
|
||||
isNodeValueNotFunction,
|
||||
} from './utils/index.js';
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
|
||||
const ERROR_PROTOTYPE_METHOD = 'error-prototype-method';
|
||||
const ERROR_STATIC_METHOD = 'error-static-method';
|
||||
const SUGGESTION_BIND = 'suggestion-bind';
|
||||
const SUGGESTION_REMOVE = 'suggestion-remove';
|
||||
const messages = {
|
||||
[ERROR_PROTOTYPE_METHOD]: 'Do not use the `this` argument in `Array#{{method}}()`.',
|
||||
[ERROR_STATIC_METHOD]: 'Do not use the `this` argument in `Array.{{method}}()`.',
|
||||
[SUGGESTION_REMOVE]: 'Remove this argument.',
|
||||
[SUGGESTION_BIND]: 'Use a bound function.',
|
||||
};
|
||||
|
||||
const ignored = [
|
||||
'lodash.every',
|
||||
'_.every',
|
||||
'underscore.every',
|
||||
|
||||
'lodash.filter',
|
||||
'_.filter',
|
||||
'underscore.filter',
|
||||
'Vue.filter',
|
||||
'R.filter',
|
||||
|
||||
'lodash.find',
|
||||
'_.find',
|
||||
'underscore.find',
|
||||
'R.find',
|
||||
|
||||
'lodash.findLast',
|
||||
'_.findLast',
|
||||
'underscore.findLast',
|
||||
'R.findLast',
|
||||
|
||||
'lodash.findIndex',
|
||||
'_.findIndex',
|
||||
'underscore.findIndex',
|
||||
'R.findIndex',
|
||||
|
||||
'lodash.findLastIndex',
|
||||
'_.findLastIndex',
|
||||
'underscore.findLastIndex',
|
||||
'R.findLastIndex',
|
||||
|
||||
'lodash.flatMap',
|
||||
'_.flatMap',
|
||||
|
||||
'lodash.forEach',
|
||||
'_.forEach',
|
||||
'React.Children.forEach',
|
||||
'Children.forEach',
|
||||
'R.forEach',
|
||||
|
||||
'lodash.map',
|
||||
'_.map',
|
||||
'underscore.map',
|
||||
'React.Children.map',
|
||||
'Children.map',
|
||||
'jQuery.map',
|
||||
'$.map',
|
||||
'R.map',
|
||||
|
||||
'lodash.some',
|
||||
'_.some',
|
||||
'underscore.some',
|
||||
];
|
||||
|
||||
function removeThisArgument(thisArgumentNode, context) {
|
||||
return fixer => removeArgument(fixer, thisArgumentNode, context);
|
||||
}
|
||||
|
||||
function useBoundFunction(callbackNode, thisArgumentNode, context) {
|
||||
return function * (fixer) {
|
||||
yield removeThisArgument(thisArgumentNode, context)(fixer);
|
||||
|
||||
const callbackParentheses = getParentheses(callbackNode, context);
|
||||
const isParenthesized = callbackParentheses.length > 0;
|
||||
const callbackLastToken = isParenthesized
|
||||
? callbackParentheses.at(-1)
|
||||
: callbackNode;
|
||||
if (
|
||||
!isParenthesized
|
||||
&& shouldAddParenthesesToMemberExpressionObject(callbackNode, context)
|
||||
) {
|
||||
yield fixer.insertTextBefore(callbackLastToken, '(');
|
||||
yield fixer.insertTextAfter(callbackLastToken, ')');
|
||||
}
|
||||
|
||||
const thisArgumentText = getParenthesizedText(thisArgumentNode, context);
|
||||
// `thisArgument` was an argument, no need to add extra parentheses
|
||||
yield fixer.insertTextAfter(callbackLastToken, `.bind(${thisArgumentText})`);
|
||||
};
|
||||
}
|
||||
|
||||
function getProblem({
|
||||
context,
|
||||
callExpression,
|
||||
callbackNode,
|
||||
thisArgumentNode,
|
||||
messageId,
|
||||
}) {
|
||||
const problem = {
|
||||
node: thisArgumentNode,
|
||||
messageId,
|
||||
data: {
|
||||
method: callExpression.callee.property.name,
|
||||
},
|
||||
};
|
||||
|
||||
const isArrowCallback = callbackNode.type === 'ArrowFunctionExpression';
|
||||
if (isArrowCallback) {
|
||||
const thisArgumentHasSideEffect = hasSideEffect(thisArgumentNode, context.sourceCode);
|
||||
if (thisArgumentHasSideEffect) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: SUGGESTION_REMOVE,
|
||||
fix: removeThisArgument(thisArgumentNode, context),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = removeThisArgument(thisArgumentNode, context);
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: SUGGESTION_REMOVE,
|
||||
fix: removeThisArgument(thisArgumentNode, context),
|
||||
},
|
||||
{
|
||||
messageId: SUGGESTION_BIND,
|
||||
fix: useBoundFunction(callbackNode, thisArgumentNode, context),
|
||||
},
|
||||
];
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
// Prototype methods
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (
|
||||
!isMethodCall(callExpression, {
|
||||
methods: [
|
||||
'every',
|
||||
'filter',
|
||||
'find',
|
||||
'findLast',
|
||||
'findIndex',
|
||||
'findLastIndex',
|
||||
'flatMap',
|
||||
'forEach',
|
||||
'map',
|
||||
'some',
|
||||
],
|
||||
argumentsLength: 2,
|
||||
optionalCall: false,
|
||||
})
|
||||
|| isNodeMatches(callExpression.callee, ignored)
|
||||
|| isNodeValueNotFunction(callExpression.arguments[0])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem({
|
||||
context,
|
||||
callExpression,
|
||||
callbackNode: callExpression.arguments[0],
|
||||
thisArgumentNode: callExpression.arguments[1],
|
||||
messageId: ERROR_PROTOTYPE_METHOD,
|
||||
});
|
||||
});
|
||||
|
||||
// `Array.from()` and `Array.fromAsync()`
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (
|
||||
!isMethodCall(callExpression, {
|
||||
object: 'Array',
|
||||
methods: ['from', 'fromAsync'],
|
||||
argumentsLength: 3,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
|| isNodeValueNotFunction(callExpression.arguments[1])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem({
|
||||
context,
|
||||
callExpression,
|
||||
callbackNode: callExpression.arguments[1],
|
||||
thisArgumentNode: callExpression.arguments[2],
|
||||
messageId: ERROR_STATIC_METHOD,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow using the `this` argument in array methods.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
import {isNodeValueNotFunction, isArrayPrototypeProperty} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_REDUCE = 'reduce';
|
||||
const MESSAGE_ID_REDUCE_RIGHT = 'reduceRight';
|
||||
const messages = {
|
||||
[MESSAGE_ID_REDUCE]: '`Array#reduce()` is not allowed. Prefer other types of loop for readability.',
|
||||
[MESSAGE_ID_REDUCE_RIGHT]: '`Array#reduceRight()` is not allowed. Prefer other types of loop for readability. You may want to call `Array#toReversed()` before looping it.',
|
||||
};
|
||||
|
||||
const cases = [
|
||||
// `array.{reduce,reduceRight}()`
|
||||
{
|
||||
test: callExpression =>
|
||||
isMethodCall(callExpression, {
|
||||
methods: ['reduce', 'reduceRight'],
|
||||
minimumArguments: 1,
|
||||
maximumArguments: 2,
|
||||
optionalCall: false,
|
||||
})
|
||||
&& !isNodeValueNotFunction(callExpression.arguments[0]),
|
||||
getMethodNode: callExpression => callExpression.callee.property,
|
||||
isSimpleOperation(callExpression) {
|
||||
const [callback] = callExpression.arguments;
|
||||
|
||||
return (
|
||||
callback
|
||||
&& (
|
||||
// `array.reduce((accumulator, element) => accumulator + element)`
|
||||
(callback.type === 'ArrowFunctionExpression' && callback.body.type === 'BinaryExpression')
|
||||
// `array.reduce((accumulator, element) => {return accumulator + element;})`
|
||||
// `array.reduce(function (accumulator, element){return accumulator + element;})`
|
||||
|| (
|
||||
(callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
|
||||
&& callback.body.type === 'BlockStatement'
|
||||
&& callback.body.body.length === 1
|
||||
&& callback.body.body[0].type === 'ReturnStatement'
|
||||
&& callback.body.body[0].argument.type === 'BinaryExpression'
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
// `[].{reduce,reduceRight}.call()` and `Array.{reduce,reduceRight}.call()`
|
||||
{
|
||||
test: callExpression =>
|
||||
isMethodCall(callExpression, {
|
||||
method: 'call',
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& isArrayPrototypeProperty(callExpression.callee.object, {
|
||||
properties: ['reduce', 'reduceRight'],
|
||||
})
|
||||
&& (
|
||||
!callExpression.arguments[1]
|
||||
|| !isNodeValueNotFunction(callExpression.arguments[1])
|
||||
),
|
||||
getMethodNode: callExpression => callExpression.callee.object.property,
|
||||
},
|
||||
// `[].{reduce,reduceRight}.apply()` and `Array.{reduce,reduceRight}.apply()`
|
||||
{
|
||||
test: callExpression =>
|
||||
isMethodCall(callExpression, {
|
||||
method: 'apply',
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& isArrayPrototypeProperty(callExpression.callee.object, {
|
||||
properties: ['reduce', 'reduceRight'],
|
||||
}),
|
||||
getMethodNode: callExpression => callExpression.callee.object.property,
|
||||
},
|
||||
];
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
allowSimpleOperations: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to allow simple reduce operations whose callback body is a single binary expression.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {allowSimpleOperations} = context.options[0];
|
||||
|
||||
context.on('CallExpression', function * (callExpression) {
|
||||
for (const {test, getMethodNode, isSimpleOperation} of cases) {
|
||||
if (!test(callExpression)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowSimpleOperations && isSimpleOperation?.(callExpression)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const methodNode = getMethodNode(callExpression);
|
||||
yield {
|
||||
node: methodNode,
|
||||
messageId: methodNode.name,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow `Array#reduce()` and `Array#reduceRight()`.',
|
||||
recommended: true,
|
||||
},
|
||||
schema,
|
||||
defaultOptions: [{allowSimpleOperations: true}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import noArrayMutateRule from './shared/no-array-mutate-rule.js';
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = noArrayMutateRule('reverse');
|
||||
|
||||
export default config;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import noArrayMutateRule from './shared/no-array-mutate-rule.js';
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = noArrayMutateRule('sort');
|
||||
|
||||
export default config;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import {removeParentheses, removeMemberExpressionProperty} from './fix/index.js';
|
||||
import {isLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-await-expression-member';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not access a member directly from an await expression.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('MemberExpression', memberExpression => {
|
||||
if (memberExpression.object.type !== 'AwaitExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {property} = memberExpression;
|
||||
const problem = {
|
||||
node: property,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
|
||||
// `const foo = (await bar)[0]`
|
||||
if (
|
||||
memberExpression.computed
|
||||
&& !memberExpression.optional
|
||||
&& (isLiteral(property, 0) || isLiteral(property, 1))
|
||||
&& memberExpression.parent.type === 'VariableDeclarator'
|
||||
&& memberExpression.parent.init === memberExpression
|
||||
&& memberExpression.parent.id.type === 'Identifier'
|
||||
&& !memberExpression.parent.id.typeAnnotation
|
||||
) {
|
||||
problem.fix = function * (fixer) {
|
||||
const variable = memberExpression.parent.id;
|
||||
yield fixer.insertTextBefore(variable, property.value === 0 ? '[' : '[, ');
|
||||
yield fixer.insertTextAfter(variable, ']');
|
||||
|
||||
yield removeMemberExpressionProperty(fixer, memberExpression, context);
|
||||
yield removeParentheses(memberExpression.object, fixer, context);
|
||||
};
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
// `const foo = (await bar).foo`
|
||||
if (
|
||||
!memberExpression.computed
|
||||
&& !memberExpression.optional
|
||||
&& property.type === 'Identifier'
|
||||
&& memberExpression.parent.type === 'VariableDeclarator'
|
||||
&& memberExpression.parent.init === memberExpression
|
||||
&& memberExpression.parent.id.type === 'Identifier'
|
||||
&& memberExpression.parent.id.name === property.name
|
||||
&& !memberExpression.parent.id.typeAnnotation
|
||||
) {
|
||||
problem.fix = function * (fixer) {
|
||||
const variable = memberExpression.parent.id;
|
||||
yield fixer.insertTextBefore(variable, '{');
|
||||
yield fixer.insertTextAfter(variable, '}');
|
||||
|
||||
yield removeMemberExpressionProperty(fixer, memberExpression, context);
|
||||
yield removeParentheses(memberExpression.object, fixer, context);
|
||||
};
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow member access from await expression.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
import {removeSpacesAfter} from './fix/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-await-in-promise-methods/error';
|
||||
const MESSAGE_ID_SUGGESTION = 'no-await-in-promise-methods/suggestion';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Promise in `Promise.{{method}}()` should not be awaited.',
|
||||
[MESSAGE_ID_SUGGESTION]: 'Remove `await`.',
|
||||
};
|
||||
const METHODS = ['all', 'allSettled', 'any', 'race'];
|
||||
|
||||
const isPromiseMethodCallWithArrayExpression = node =>
|
||||
isMethodCall(node, {
|
||||
object: 'Promise',
|
||||
methods: METHODS,
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
argumentsLength: 1,
|
||||
})
|
||||
&& node.arguments[0].type === 'ArrayExpression';
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', function * (callExpression) {
|
||||
if (!isPromiseMethodCallWithArrayExpression(callExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of callExpression.arguments[0].elements) {
|
||||
if (element?.type !== 'AwaitExpression') {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield {
|
||||
node: element,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {
|
||||
method: callExpression.callee.property.name,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION,
|
||||
* fix(fixer) {
|
||||
const awaitToken = context.sourceCode.getFirstToken(element);
|
||||
yield fixer.remove(awaitToken);
|
||||
yield removeSpacesAfter(awaitToken, context, fixer);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow using `await` in `Promise` method parameters.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import toLocation from './utils/to-location.js';
|
||||
import {isStringLiteral, isMethodCall} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-console-spaces';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not use {{position}} space between `console.{{method}}` parameters.',
|
||||
};
|
||||
|
||||
// Find exactly one leading space, allow exactly one space
|
||||
const hasLeadingSpace = value => value.length > 1 && value.charAt(0) === ' ' && value.charAt(1) !== ' ';
|
||||
|
||||
// Find exactly one trailing space, allow exactly one space
|
||||
const hasTrailingSpace = value => value.length > 1 && value.at(-1) === ' ' && value.at(-2) !== ' ';
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
const getProblem = (node, method, position) => {
|
||||
const [start, end] = sourceCode.getRange(node);
|
||||
const index = position === 'leading'
|
||||
? start + 1
|
||||
: end - 2;
|
||||
const range = [index, index + 1];
|
||||
|
||||
return {
|
||||
loc: toLocation(range, context),
|
||||
messageId: MESSAGE_ID,
|
||||
data: {method, position},
|
||||
fix: fixer => fixer.removeRange(range),
|
||||
};
|
||||
};
|
||||
|
||||
context.on('CallExpression', function * (node) {
|
||||
if (
|
||||
!isMethodCall(node, {
|
||||
object: 'console',
|
||||
methods: [
|
||||
'log',
|
||||
'debug',
|
||||
'info',
|
||||
'warn',
|
||||
'error',
|
||||
],
|
||||
minimumArguments: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const method = node.callee.property.name;
|
||||
const {arguments: messages} = node;
|
||||
const {length} = messages;
|
||||
for (const [index, node] of messages.entries()) {
|
||||
if (!isStringLiteral(node) && node.type !== 'TemplateLiteral') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const raw = sourceCode.getText(node).slice(1, -1);
|
||||
|
||||
if (index !== 0 && hasLeadingSpace(raw)) {
|
||||
yield getProblem(node, method, 'leading');
|
||||
}
|
||||
|
||||
if (index !== length - 1 && hasTrailingSpace(raw)) {
|
||||
yield getProblem(node, method, 'trailing');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Do not use leading/trailing space between `console.log` parameters.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import {GlobalReferenceTracker} from './utils/global-reference-tracker.js';
|
||||
|
||||
const MESSAGE_ID = 'no-document-cookie';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not use `document.cookie` directly.',
|
||||
};
|
||||
|
||||
const tracker = new GlobalReferenceTracker({
|
||||
object: 'document.cookie',
|
||||
filter: ({node}) => node.parent.type === 'AssignmentExpression' && node.parent.left === node,
|
||||
handle: ({node}) => ({node, messageId: MESSAGE_ID}),
|
||||
});
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create(context) {
|
||||
tracker.listen({context});
|
||||
},
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Do not use `document.cookie` directly.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import {isEmptyNode, isDirective} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-empty-file';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Empty files are not allowed.',
|
||||
};
|
||||
|
||||
const isEmpty = node => isEmptyNode(node, isDirective);
|
||||
|
||||
const isTripleSlashDirective = node =>
|
||||
node.type === 'Line' && node.value.startsWith('/');
|
||||
|
||||
const hasTripeSlashDirectives = comments =>
|
||||
comments.some(currentNode => isTripleSlashDirective(currentNode));
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const filename = context.physicalFilename;
|
||||
|
||||
if (!/\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i.test(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.on('Program', node => {
|
||||
if (node.body.some(node => !isEmpty(node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const comments = sourceCode.getAllComments();
|
||||
|
||||
if (hasTripeSlashDirectives(comments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow empty files.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+493
@@ -0,0 +1,493 @@
|
||||
import {isClosingParenToken, getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
getAvailableVariableName,
|
||||
getScopes,
|
||||
singular,
|
||||
toLocation,
|
||||
getReferences,
|
||||
} from './utils/index.js';
|
||||
import {isLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-for-loop';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Use a `for-of` loop instead of this `for` loop.',
|
||||
};
|
||||
|
||||
const defaultElementName = 'element';
|
||||
const isLiteralZero = node => isLiteral(node, 0);
|
||||
const isLiteralOne = node => isLiteral(node, 1);
|
||||
|
||||
const isIdentifierWithName = (node, name) => node?.type === 'Identifier' && node.name === name;
|
||||
|
||||
const getTypeReferenceTypeAnnotation = (typeReferenceName, scope) => {
|
||||
const typeVariable = scope && resolveIdentifierName(typeReferenceName, scope);
|
||||
const [definition] = typeVariable?.defs ?? [];
|
||||
|
||||
if (!definition || definition.type !== 'Type') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.node.type === 'TSTypeAliasDeclaration') {
|
||||
return definition.node.typeAnnotation;
|
||||
}
|
||||
|
||||
if (definition.node.type === 'TSTypeParameter') {
|
||||
return definition.node.constraint;
|
||||
}
|
||||
};
|
||||
|
||||
const isArrayTypeReference = (node, scope, visitedTypeReferenceNames) => {
|
||||
if (node.typeName.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const typeReferenceName = node.typeName.name;
|
||||
|
||||
if (typeReferenceName === 'Array' || typeReferenceName === 'ReadonlyArray') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (visitedTypeReferenceNames.has(typeReferenceName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visitedTypeReferenceNames.add(typeReferenceName);
|
||||
|
||||
const typeAnnotation = getTypeReferenceTypeAnnotation(typeReferenceName, scope);
|
||||
const isArray = isArrayType(typeAnnotation, scope, visitedTypeReferenceNames);
|
||||
|
||||
visitedTypeReferenceNames.delete(typeReferenceName);
|
||||
|
||||
return isArray;
|
||||
};
|
||||
|
||||
const isArrayType = (node, scope, visitedTypeReferenceNames = new Set()) => {
|
||||
switch (node?.type) {
|
||||
case 'TSArrayType':
|
||||
case 'TSTupleType': {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'TSTypeReference': {
|
||||
return isArrayTypeReference(node, scope, visitedTypeReferenceNames);
|
||||
}
|
||||
|
||||
case 'TSTypeOperator': {
|
||||
return node.operator === 'readonly' && isArrayType(node.typeAnnotation, scope, visitedTypeReferenceNames);
|
||||
}
|
||||
|
||||
case 'TSUnionType': {
|
||||
return node.types.every(type => isArrayType(type, scope, visitedTypeReferenceNames));
|
||||
}
|
||||
|
||||
case 'TSIntersectionType': {
|
||||
return node.types.some(type => isArrayType(type, scope, visitedTypeReferenceNames));
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getIndexIdentifierName = forStatement => {
|
||||
const {init: variableDeclaration} = forStatement;
|
||||
|
||||
if (
|
||||
!variableDeclaration
|
||||
|| variableDeclaration.type !== 'VariableDeclaration'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (variableDeclaration.declarations.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [variableDeclarator] = variableDeclaration.declarations;
|
||||
|
||||
if (!isLiteralZero(variableDeclarator.init)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (variableDeclarator.id.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
return variableDeclarator.id.name;
|
||||
};
|
||||
|
||||
const getStrictComparisonOperands = binaryExpression => {
|
||||
if (binaryExpression.operator === '<') {
|
||||
return {
|
||||
lesser: binaryExpression.left,
|
||||
greater: binaryExpression.right,
|
||||
};
|
||||
}
|
||||
|
||||
if (binaryExpression.operator === '>') {
|
||||
return {
|
||||
lesser: binaryExpression.right,
|
||||
greater: binaryExpression.left,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getArrayIdentifierFromBinaryExpression = (binaryExpression, indexIdentifierName) => {
|
||||
const operands = getStrictComparisonOperands(binaryExpression);
|
||||
|
||||
if (!operands) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {lesser, greater} = operands;
|
||||
|
||||
if (!isIdentifierWithName(lesser, indexIdentifierName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (greater.type !== 'MemberExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
greater.object.type !== 'Identifier'
|
||||
|| greater.property.type !== 'Identifier'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (greater.property.name !== 'length') {
|
||||
return;
|
||||
}
|
||||
|
||||
return greater.object;
|
||||
};
|
||||
|
||||
const getArrayIdentifier = (forStatement, indexIdentifierName) => {
|
||||
const {test} = forStatement;
|
||||
|
||||
if (!test || test.type !== 'BinaryExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
return getArrayIdentifierFromBinaryExpression(test, indexIdentifierName);
|
||||
};
|
||||
|
||||
const isLiteralOnePlusIdentifierWithName = (node, identifierName) => {
|
||||
if (node?.type === 'BinaryExpression' && node.operator === '+') {
|
||||
return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right))
|
||||
|| (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const checkUpdateExpression = (forStatement, indexIdentifierName) => {
|
||||
const {update} = forStatement;
|
||||
|
||||
if (!update) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (update.type === 'UpdateExpression') {
|
||||
return update.operator === '++' && isIdentifierWithName(update.argument, indexIdentifierName);
|
||||
}
|
||||
|
||||
if (
|
||||
update.type === 'AssignmentExpression'
|
||||
&& isIdentifierWithName(update.left, indexIdentifierName)
|
||||
) {
|
||||
if (update.operator === '+=') {
|
||||
return isLiteralOne(update.right);
|
||||
}
|
||||
|
||||
if (update.operator === '=') {
|
||||
return isLiteralOnePlusIdentifierWithName(update.right, indexIdentifierName);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => arrayReferences.every(reference => {
|
||||
const node = reference.identifier.parent;
|
||||
|
||||
if (node.type !== 'MemberExpression') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.property.name !== indexIdentifierName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
node.parent.type === 'AssignmentExpression'
|
||||
&& node.parent.left === node
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const getRemovalRange = (node, sourceCode) => {
|
||||
const declarationNode = node.parent;
|
||||
|
||||
if (declarationNode.declarations.length === 1) {
|
||||
const {line} = sourceCode.getLoc(declarationNode).start;
|
||||
const lineText = sourceCode.lines[line - 1];
|
||||
|
||||
const isOnlyNodeOnLine = lineText.trim() === sourceCode.getText(declarationNode);
|
||||
|
||||
return isOnlyNodeOnLine
|
||||
? [
|
||||
sourceCode.getIndexFromLoc({line, column: 0}),
|
||||
sourceCode.getIndexFromLoc({line: line + 1, column: 0}),
|
||||
]
|
||||
: sourceCode.getRange(declarationNode);
|
||||
}
|
||||
|
||||
const index = declarationNode.declarations.indexOf(node);
|
||||
|
||||
if (index === 0) {
|
||||
return [
|
||||
sourceCode.getRange(node)[0],
|
||||
sourceCode.getRange(declarationNode.declarations[1])[0],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
sourceCode.getRange(declarationNode.declarations[index - 1])[1],
|
||||
sourceCode.getRange(node)[1],
|
||||
];
|
||||
};
|
||||
|
||||
const resolveIdentifierName = (name, scope) => {
|
||||
while (scope) {
|
||||
const variable = scope.set.get(name);
|
||||
|
||||
if (variable) {
|
||||
return variable;
|
||||
}
|
||||
|
||||
scope = scope.upper;
|
||||
}
|
||||
};
|
||||
|
||||
const scopeContains = (ancestor, descendant) => {
|
||||
while (descendant) {
|
||||
if (descendant === ancestor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
descendant = descendant.upper;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const nodeContains = (ancestor, descendant) => {
|
||||
while (descendant) {
|
||||
if (descendant === ancestor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
descendant = descendant.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isIndexVariableUsedElsewhereInTheLoopBody = (indexVariable, bodyScope, arrayIdentifierName) => {
|
||||
const inBodyReferences = indexVariable.references.filter(reference => scopeContains(bodyScope, reference.from));
|
||||
|
||||
const referencesOtherThanArrayAccess = inBodyReferences.filter(reference => {
|
||||
const node = reference.identifier.parent;
|
||||
|
||||
if (node.type !== 'MemberExpression') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.object.name !== arrayIdentifierName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return referencesOtherThanArrayAccess.length > 0;
|
||||
};
|
||||
|
||||
const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) =>
|
||||
indexVariable.references
|
||||
.filter(reference => scopeContains(bodyScope, reference.from))
|
||||
.some(inBodyReference => inBodyReference.isWrite());
|
||||
|
||||
const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) =>
|
||||
variables.some(variable => !variable.references.every(reference => scopeContains(forScope, reference.from) || nodeContains(forStatement, reference.identifier)));
|
||||
|
||||
const getReferencesInChildScopes = (scope, name) =>
|
||||
getReferences(scope).filter(reference => reference.identifier.name === name);
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
const {scopeManager} = sourceCode;
|
||||
|
||||
context.on('ForStatement', node => {
|
||||
const indexIdentifierName = getIndexIdentifierName(node);
|
||||
|
||||
if (!indexIdentifierName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayIdentifier = getArrayIdentifier(node, indexIdentifierName);
|
||||
if (!arrayIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayIdentifierName = arrayIdentifier.name;
|
||||
|
||||
const scope = sourceCode.getScope(node);
|
||||
const staticResult = getStaticValue(arrayIdentifier, scope);
|
||||
if (staticResult && !Array.isArray(staticResult.value)) {
|
||||
// Bail out if we can tell that the array variable has a non-array value (i.e. we're looping through the characters of a string constant).
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkUpdateExpression(node, indexIdentifierName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.body || node.body.type !== 'BlockStatement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const forScope = scopeManager.acquire(node);
|
||||
const bodyScope = scopeManager.acquire(node.body);
|
||||
|
||||
if (!bodyScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexVariable = resolveIdentifierName(indexIdentifierName, bodyScope);
|
||||
|
||||
if (isIndexVariableAssignedToInTheLoopBody(indexVariable, bodyScope)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayReferences = getReferencesInChildScopes(bodyScope, arrayIdentifierName);
|
||||
|
||||
if (arrayReferences.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOnlyArrayOfIndexVariableRead(arrayReferences, indexIdentifierName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [start] = sourceCode.getRange(node);
|
||||
const closingParenthesisToken = sourceCode.getTokenBefore(node.body, isClosingParenToken);
|
||||
const [, end] = sourceCode.getRange(closingParenthesisToken);
|
||||
|
||||
const problem = {
|
||||
loc: toLocation([start, end], context),
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
|
||||
const elementReference = arrayReferences.find(reference => {
|
||||
const node = reference.identifier.parent;
|
||||
|
||||
if (node.parent.type !== 'VariableDeclarator') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
const elementNode = elementReference?.identifier.parent.parent;
|
||||
const elementIdentifierName = elementNode?.id.name;
|
||||
const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope);
|
||||
|
||||
const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
|
||||
|
||||
// When `.entries()` would be generated, only autofix if the type annotation confirms it's an array (or there's no type annotation).
|
||||
const hasNonArrayTypeAnnotation = resolveIdentifierName(arrayIdentifierName, scope)
|
||||
?.defs.some(definition => {
|
||||
const typeAnnotation = definition.name.typeAnnotation?.typeAnnotation;
|
||||
return typeAnnotation && !isArrayType(typeAnnotation, scope);
|
||||
});
|
||||
|
||||
const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope)
|
||||
&& !elementNode?.id.typeAnnotation
|
||||
&& !(hasNonArrayTypeAnnotation && shouldGenerateIndex);
|
||||
|
||||
if (shouldFix) {
|
||||
problem.fix = function * (fixer) {
|
||||
const index = indexIdentifierName;
|
||||
const element = elementIdentifierName
|
||||
|| getAvailableVariableName(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope));
|
||||
const array = arrayIdentifierName;
|
||||
|
||||
let declarationElement = element;
|
||||
let declarationType = 'const';
|
||||
let removeDeclaration = true;
|
||||
|
||||
if (elementNode) {
|
||||
if (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern') {
|
||||
removeDeclaration = arrayReferences.length === 1;
|
||||
}
|
||||
|
||||
if (removeDeclaration) {
|
||||
declarationType = element.type === 'VariableDeclarator' ? elementNode.kind : elementNode.parent.kind;
|
||||
declarationElement = sourceCode.getText(elementNode.id);
|
||||
}
|
||||
}
|
||||
|
||||
const parts = [declarationType];
|
||||
if (shouldGenerateIndex) {
|
||||
parts.push(` [${index}, ${declarationElement}] of ${array}.entries()`);
|
||||
} else {
|
||||
parts.push(` ${declarationElement} of ${array}`);
|
||||
}
|
||||
|
||||
const replacement = parts.join('');
|
||||
const [start] = sourceCode.getRange(node.init);
|
||||
const [, end] = sourceCode.getRange(node.update);
|
||||
|
||||
yield fixer.replaceTextRange([start, end], replacement);
|
||||
|
||||
for (const reference of arrayReferences) {
|
||||
if (reference !== elementReference) {
|
||||
yield fixer.replaceText(reference.identifier.parent, element);
|
||||
}
|
||||
}
|
||||
|
||||
if (elementNode) {
|
||||
yield removeDeclaration
|
||||
? fixer.removeRange(getRemovalRange(elementNode, sourceCode))
|
||||
: fixer.replaceText(elementNode.init, element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import {replaceTemplateElement} from './fix/index.js';
|
||||
import {isStringLiteral, isRegexLiteral, isTaggedTemplateLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-hex-escape';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Use Unicode escapes instead of hexadecimal escapes.',
|
||||
};
|
||||
|
||||
function checkEscape(context, node, value) {
|
||||
const fixedValue = value.replaceAll(/(?<=(?:^|[^\\])(?:\\\\)*\\)x/g, 'u00');
|
||||
|
||||
if (value !== fixedValue) {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
fix: fixer =>
|
||||
node.type === 'TemplateElement'
|
||||
? replaceTemplateElement(node, fixedValue, context, fixer)
|
||||
: fixer.replaceText(node, fixedValue),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('Literal', node => {
|
||||
if (isStringLiteral(node) || isRegexLiteral(node)) {
|
||||
return checkEscape(context, node, node.raw);
|
||||
}
|
||||
});
|
||||
|
||||
context.on('TemplateElement', node => {
|
||||
if (isTaggedTemplateLiteral(node.parent, ['String.raw'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
return checkEscape(context, node, node.value.raw);
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce the use of Unicode escapes instead of hexadecimal escapes.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+778
@@ -0,0 +1,778 @@
|
||||
import {
|
||||
hasSideEffect,
|
||||
isCommaToken,
|
||||
isSemicolonToken,
|
||||
findVariable,
|
||||
} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isMethodCall,
|
||||
isMemberExpression,
|
||||
isNewExpression,
|
||||
} from './ast/index.js';
|
||||
import {
|
||||
removeExpressionStatement,
|
||||
removeArgument,
|
||||
} from './fix/index.js';
|
||||
import {
|
||||
getNextNode,
|
||||
getCallExpressionArgumentsText,
|
||||
getParenthesizedText,
|
||||
getVariableIdentifiers,
|
||||
getNewExpressionTokens,
|
||||
isNewExpressionWithParentheses,
|
||||
} from './utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
const MESSAGE_ID_ERROR = 'error';
|
||||
const MESSAGE_ID_SUGGESTION_ARRAY = 'suggestion/array';
|
||||
const MESSAGE_ID_SUGGESTION_OBJECT = 'suggestion/object';
|
||||
const MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN = 'suggestion/object-assign';
|
||||
const MESSAGE_ID_SUGGESTION_SET = 'suggestion/set';
|
||||
const MESSAGE_ID_SUGGESTION_MAP = 'suggestion/map';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Immediate mutation on {{objectType}} is not allowed.',
|
||||
[MESSAGE_ID_SUGGESTION_ARRAY]: '{{operation}} the elements to the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_OBJECT]: 'Move this property to the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN]: '{{description}} the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_SET]: 'Move the element to the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_MAP]: 'Move the entry to the {{assignType}}.',
|
||||
};
|
||||
|
||||
const hasVariableInNodes = (variable, nodes, context) => {
|
||||
const {sourceCode} = context;
|
||||
const identifiers = getVariableIdentifiers(variable);
|
||||
return nodes.some(node => {
|
||||
const range = sourceCode.getRange(node);
|
||||
return identifiers.some(identifier => {
|
||||
const [start, end] = sourceCode.getRange(identifier);
|
||||
return start >= range[0] && end <= range[1];
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function isCallExpressionWithOptionalArrayExpression(newExpression, names) {
|
||||
if (!isNewExpression(
|
||||
newExpression,
|
||||
{names, maximumArguments: 1},
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `new Set();` and `new Set([]);`
|
||||
const [iterable] = newExpression.arguments;
|
||||
return (!iterable || iterable.type === 'ArrayExpression');
|
||||
}
|
||||
|
||||
function * removeExpressionStatementAfterAssign(expressionStatement, context, fixer) {
|
||||
const tokenBefore = context.sourceCode.getTokenBefore(expressionStatement);
|
||||
const shouldPreserveSemiColon = !isSemicolonToken(tokenBefore);
|
||||
yield removeExpressionStatement(expressionStatement, context, fixer, shouldPreserveSemiColon);
|
||||
}
|
||||
|
||||
function appendListTextToArrayExpressionOrObjectExpression(
|
||||
context,
|
||||
fixer,
|
||||
arrayOrObjectExpression,
|
||||
listText,
|
||||
) {
|
||||
const {sourceCode} = context;
|
||||
const [
|
||||
penultimateToken,
|
||||
closingBracketToken,
|
||||
] = sourceCode.getLastTokens(arrayOrObjectExpression, 2);
|
||||
const list = arrayOrObjectExpression.type === 'ArrayExpression'
|
||||
? arrayOrObjectExpression.elements
|
||||
: arrayOrObjectExpression.properties;
|
||||
const shouldInsertComma = list.length > 0 && !isCommaToken(penultimateToken);
|
||||
|
||||
return fixer.insertTextBefore(
|
||||
closingBracketToken,
|
||||
`${shouldInsertComma ? ',' : ''} ${listText}`,
|
||||
);
|
||||
}
|
||||
|
||||
function * appendElementsTextToSetConstructor({
|
||||
context,
|
||||
fixer,
|
||||
newExpression,
|
||||
elementsText,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
if (isNewExpressionWithParentheses(newExpression, context)) {
|
||||
const [setInitialValue] = newExpression.arguments;
|
||||
if (setInitialValue) {
|
||||
yield appendListTextToArrayExpressionOrObjectExpression(context, fixer, setInitialValue, elementsText);
|
||||
} else {
|
||||
const {
|
||||
openingParenthesisToken,
|
||||
} = getNewExpressionTokens(newExpression, context);
|
||||
yield fixer.insertTextAfter(openingParenthesisToken, `[${elementsText}]`);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
The new expression doesn't have parentheses
|
||||
```
|
||||
const set = (( new (( Set )) ));
|
||||
set.add(1);
|
||||
```
|
||||
*/
|
||||
yield fixer.insertTextAfter(newExpression, `([${elementsText}])`);
|
||||
}
|
||||
|
||||
yield * removeExpressionStatementAfterAssign(nextExpressionStatement, context, fixer);
|
||||
}
|
||||
|
||||
function getObjectExpressionPropertiesText(objectExpression, context) {
|
||||
const {sourceCode} = context;
|
||||
const openingBraceToken = sourceCode.getFirstToken(objectExpression);
|
||||
const [penultimateToken, closingBraceToken] = sourceCode.getLastTokens(objectExpression, 2);
|
||||
const [, start] = sourceCode.getRange(openingBraceToken);
|
||||
const [end] = sourceCode.getRange(isCommaToken(penultimateToken) ? penultimateToken : closingBraceToken);
|
||||
return sourceCode.text.slice(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
@typedef {ESTree.VariableDeclarator['init'] | ESTree.AssignmentExpression['right']} ValueNode
|
||||
@typedef {(information: ViolationCaseInformation, arguments: any)} GetFix
|
||||
@typedef {Parameters<ESLint.Rule.RuleContext['report']>[0]} Problem
|
||||
@typedef {(information: ViolationCaseInformation) => ESTree.Node} GetProblematicNode
|
||||
@typedef {{
|
||||
context: ESLint.Rule.RuleContext,
|
||||
variable: ESLint.Scope.Variable,
|
||||
variableNode: ESTree.Identifier,
|
||||
valueNode: ValueNode,
|
||||
statement: ESTree.VariableDeclaration | ESTree.ExpressionStatement,
|
||||
nextExpressionStatement: ESTree.ExpressionStatement,
|
||||
assignType: 'assignment' | 'declaration',
|
||||
getFix: GetFix,
|
||||
}} ViolationCaseInformation
|
||||
@typedef {{
|
||||
testValue: (value: ValueNode) => boolean,
|
||||
getProblematicNode: GetProblematicNode,
|
||||
getProblem: (node: ReturnType<GetProblematicNode>, information: ViolationCaseInformation) => Problem,
|
||||
getFix: GetFix,
|
||||
}} ViolationCase
|
||||
*/
|
||||
|
||||
// `Array`
|
||||
/** @type {ViolationCase} */
|
||||
const arrayMutationSettings = {
|
||||
testValue: value => value?.type === 'ArrayExpression',
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const callExpression = nextExpressionStatement.expression;
|
||||
|
||||
if (!(
|
||||
isMethodCall(callExpression, {
|
||||
object: variable.name,
|
||||
methods: ['push', 'unshift'],
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
})
|
||||
&& callExpression.arguments.length > 0
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const memberExpression = callExpression.callee;
|
||||
const method = memberExpression.property;
|
||||
const problem = {
|
||||
node: memberExpression,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: 'array'},
|
||||
};
|
||||
|
||||
const isPrepend = method.name === 'unshift';
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
isPrepend,
|
||||
});
|
||||
|
||||
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_ARRAY,
|
||||
fix,
|
||||
data: {operation: isPrepend ? 'Prepend' : 'Append', assignType},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
valueNode: arrayExpression,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
isPrepend,
|
||||
},
|
||||
) => function * (fixer) {
|
||||
const text = getCallExpressionArgumentsText(context, callExpression, /* includeTrailingComma */ false);
|
||||
|
||||
yield (
|
||||
isPrepend
|
||||
? fixer.insertTextAfter(
|
||||
context.sourceCode.getFirstToken(arrayExpression),
|
||||
`${text}, `,
|
||||
)
|
||||
: appendListTextToArrayExpressionOrObjectExpression(context, fixer, arrayExpression, text)
|
||||
);
|
||||
|
||||
yield removeExpressionStatementAfterAssign(
|
||||
nextExpressionStatement,
|
||||
context,
|
||||
fixer,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// `Object` + `AssignmentExpression`
|
||||
/** @type {ViolationCase} */
|
||||
const objectWithAssignmentExpressionSettings = {
|
||||
testValue: value => value?.type === 'ObjectExpression',
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const assignmentExpression = nextExpressionStatement.expression;
|
||||
if (!(
|
||||
assignmentExpression.type === 'AssignmentExpression'
|
||||
&& assignmentExpression.operator === '='
|
||||
&& isMemberExpression(assignmentExpression.left, {object: variable.name, optional: false})
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = assignmentExpression.right;
|
||||
const memberExpression = assignmentExpression.left;
|
||||
const {property} = memberExpression;
|
||||
|
||||
if (
|
||||
hasVariableInNodes(
|
||||
variable,
|
||||
memberExpression.computed ? [property, value] : [value],
|
||||
context,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return assignmentExpression;
|
||||
},
|
||||
getProblem(assignmentExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const {
|
||||
left: memberExpression,
|
||||
right: value,
|
||||
} = assignmentExpression;
|
||||
|
||||
const {property} = memberExpression;
|
||||
const operatorToken = sourceCode.getTokenAfter(memberExpression, token => token.type === 'Punctuator' && token.value === assignmentExpression.operator);
|
||||
|
||||
const problem = {
|
||||
node: assignmentExpression,
|
||||
loc: {
|
||||
start: sourceCode.getLoc(assignmentExpression).start,
|
||||
end: sourceCode.getLoc(operatorToken).end,
|
||||
},
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: 'object'},
|
||||
};
|
||||
const fix = getFix(information, {
|
||||
assignmentExpression,
|
||||
memberExpression,
|
||||
property,
|
||||
value,
|
||||
});
|
||||
|
||||
if (
|
||||
(memberExpression.computed && hasSideEffect(property, sourceCode))
|
||||
|| hasSideEffect(value, sourceCode)
|
||||
) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_OBJECT,
|
||||
data: {assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
valueNode: objectExpression,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
memberExpression,
|
||||
property,
|
||||
value,
|
||||
},
|
||||
) => function * (fixer) {
|
||||
let propertyText = getParenthesizedText(property, context);
|
||||
if (memberExpression.computed) {
|
||||
propertyText = `[${propertyText}]`;
|
||||
}
|
||||
|
||||
const valueText = getParenthesizedText(value, context);
|
||||
|
||||
const text = `${propertyText}: ${valueText},`;
|
||||
const [
|
||||
penultimateToken,
|
||||
closingBraceToken,
|
||||
] = context.sourceCode.getLastTokens(objectExpression, 2);
|
||||
const shouldInsertComma = objectExpression.properties.length > 0 && !isCommaToken(penultimateToken);
|
||||
|
||||
yield fixer.insertTextBefore(
|
||||
closingBraceToken,
|
||||
`${shouldInsertComma ? ',' : ''} ${text}`,
|
||||
);
|
||||
|
||||
yield removeExpressionStatementAfterAssign(
|
||||
nextExpressionStatement,
|
||||
context,
|
||||
fixer,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// `Object` + `Object.assign()`
|
||||
/** @type {ViolationCase} */
|
||||
const objectWithObjectAssignSettings = {
|
||||
testValue: value => value?.type === 'ObjectExpression',
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const callExpression = nextExpressionStatement.expression;
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
object: 'Object',
|
||||
method: 'assign',
|
||||
minimumArguments: 2,
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [object, firstValue] = callExpression.arguments;
|
||||
|
||||
if (
|
||||
!(object.type === 'Identifier' && object.name === variable.name)
|
||||
|| firstValue.type === 'SpreadElement'
|
||||
|| hasVariableInNodes(variable, [firstValue], context)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const [, firstValue] = callExpression.arguments;
|
||||
|
||||
const problem = {
|
||||
node: callExpression.callee,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: 'object'},
|
||||
};
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
firstValue,
|
||||
});
|
||||
|
||||
if (hasSideEffect(firstValue, sourceCode)) {
|
||||
const description = firstValue.type === 'ObjectExpression'
|
||||
? 'Move properties to'
|
||||
: 'Spread properties in';
|
||||
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN,
|
||||
data: {description, assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
valueNode: objectExpression,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
firstValue,
|
||||
},
|
||||
) => function * (fixer) {
|
||||
let text;
|
||||
if (firstValue.type === 'ObjectExpression') {
|
||||
if (firstValue.properties.length > 0) {
|
||||
text = getObjectExpressionPropertiesText(firstValue, context);
|
||||
}
|
||||
} else {
|
||||
text = `...${getParenthesizedText(firstValue, context)}`;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
yield appendListTextToArrayExpressionOrObjectExpression(context, fixer, objectExpression, text);
|
||||
}
|
||||
|
||||
if (callExpression.arguments.length !== 2) {
|
||||
yield removeArgument(fixer, firstValue, context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
yield removeExpressionStatementAfterAssign(
|
||||
nextExpressionStatement,
|
||||
context,
|
||||
fixer,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// `Set` and `WeakSet`
|
||||
/** @type {ViolationCase} */
|
||||
const setMutationSettings = {
|
||||
testValue: value => isCallExpressionWithOptionalArrayExpression(value, ['Set', 'WeakSet']),
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
let callExpression = nextExpressionStatement.expression;
|
||||
if (callExpression.type === 'ChainExpression') {
|
||||
callExpression = callExpression.expression;
|
||||
}
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
object: variable.name,
|
||||
method: 'add',
|
||||
argumentsLength: 1,
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
valueNode: newExpression,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const memberExpression = callExpression.callee;
|
||||
const problem = {
|
||||
node: memberExpression,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: `\`${newExpression.callee.name}\``},
|
||||
};
|
||||
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
newExpression,
|
||||
});
|
||||
|
||||
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_SET,
|
||||
data: {assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
newExpression,
|
||||
},
|
||||
) => fixer => {
|
||||
const elementsText = getCallExpressionArgumentsText(
|
||||
context,
|
||||
callExpression,
|
||||
/* IncludeTrailingComma */ false,
|
||||
);
|
||||
return appendElementsTextToSetConstructor({
|
||||
context,
|
||||
fixer,
|
||||
newExpression,
|
||||
elementsText,
|
||||
nextExpressionStatement,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// `Map` and `WeakMap`
|
||||
/** @type {ViolationCase} */
|
||||
const mapMutationSettings = {
|
||||
testValue: value => isCallExpressionWithOptionalArrayExpression(value, ['Map', 'WeakMap']),
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const callExpression = nextExpressionStatement.expression;
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
object: variable.name,
|
||||
method: 'set',
|
||||
argumentsLength: 2,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
valueNode: newExpression,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const memberExpression = callExpression.callee;
|
||||
const problem = {
|
||||
node: memberExpression,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: `\`${newExpression.callee.name}\``},
|
||||
};
|
||||
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
newExpression,
|
||||
});
|
||||
|
||||
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_MAP,
|
||||
data: {assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
newExpression,
|
||||
},
|
||||
) => fixer => {
|
||||
const argumentsText = getCallExpressionArgumentsText(
|
||||
context,
|
||||
callExpression,
|
||||
/* IncludeTrailingComma */ false,
|
||||
);
|
||||
const entryText = `[${argumentsText}]`;
|
||||
return appendElementsTextToSetConstructor({
|
||||
context,
|
||||
fixer,
|
||||
newExpression,
|
||||
elementsText: entryText,
|
||||
nextExpressionStatement,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const cases = [
|
||||
arrayMutationSettings,
|
||||
objectWithAssignmentExpressionSettings,
|
||||
objectWithObjectAssignSettings,
|
||||
setMutationSettings,
|
||||
mapMutationSettings,
|
||||
];
|
||||
|
||||
function isLastDeclarator(variableDeclarator) {
|
||||
const variableDeclaration = variableDeclarator.parent;
|
||||
return (
|
||||
variableDeclaration.type === 'VariableDeclaration'
|
||||
&& variableDeclaration.declarations.at(-1) === variableDeclarator
|
||||
);
|
||||
}
|
||||
|
||||
const getVariable = (node, context) => {
|
||||
if (node.type === 'VariableDeclarator') {
|
||||
return context.sourceCode.getDeclaredVariables(node)
|
||||
.find(variable => variable.defs.length === 1 && variable.defs[0].name === node.id);
|
||||
}
|
||||
|
||||
return findVariable(context.sourceCode.getScope(node), node.left.name);
|
||||
};
|
||||
|
||||
function getCaseProblem(
|
||||
context,
|
||||
assignNode,
|
||||
{
|
||||
testValue,
|
||||
getProblematicNode,
|
||||
getProblem,
|
||||
getFix,
|
||||
},
|
||||
) {
|
||||
const isAssignment = assignNode.type === 'AssignmentExpression';
|
||||
const [variableNode, valueNode] = (isAssignment ? ['left', 'right'] : ['id', 'init'])
|
||||
.map(property => assignNode[property]);
|
||||
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO[@fisker]: `AssignmentExpression` should not limit to `Identifier`
|
||||
if (!(variableNode.type === 'Identifier' && testValue(valueNode))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statement = assignNode.parent;
|
||||
|
||||
if (!(
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO[@fisker]: `AssignmentExpression` should support `a = b = c` too
|
||||
(
|
||||
isAssignment
|
||||
&& assignNode.operator === '='
|
||||
&& statement.type === 'ExpressionStatement'
|
||||
&& statement.expression === assignNode)
|
||||
|| (!isAssignment && isLastDeclarator(assignNode))
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextExpressionStatement = getNextNode(statement, context);
|
||||
if (nextExpressionStatement?.type !== 'ExpressionStatement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = getVariable(assignNode, context);
|
||||
/* c8 ignore next */
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const information = {
|
||||
context,
|
||||
variable,
|
||||
variableNode,
|
||||
valueNode,
|
||||
statement,
|
||||
nextExpressionStatement,
|
||||
assignType: isAssignment ? 'assignment' : 'declaration',
|
||||
getFix,
|
||||
};
|
||||
|
||||
const problematicNode = getProblematicNode(information);
|
||||
|
||||
if (!problematicNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem(problematicNode, information);
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
for (const caseSettings of cases) {
|
||||
context.on(
|
||||
[
|
||||
'VariableDeclarator',
|
||||
'AssignmentExpression',
|
||||
],
|
||||
assignNode => getCaseProblem(context, assignNode, caseSettings),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow immediate mutation after variable assignment.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
checkVueTemplate,
|
||||
getParenthesizedRange,
|
||||
getTokenStore,
|
||||
} from './utils/index.js';
|
||||
import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
|
||||
import builtinErrors from './shared/builtin-errors.js';
|
||||
import typedArray from './shared/typed-array.js';
|
||||
|
||||
const isInstanceofToken = token => token.value === 'instanceof' && token.type === 'Keyword';
|
||||
|
||||
const MESSAGE_ID = 'no-instanceof-builtins';
|
||||
const MESSAGE_ID_SWITCH_TO_TYPE_OF = 'switch-to-type-of';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Avoid using `instanceof` for type checking as it can lead to unreliable results.',
|
||||
[MESSAGE_ID_SWITCH_TO_TYPE_OF]: 'Switch to `typeof … === \'{{type}}\'`.',
|
||||
};
|
||||
|
||||
const primitiveWrappers = new Set([
|
||||
'String',
|
||||
'Number',
|
||||
'Boolean',
|
||||
'BigInt',
|
||||
'Symbol',
|
||||
]);
|
||||
|
||||
const strictStrategyConstructors = [
|
||||
// Error types
|
||||
...builtinErrors,
|
||||
|
||||
// Collection types
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakRef',
|
||||
'WeakSet',
|
||||
|
||||
// Arrays and Typed Arrays
|
||||
'ArrayBuffer',
|
||||
...typedArray,
|
||||
|
||||
// Data types
|
||||
'Object',
|
||||
|
||||
// Regular Expressions
|
||||
'RegExp',
|
||||
|
||||
// Async and functions
|
||||
'Promise',
|
||||
'Proxy',
|
||||
|
||||
// Other
|
||||
'DataView',
|
||||
'Date',
|
||||
'SharedArrayBuffer',
|
||||
'FinalizationRegistry',
|
||||
];
|
||||
|
||||
const replaceWithFunctionCall = (node, context, functionName) => function * (fixer) {
|
||||
const {left, right} = node;
|
||||
const tokenStore = getTokenStore(context, node);
|
||||
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
|
||||
|
||||
yield fixSpaceAroundKeyword(fixer, node, context);
|
||||
|
||||
const range = getParenthesizedRange(left, {sourceCode: tokenStore});
|
||||
yield fixer.insertTextBeforeRange(range, functionName + '(');
|
||||
yield fixer.insertTextAfterRange(range, ')');
|
||||
|
||||
yield replaceNodeOrTokenAndSpacesBefore(instanceofToken, '', fixer, context, tokenStore);
|
||||
yield replaceNodeOrTokenAndSpacesBefore(right, '', fixer, context, tokenStore);
|
||||
};
|
||||
|
||||
const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
|
||||
const {left, right} = node;
|
||||
const tokenStore = getTokenStore(context, node);
|
||||
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
|
||||
const {sourceCode} = context;
|
||||
|
||||
// Check if the node is in a Vue template expression
|
||||
const vueExpressionContainer = sourceCode.getAncestors(node).findLast(ancestor => ancestor.type === 'VExpressionContainer');
|
||||
|
||||
// Get safe quote
|
||||
const safeQuote = vueExpressionContainer ? (sourceCode.getText(vueExpressionContainer)[0] === '"' ? '\'' : '"') : '\'';
|
||||
|
||||
yield fixSpaceAroundKeyword(fixer, node, context);
|
||||
|
||||
const leftRange = getParenthesizedRange(left, {sourceCode: tokenStore});
|
||||
yield fixer.insertTextBeforeRange(leftRange, 'typeof ');
|
||||
|
||||
yield fixer.replaceText(instanceofToken, '===');
|
||||
|
||||
const rightRange = getParenthesizedRange(right, {sourceCode: tokenStore});
|
||||
|
||||
yield fixer.replaceTextRange(rightRange, safeQuote + sourceCode.getText(right).toLowerCase() + safeQuote);
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {
|
||||
useErrorIsError = false,
|
||||
strategy = 'loose',
|
||||
include = [],
|
||||
exclude = [],
|
||||
} = context.options[0] ?? {};
|
||||
|
||||
const forbiddenConstructors = new Set(strategy === 'strict'
|
||||
? [...strictStrategyConstructors, ...include]
|
||||
: include);
|
||||
|
||||
context.on('BinaryExpression', /** @param {import('estree').BinaryExpression} node */ node => {
|
||||
const {right, operator} = node;
|
||||
|
||||
if ((operator !== 'instanceof') || (right.type !== 'Identifier') || exclude.includes(right.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const constructorName = right.name;
|
||||
|
||||
/** @type {import('eslint').Rule.ReportDescriptor} */
|
||||
const problem = {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
|
||||
if (
|
||||
constructorName === 'Array'
|
||||
|| (constructorName === 'Error' && useErrorIsError)
|
||||
) {
|
||||
const functionName = constructorName === 'Array' ? 'Array.isArray' : 'Error.isError';
|
||||
problem.fix = replaceWithFunctionCall(node, context, functionName);
|
||||
return problem;
|
||||
}
|
||||
|
||||
if (constructorName === 'Function') {
|
||||
problem.fix = replaceWithTypeOfExpression(node, context);
|
||||
return problem;
|
||||
}
|
||||
|
||||
if (primitiveWrappers.has(constructorName)) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
|
||||
data: {type: constructorName.toLowerCase()},
|
||||
fix: replaceWithTypeOfExpression(node, context),
|
||||
},
|
||||
];
|
||||
return problem;
|
||||
}
|
||||
|
||||
if (!forbiddenConstructors.has(constructorName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
useErrorIsError: {
|
||||
type: 'boolean',
|
||||
},
|
||||
strategy: {
|
||||
enum: [
|
||||
'loose',
|
||||
'strict',
|
||||
],
|
||||
},
|
||||
include: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
exclude: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create: checkVueTemplate(create),
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow `instanceof` with built-in objects',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema,
|
||||
defaultOptions: [{
|
||||
useErrorIsError: false,
|
||||
strategy: 'loose',
|
||||
include: [],
|
||||
exclude: [],
|
||||
}],
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import {getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isCallExpression,
|
||||
isNewExpression,
|
||||
isUndefined,
|
||||
isNullLiteral,
|
||||
} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-invalid-fetch-options';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: '"body" is not allowed when method is "{{method}}".',
|
||||
};
|
||||
|
||||
const isObjectPropertyWithName = (node, name) =>
|
||||
node.type === 'Property'
|
||||
&& !node.computed
|
||||
&& node.key.type === 'Identifier'
|
||||
&& node.key.name === name;
|
||||
|
||||
function checkFetchOptions(context, node) {
|
||||
if (node.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {properties} = node;
|
||||
|
||||
const bodyProperty = properties.findLast(property => isObjectPropertyWithName(property, 'body'));
|
||||
|
||||
if (!bodyProperty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyValue = bodyProperty.value;
|
||||
if (isUndefined(bodyValue) || isNullLiteral(bodyValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methodProperty = properties.findLast(property => isObjectPropertyWithName(property, 'method'));
|
||||
// If `method` is omitted but there is a `SpreadElement`, we just ignore the case
|
||||
if (!methodProperty) {
|
||||
if (properties.some(node => node.type === 'SpreadElement')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: bodyProperty.key,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {method: 'GET'},
|
||||
};
|
||||
}
|
||||
|
||||
const methodValue = methodProperty.value;
|
||||
|
||||
const scope = context.sourceCode.getScope(methodValue);
|
||||
let method = getStaticValue(methodValue, scope)?.value;
|
||||
|
||||
if (typeof method !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
method = method.toUpperCase();
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: bodyProperty.key,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {method},
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (!isCallExpression(callExpression, {
|
||||
name: 'fetch',
|
||||
minimumArguments: 2,
|
||||
optional: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
return checkFetchOptions(context, callExpression.arguments[1]);
|
||||
});
|
||||
|
||||
context.on('NewExpression', newExpression => {
|
||||
if (!isNewExpression(newExpression, {
|
||||
name: 'Request',
|
||||
minimumArguments: 2,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
return checkFetchOptions(context, newExpression.arguments[1]);
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow invalid options in `fetch()` and `new Request()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
import {getFunctionHeadLocation} from '@eslint-community/eslint-utils';
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-invalid-remove-event-listener';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'The listener argument should be a function reference.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (!(
|
||||
isMethodCall(callExpression, {
|
||||
method: 'removeEventListener',
|
||||
minimumArguments: 2,
|
||||
optionalCall: false,
|
||||
})
|
||||
&& callExpression.arguments[0].type !== 'SpreadElement'
|
||||
&& (
|
||||
callExpression.arguments[1].type === 'FunctionExpression'
|
||||
|| callExpression.arguments[1].type === 'ArrowFunctionExpression'
|
||||
|| isMethodCall(callExpression.arguments[1], {
|
||||
method: 'bind',
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, listener] = callExpression.arguments;
|
||||
if (['ArrowFunctionExpression', 'FunctionExpression'].includes(listener.type)) {
|
||||
return {
|
||||
node: listener,
|
||||
loc: getFunctionHeadLocation(listener, context.sourceCode),
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
node: listener.callee.property,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Prevent calling `EventTarget#removeEventListener()` with the result of an expression.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import isShorthandPropertyAssignmentPatternLeft from './utils/is-shorthand-property-assignment-pattern-left.js';
|
||||
|
||||
const MESSAGE_ID = 'noKeywordPrefix';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not prefix identifiers with keyword `{{keyword}}`.',
|
||||
};
|
||||
|
||||
const prepareOptions = ({
|
||||
disallowedPrefixes,
|
||||
checkProperties = true,
|
||||
onlyCamelCase = true,
|
||||
} = {}) => ({
|
||||
disallowedPrefixes: (disallowedPrefixes || [
|
||||
'new',
|
||||
'class',
|
||||
]),
|
||||
checkProperties,
|
||||
onlyCamelCase,
|
||||
});
|
||||
|
||||
function findKeywordPrefix(name, options) {
|
||||
return options.disallowedPrefixes.find(keyword => {
|
||||
const suffix = options.onlyCamelCase ? '[A-Z]' : '.';
|
||||
const regex = new RegExp(`^${keyword}${suffix}`);
|
||||
return name.match(regex);
|
||||
});
|
||||
}
|
||||
|
||||
function checkMemberExpression(report, node, options) {
|
||||
const {name, parent} = node;
|
||||
const keyword = findKeywordPrefix(name, options);
|
||||
const effectiveParent = parent.type === 'MemberExpression' ? parent.parent : parent;
|
||||
|
||||
if (!options.checkProperties) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent.object.type === 'Identifier' && parent.object.name === name && Boolean(keyword)) {
|
||||
report(node, keyword);
|
||||
} else if (
|
||||
effectiveParent.type === 'AssignmentExpression'
|
||||
&& Boolean(keyword)
|
||||
&& (effectiveParent.right.type !== 'MemberExpression' || effectiveParent.left.type === 'MemberExpression')
|
||||
&& effectiveParent.left.property.name === name
|
||||
) {
|
||||
report(node, keyword);
|
||||
}
|
||||
}
|
||||
|
||||
function checkObjectPattern(report, node, options) {
|
||||
const {name, parent} = node;
|
||||
const keyword = findKeywordPrefix(name, options);
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
if (parent.shorthand && parent.value.left && Boolean(keyword)) {
|
||||
report(node, keyword);
|
||||
}
|
||||
|
||||
const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
|
||||
|
||||
if (Boolean(keyword) && parent.computed) {
|
||||
report(node, keyword);
|
||||
}
|
||||
|
||||
// Prevent checking right hand side of destructured object
|
||||
if (parent.key === node && parent.value !== node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const valueIsInvalid = parent.value.name && Boolean(keyword);
|
||||
|
||||
// Ignore destructuring if the option is set, unless a new identifier is created
|
||||
if (valueIsInvalid && !assignmentKeyEqualsValue) {
|
||||
report(node, keyword);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Core logic copied from:
|
||||
// https://github.com/eslint/eslint/blob/master/lib/rules/camelcase.js
|
||||
const create = context => {
|
||||
const options = prepareOptions(context.options[0]);
|
||||
|
||||
// Contains reported nodes to avoid reporting twice on destructuring with shorthand notation
|
||||
const reported = [];
|
||||
const ALLOWED_PARENT_TYPES = new Set(['CallExpression', 'NewExpression']);
|
||||
|
||||
function report(node, keyword) {
|
||||
if (!reported.includes(node)) {
|
||||
reported.push(node);
|
||||
context.report({
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {
|
||||
name: node.name,
|
||||
keyword,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.on('Identifier', node => {
|
||||
const {name, parent} = node;
|
||||
const keyword = findKeywordPrefix(name, options);
|
||||
const effectiveParent = parent.type === 'MemberExpression' ? parent.parent : parent;
|
||||
|
||||
if (parent.type === 'MemberExpression') {
|
||||
checkMemberExpression(report, node, options);
|
||||
} else if (
|
||||
parent.type === 'Property'
|
||||
|| parent.type === 'AssignmentPattern'
|
||||
) {
|
||||
if (parent.parent.type === 'ObjectPattern') {
|
||||
const finished = checkObjectPattern(report, node, options);
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!options.checkProperties
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't check right hand side of AssignmentExpression to prevent duplicate warnings
|
||||
if (
|
||||
Boolean(keyword)
|
||||
&& !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
|
||||
&& !(parent.right === node)
|
||||
&& !isShorthandPropertyAssignmentPatternLeft(node)
|
||||
) {
|
||||
report(node, keyword);
|
||||
}
|
||||
|
||||
// Check if it's an import specifier
|
||||
} else if (
|
||||
[
|
||||
'ImportSpecifier',
|
||||
'ImportNamespaceSpecifier',
|
||||
'ImportDefaultSpecifier',
|
||||
].includes(parent.type)
|
||||
) {
|
||||
// Report only if the local imported identifier is invalid
|
||||
if (Boolean(keyword) && parent.local?.name === name) {
|
||||
report(node, keyword);
|
||||
}
|
||||
|
||||
// Report anything that is invalid that isn't a CallExpression
|
||||
} else if (
|
||||
Boolean(keyword)
|
||||
&& !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
|
||||
) {
|
||||
report(node, keyword);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
disallowedPrefixes: {
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
minItems: 0,
|
||||
uniqueItems: true,
|
||||
description: 'The prefixes to disallow.',
|
||||
},
|
||||
checkProperties: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check property names.',
|
||||
},
|
||||
onlyCamelCase: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to only check camelCase names.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow identifiers starting with `new` or `class`.',
|
||||
recommended: false,
|
||||
},
|
||||
schema,
|
||||
defaultOptions: [{}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import {isNotSemicolonToken} from '@eslint-community/eslint-utils';
|
||||
import {isParenthesized, needsSemicolon} from './utils/index.js';
|
||||
import {removeSpacesAfter} from './fix/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-lonely-if';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Unexpected `if` as the only statement in a `if` block without `else`.',
|
||||
};
|
||||
|
||||
const isIfStatementWithoutAlternate = node => node.type === 'IfStatement' && !node.alternate;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
|
||||
// Lower precedence than `&&`
|
||||
const needParenthesis = node => (
|
||||
(node.type === 'LogicalExpression' && (node.operator === '||' || node.operator === '??'))
|
||||
|| node.type === 'ConditionalExpression'
|
||||
|| node.type === 'AssignmentExpression'
|
||||
|| node.type === 'YieldExpression'
|
||||
|| node.type === 'SequenceExpression'
|
||||
);
|
||||
|
||||
function getIfStatementTokens(node, sourceCode) {
|
||||
const tokens = {
|
||||
ifToken: sourceCode.getFirstToken(node),
|
||||
openingParenthesisToken: sourceCode.getFirstToken(node, 1),
|
||||
};
|
||||
|
||||
const {consequent} = node;
|
||||
tokens.closingParenthesisToken = sourceCode.getTokenBefore(consequent);
|
||||
|
||||
if (consequent.type === 'BlockStatement') {
|
||||
tokens.openingBraceToken = sourceCode.getFirstToken(consequent);
|
||||
tokens.closingBraceToken = sourceCode.getLastToken(consequent);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function fix(innerIfStatement, context) {
|
||||
const {sourceCode} = context;
|
||||
|
||||
return function * (fixer) {
|
||||
const outerIfStatement = (
|
||||
innerIfStatement.parent.type === 'BlockStatement'
|
||||
? innerIfStatement.parent
|
||||
: innerIfStatement
|
||||
).parent;
|
||||
const outer = {
|
||||
...outerIfStatement,
|
||||
...getIfStatementTokens(outerIfStatement, sourceCode),
|
||||
};
|
||||
const inner = {
|
||||
...innerIfStatement,
|
||||
...getIfStatementTokens(innerIfStatement, sourceCode),
|
||||
};
|
||||
|
||||
// Remove inner `if` token
|
||||
yield fixer.remove(inner.ifToken);
|
||||
yield removeSpacesAfter(inner.ifToken, context, fixer);
|
||||
|
||||
// Remove outer `{}`
|
||||
if (outer.openingBraceToken) {
|
||||
yield fixer.remove(outer.openingBraceToken);
|
||||
yield removeSpacesAfter(outer.openingBraceToken, context, fixer);
|
||||
yield fixer.remove(outer.closingBraceToken);
|
||||
|
||||
const tokenBefore = sourceCode.getTokenBefore(outer.closingBraceToken, {includeComments: true});
|
||||
yield removeSpacesAfter(tokenBefore, context, fixer);
|
||||
}
|
||||
|
||||
// Add new `()`
|
||||
yield fixer.insertTextBefore(outer.openingParenthesisToken, '(');
|
||||
yield fixer.insertTextAfter(
|
||||
inner.closingParenthesisToken,
|
||||
`)${inner.consequent.type === 'EmptyStatement' ? '' : ' '}`,
|
||||
);
|
||||
|
||||
// Add ` && `
|
||||
yield fixer.insertTextAfter(outer.closingParenthesisToken, ' && ');
|
||||
|
||||
// Remove `()` if `test` doesn't need it
|
||||
for (const {test, openingParenthesisToken, closingParenthesisToken} of [outer, inner]) {
|
||||
if (
|
||||
isParenthesized(test, context)
|
||||
|| !needParenthesis(test)
|
||||
) {
|
||||
yield fixer.remove(openingParenthesisToken);
|
||||
yield fixer.remove(closingParenthesisToken);
|
||||
}
|
||||
|
||||
yield removeSpacesAfter(closingParenthesisToken, context, fixer);
|
||||
}
|
||||
|
||||
// If the `if` statement has no block, and is not followed by a semicolon,
|
||||
// make sure that fixing the issue would not change semantics due to ASI.
|
||||
// Similar logic https://github.com/eslint/eslint/blob/2124e1b5dad30a905dc26bde9da472bf622d3f50/lib/rules/no-lonely-if.js#L61-L77
|
||||
if (inner.consequent.type !== 'BlockStatement') {
|
||||
const lastToken = sourceCode.getLastToken(inner.consequent);
|
||||
if (isNotSemicolonToken(lastToken)) {
|
||||
const nextToken = sourceCode.getTokenAfter(outer);
|
||||
if (nextToken && needsSemicolon(lastToken, context, nextToken.value)) {
|
||||
yield fixer.insertTextBefore(nextToken, ';');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('IfStatement', ifStatement => {
|
||||
if (!(
|
||||
isIfStatementWithoutAlternate(ifStatement)
|
||||
&& (
|
||||
// `if (a) { if (b) {} }`
|
||||
(
|
||||
ifStatement.parent.type === 'BlockStatement'
|
||||
&& ifStatement.parent.body.length === 1
|
||||
&& ifStatement.parent.body[0] === ifStatement
|
||||
&& isIfStatementWithoutAlternate(ifStatement.parent.parent)
|
||||
&& ifStatement.parent.parent.consequent === ifStatement.parent
|
||||
)
|
||||
// `if (a) if (b) {}`
|
||||
|| (
|
||||
isIfStatementWithoutAlternate(ifStatement.parent)
|
||||
&& ifStatement.parent.consequent === ifStatement
|
||||
)
|
||||
)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: ifStatement,
|
||||
messageId: MESSAGE_ID,
|
||||
fix: fix(ifStatement, context),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow `if` statements as the only statement in `if` blocks without `else`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import {isMethodCall, isNumericLiteral} from './ast/index.js';
|
||||
import {getCallExpressionTokens} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-magic-array-flat-depth';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Magic number as depth is not allowed.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (!isMethodCall(callExpression, {
|
||||
method: 'flat',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [depth] = callExpression.arguments;
|
||||
|
||||
if (!isNumericLiteral(depth) || depth.value === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const {
|
||||
openingParenthesisToken,
|
||||
closingParenthesisToken,
|
||||
} = getCallExpressionTokens(callExpression, context);
|
||||
if (sourceCode.commentsExistBetween(openingParenthesisToken, closingParenthesisToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: depth,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow a magic number as the `depth` argument in `Array#flat(…).`',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import {removeSpecifier} from './fix/index.js';
|
||||
import assertToken from './utils/assert-token.js';
|
||||
|
||||
const MESSAGE_ID = 'no-named-default';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Prefer using the default {{type}} over named {{type}}.',
|
||||
};
|
||||
|
||||
const isValueImport = node => !node.importKind || node.importKind === 'value';
|
||||
const isValueExport = node => !node.exportKind || node.exportKind === 'value';
|
||||
|
||||
const fixImportSpecifier = (importSpecifier, context) => function * (fixer) {
|
||||
const {sourceCode} = context;
|
||||
const declaration = importSpecifier.parent;
|
||||
|
||||
yield removeSpecifier(importSpecifier, fixer, context, /* keepDeclaration */ true);
|
||||
|
||||
const nameText = sourceCode.getText(importSpecifier.local);
|
||||
const hasDefaultImport = declaration.specifiers.some(({type}) => type === 'ImportDefaultSpecifier');
|
||||
|
||||
// Insert a new `ImportDeclaration`
|
||||
if (hasDefaultImport) {
|
||||
const fromToken = sourceCode.getTokenBefore(declaration.source, token => token.type === 'Identifier' && token.value === 'from');
|
||||
const [startOfFromToken] = sourceCode.getRange(fromToken);
|
||||
const [, endOfDeclaration] = sourceCode.getRange(declaration);
|
||||
const text = `import ${nameText} ${sourceCode.text.slice(startOfFromToken, endOfDeclaration)}`;
|
||||
yield fixer.insertTextBefore(declaration, `${text}\n`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const importToken = sourceCode.getFirstToken(declaration);
|
||||
assertToken(importToken, {
|
||||
expected: {type: 'Keyword', value: 'import'},
|
||||
ruleId: 'no-named-default',
|
||||
});
|
||||
|
||||
const shouldAddComma = declaration.specifiers.some(specifier => specifier !== importSpecifier && specifier.type === importSpecifier.type);
|
||||
yield fixer.insertTextAfter(importToken, ` ${nameText}${shouldAddComma ? ',' : ''}`);
|
||||
};
|
||||
|
||||
const fixExportSpecifier = (exportSpecifier, context) => function * (fixer) {
|
||||
const declaration = exportSpecifier.parent;
|
||||
yield removeSpecifier(exportSpecifier, fixer, context);
|
||||
|
||||
const text = `export default ${context.sourceCode.getText(exportSpecifier.local)};`;
|
||||
yield fixer.insertTextBefore(declaration, `${text}\n`);
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('ImportSpecifier', specifier => {
|
||||
if (!(
|
||||
isValueImport(specifier)
|
||||
&& specifier.imported.name === 'default'
|
||||
&& isValueImport(specifier.parent)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: specifier,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {type: 'import'},
|
||||
fix: fixImportSpecifier(specifier, context),
|
||||
};
|
||||
});
|
||||
|
||||
context.on('ExportSpecifier', specifier => {
|
||||
if (!(
|
||||
isValueExport(specifier)
|
||||
&& specifier.exported.name === 'default'
|
||||
&& isValueExport(specifier.parent)
|
||||
&& !specifier.parent.source
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: specifier,
|
||||
messageId: MESSAGE_ID,
|
||||
data: {type: 'export'},
|
||||
fix: fixExportSpecifier(specifier, context),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow named usage of default import and export.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Based on ESLint builtin `no-negated-condition` rule
|
||||
https://github.com/eslint/eslint/blob/5c39425fc55ecc0b97bbd07ac22654c0eb4f789c/lib/rules/no-negated-condition.js
|
||||
*/
|
||||
import {
|
||||
removeParentheses,
|
||||
fixSpaceAroundKeyword,
|
||||
addParenthesizesToReturnOrThrowExpression,
|
||||
} from './fix/index.js';
|
||||
import {
|
||||
getParenthesizedRange,
|
||||
isParenthesized,
|
||||
isOnSameLine,
|
||||
needsSemicolon,
|
||||
} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-negated-condition';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Unexpected negated condition.',
|
||||
};
|
||||
|
||||
function * convertNegatedCondition(fixer, node, context) {
|
||||
const {sourceCode} = context;
|
||||
const {test} = node;
|
||||
if (test.type === 'UnaryExpression') {
|
||||
const token = sourceCode.getFirstToken(test);
|
||||
|
||||
if (node.type === 'IfStatement') {
|
||||
yield removeParentheses(test.argument, fixer, context);
|
||||
}
|
||||
|
||||
yield fixer.remove(token);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = sourceCode.getTokenAfter(
|
||||
test.left,
|
||||
token => token.type === 'Punctuator' && token.value === test.operator,
|
||||
);
|
||||
|
||||
yield fixer.replaceText(token, '=' + token.value.slice(1));
|
||||
}
|
||||
|
||||
function * swapConsequentAndAlternate(fixer, node, context) {
|
||||
const isIfStatement = node.type === 'IfStatement';
|
||||
const [consequent, alternate] = [
|
||||
node.consequent,
|
||||
node.alternate,
|
||||
].map(node => {
|
||||
const range = getParenthesizedRange(node, context);
|
||||
let text = context.sourceCode.text.slice(...range);
|
||||
// `if (!a) b(); else c()` can't fix to `if (!a) c() else b();`
|
||||
if (isIfStatement && node.type !== 'BlockStatement') {
|
||||
text = `{${text}}`;
|
||||
}
|
||||
|
||||
return {
|
||||
range,
|
||||
text,
|
||||
};
|
||||
});
|
||||
|
||||
if (consequent.text === alternate.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
yield fixer.replaceTextRange(sourceCode.getRange(consequent), alternate.text);
|
||||
yield fixer.replaceTextRange(sourceCode.getRange(alternate), consequent.text);
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on(['IfStatement', 'ConditionalExpression'], node => {
|
||||
if (
|
||||
node.type === 'IfStatement'
|
||||
&& (
|
||||
!node.alternate
|
||||
|| node.alternate.type === 'IfStatement'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {test} = node;
|
||||
|
||||
if (!(
|
||||
(test.type === 'UnaryExpression' && test.operator === '!')
|
||||
|| (test.type === 'BinaryExpression' && (test.operator === '!=' || test.operator === '!=='))
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: test,
|
||||
messageId: MESSAGE_ID,
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
* fix(fixer) {
|
||||
yield convertNegatedCondition(fixer, node, context);
|
||||
yield swapConsequentAndAlternate(fixer, node, context);
|
||||
|
||||
if (
|
||||
node.type !== 'ConditionalExpression'
|
||||
|| test.type !== 'UnaryExpression'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield fixSpaceAroundKeyword(fixer, node, context);
|
||||
|
||||
const {sourceCode} = context;
|
||||
const {parent} = node;
|
||||
const [firstToken, secondToken] = sourceCode.getFirstTokens(test, 2);
|
||||
if (
|
||||
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
|
||||
&& parent.argument === node
|
||||
&& !isOnSameLine(firstToken, secondToken, context)
|
||||
&& !isParenthesized(node, context)
|
||||
&& !isParenthesized(test, context)
|
||||
) {
|
||||
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
if (needsSemicolon(tokenBefore, context, secondToken.value)) {
|
||||
yield fixer.insertTextBefore(node, ';');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow negated conditions.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+99
@@ -0,0 +1,99 @@
|
||||
import {fixSpaceAroundKeyword, addParenthesizesToReturnOrThrowExpression} from './fix/index.js';
|
||||
import {needsSemicolon, isParenthesized, isOnSameLine} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error';
|
||||
const MESSAGE_ID_SUGGESTION = 'no-negation-in-equality-check/suggestion';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Negated expression is not allowed in equality check.',
|
||||
[MESSAGE_ID_SUGGESTION]: 'Switch to \'{{operator}}\' check.',
|
||||
};
|
||||
|
||||
const EQUALITY_OPERATORS = new Set([
|
||||
'===',
|
||||
'!==',
|
||||
'==',
|
||||
'!=',
|
||||
]);
|
||||
|
||||
const isEqualityCheck = node => node.type === 'BinaryExpression' && EQUALITY_OPERATORS.has(node.operator);
|
||||
const isNegatedExpression = node => node.type === 'UnaryExpression' && node.prefix && node.operator === '!';
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('BinaryExpression', binaryExpression => {
|
||||
const {operator, left} = binaryExpression;
|
||||
|
||||
if (!(
|
||||
isEqualityCheck(binaryExpression)
|
||||
&& isNegatedExpression(left)
|
||||
&& !isNegatedExpression(left.argument)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const bangToken = sourceCode.getFirstToken(left);
|
||||
const negatedOperator = `${operator.startsWith('!') ? '=' : '!'}${operator.slice(1)}`;
|
||||
|
||||
return {
|
||||
node: bangToken,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
suggest: [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION,
|
||||
data: {
|
||||
operator: negatedOperator,
|
||||
},
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
* fix(fixer) {
|
||||
yield fixSpaceAroundKeyword(fixer, binaryExpression, context);
|
||||
|
||||
const tokenAfterBang = sourceCode.getTokenAfter(bangToken);
|
||||
|
||||
const {parent} = binaryExpression;
|
||||
if (
|
||||
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
|
||||
&& !isParenthesized(binaryExpression, context)
|
||||
) {
|
||||
const returnToken = sourceCode.getFirstToken(parent);
|
||||
if (!isOnSameLine(returnToken, tokenAfterBang, context)) {
|
||||
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
|
||||
}
|
||||
}
|
||||
|
||||
yield fixer.remove(bangToken);
|
||||
|
||||
const previousToken = sourceCode.getTokenBefore(bangToken);
|
||||
if (needsSemicolon(previousToken, context, tokenAfterBang.value)) {
|
||||
yield fixer.insertTextAfter(bangToken, ';');
|
||||
}
|
||||
|
||||
const operatorToken = sourceCode.getTokenAfter(
|
||||
left,
|
||||
token => token.type === 'Punctuator' && token.value === operator,
|
||||
);
|
||||
yield fixer.replaceText(operatorToken, negatedOperator);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow negated expression in equality check.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import {isParenthesized} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_TOO_DEEP = 'too-deep';
|
||||
const MESSAGE_ID_SHOULD_PARENTHESIZED = 'should-parenthesized';
|
||||
const messages = {
|
||||
[MESSAGE_ID_TOO_DEEP]: 'Do not nest ternary expressions.',
|
||||
[MESSAGE_ID_SHOULD_PARENTHESIZED]: 'Nested ternary expression should be parenthesized.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('ConditionalExpression', node => {
|
||||
if ([
|
||||
node.test,
|
||||
node.consequent,
|
||||
node.alternate,
|
||||
].some(node => node.type === 'ConditionalExpression')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const ancestors = sourceCode.getAncestors(node).toReversed();
|
||||
const nestLevel = ancestors.findIndex(node => node.type !== 'ConditionalExpression');
|
||||
|
||||
if (nestLevel === 1 && !isParenthesized(node, context)) {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_SHOULD_PARENTHESIZED,
|
||||
fix: fixer => [
|
||||
fixer.insertTextBefore(node, '('),
|
||||
fixer.insertTextAfter(node, ')'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Nesting more than one level not allowed
|
||||
if (nestLevel > 1) {
|
||||
return {
|
||||
node: nestLevel > 2 ? ancestors[nestLevel - 3] : node,
|
||||
messageId: MESSAGE_ID_TOO_DEEP,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow nested ternary expressions.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import {getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isParenthesized,
|
||||
needsSemicolon,
|
||||
isNumber,
|
||||
} from './utils/index.js';
|
||||
import {isNewExpression} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'error';
|
||||
const MESSAGE_ID_LENGTH = 'array-length';
|
||||
const MESSAGE_ID_ONLY_ELEMENT = 'only-element';
|
||||
const MESSAGE_ID_SPREAD = 'spread';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: '`new Array()` is unclear in intent; use either `[x]` or `Array.from({length: x})`',
|
||||
[MESSAGE_ID_LENGTH]: 'The argument is the length of array.',
|
||||
[MESSAGE_ID_ONLY_ELEMENT]: 'The argument is the only element of array.',
|
||||
[MESSAGE_ID_SPREAD]: 'Spread the argument.',
|
||||
};
|
||||
|
||||
function getProblem(context, node) {
|
||||
if (
|
||||
!isNewExpression(node, {
|
||||
name: 'Array',
|
||||
argumentsLength: 1,
|
||||
allowSpreadElement: true,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
};
|
||||
|
||||
const [argumentNode] = node.arguments;
|
||||
|
||||
const {sourceCode} = context;
|
||||
let text = sourceCode.getText(argumentNode);
|
||||
if (isParenthesized(argumentNode, context)) {
|
||||
text = `(${text})`;
|
||||
}
|
||||
|
||||
const maybeSemiColon = needsSemicolon(sourceCode.getTokenBefore(node), context, '[')
|
||||
? ';'
|
||||
: '';
|
||||
|
||||
// We are not sure how many `arguments` passed
|
||||
if (argumentNode.type === 'SpreadElement') {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SPREAD,
|
||||
fix: fixer => fixer.replaceText(node, `${maybeSemiColon}[${text}]`),
|
||||
},
|
||||
];
|
||||
return problem;
|
||||
}
|
||||
|
||||
const fromLengthText = `Array.from(${text === 'length' ? '{length}' : `{length: ${text}}`})`;
|
||||
const scope = sourceCode.getScope(node);
|
||||
if (isNumber(argumentNode, scope)) {
|
||||
problem.fix = fixer => fixer.replaceText(node, fromLengthText);
|
||||
return problem;
|
||||
}
|
||||
|
||||
const onlyElementText = `${maybeSemiColon}[${text}]`;
|
||||
const result = getStaticValue(argumentNode, scope);
|
||||
if (result !== null && typeof result.value !== 'number') {
|
||||
problem.fix = fixer => fixer.replaceText(node, onlyElementText);
|
||||
return problem;
|
||||
}
|
||||
|
||||
// We don't know the argument is number or not
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_LENGTH,
|
||||
fix: fixer => fixer.replaceText(node, fromLengthText),
|
||||
},
|
||||
{
|
||||
messageId: MESSAGE_ID_ONLY_ELEMENT,
|
||||
fix: fixer => fixer.replaceText(node, onlyElementText),
|
||||
},
|
||||
];
|
||||
return problem;
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('NewExpression', node => getProblem(context, node));
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow `new Array()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import {getStaticValue} from '@eslint-community/eslint-utils';
|
||||
import {switchNewExpressionToCallExpression} from './fix/index.js';
|
||||
import isNumber from './utils/is-number.js';
|
||||
import {isNewExpression} from './ast/index.js';
|
||||
|
||||
const ERROR = 'error';
|
||||
const ERROR_UNKNOWN = 'error-unknown';
|
||||
const SUGGESTION = 'suggestion';
|
||||
const messages = {
|
||||
[ERROR]: '`new Buffer()` is deprecated, use `Buffer.{{method}}()` instead.',
|
||||
[ERROR_UNKNOWN]: '`new Buffer()` is deprecated, use `Buffer.alloc()` or `Buffer.from()` instead.',
|
||||
[SUGGESTION]: 'Switch to `Buffer.{{replacement}}()`.',
|
||||
};
|
||||
|
||||
const inferMethod = (bufferArguments, scope) => {
|
||||
if (bufferArguments.length !== 1) {
|
||||
return 'from';
|
||||
}
|
||||
|
||||
const [firstArgument] = bufferArguments;
|
||||
if (firstArgument.type === 'SpreadElement') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstArgument.type === 'ArrayExpression' || firstArgument.type === 'TemplateLiteral') {
|
||||
return 'from';
|
||||
}
|
||||
|
||||
if (isNumber(firstArgument, scope)) {
|
||||
return 'alloc';
|
||||
}
|
||||
|
||||
const staticResult = getStaticValue(firstArgument, scope);
|
||||
if (staticResult) {
|
||||
const {value} = staticResult;
|
||||
if (
|
||||
typeof value === 'string'
|
||||
|| Array.isArray(value)
|
||||
) {
|
||||
return 'from';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function fix(node, context, method) {
|
||||
return function * (fixer) {
|
||||
yield fixer.insertTextAfter(node.callee, `.${method}`);
|
||||
yield switchNewExpressionToCallExpression(node, context, fixer);
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
context.on('NewExpression', node => {
|
||||
if (!isNewExpression(node, {name: 'Buffer'})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const method = inferMethod(node.arguments, sourceCode.getScope(node));
|
||||
|
||||
if (method) {
|
||||
return {
|
||||
node,
|
||||
messageId: ERROR,
|
||||
data: {method},
|
||||
fix: fix(node, context, method),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
messageId: ERROR_UNKNOWN,
|
||||
suggest: ['from', 'alloc'].map(replacement => ({
|
||||
messageId: SUGGESTION,
|
||||
data: {replacement},
|
||||
fix: fix(node, context, replacement),
|
||||
})),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import {isMethodCall, isCallExpression, isNullLiteral} from './ast/index.js';
|
||||
|
||||
const ERROR_MESSAGE_ID = 'error';
|
||||
const SUGGESTION_REPLACE_MESSAGE_ID = 'replace';
|
||||
const SUGGESTION_REMOVE_MESSAGE_ID = 'remove';
|
||||
const messages = {
|
||||
[ERROR_MESSAGE_ID]: 'Use `undefined` instead of `null`.',
|
||||
[SUGGESTION_REPLACE_MESSAGE_ID]: 'Replace `null` with `undefined`.',
|
||||
[SUGGESTION_REMOVE_MESSAGE_ID]: 'Remove `null`.',
|
||||
};
|
||||
|
||||
const isLooseEqual = node => node.type === 'BinaryExpression' && ['==', '!='].includes(node.operator);
|
||||
const isStrictEqual = node => node.type === 'BinaryExpression' && ['===', '!=='].includes(node.operator);
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {checkStrictEquality} = context.options[0];
|
||||
|
||||
context.on('Literal', node => {
|
||||
if (
|
||||
!isNullLiteral(node)
|
||||
|| (!checkStrictEquality && isStrictEqual(node.parent))
|
||||
// `Object.create(null)`, `Object.create(null, foo)`
|
||||
|| (
|
||||
isMethodCall(node.parent, {
|
||||
object: 'Object',
|
||||
method: 'create',
|
||||
minimumArguments: 1,
|
||||
maximumArguments: 2,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& node.parent.arguments[0] === node
|
||||
)
|
||||
// `useRef(null)`
|
||||
|| (
|
||||
isCallExpression(node.parent, {
|
||||
name: 'useRef',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& node.parent.arguments[0] === node
|
||||
)
|
||||
// `React.useRef(null)`
|
||||
|| (
|
||||
isMethodCall(node.parent, {
|
||||
object: 'React',
|
||||
method: 'useRef',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& node.parent.arguments[0] === node
|
||||
)
|
||||
// `foo.insertBefore(bar, null)`
|
||||
|| (
|
||||
isMethodCall(node.parent, {
|
||||
method: 'insertBefore',
|
||||
argumentsLength: 2,
|
||||
optionalCall: false,
|
||||
})
|
||||
&& node.parent.arguments[1] === node
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {parent} = node;
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: ERROR_MESSAGE_ID,
|
||||
};
|
||||
|
||||
const useUndefinedFix = fixer => fixer.replaceText(node, 'undefined');
|
||||
|
||||
if (isLooseEqual(parent)) {
|
||||
problem.fix = useUndefinedFix;
|
||||
return problem;
|
||||
}
|
||||
|
||||
const useUndefinedSuggestion = {
|
||||
messageId: SUGGESTION_REPLACE_MESSAGE_ID,
|
||||
fix: useUndefinedFix,
|
||||
};
|
||||
|
||||
if (parent.type === 'ReturnStatement' && parent.argument === node) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: SUGGESTION_REMOVE_MESSAGE_ID,
|
||||
fix: fixer => fixer.remove(node),
|
||||
},
|
||||
useUndefinedSuggestion,
|
||||
];
|
||||
return problem;
|
||||
}
|
||||
|
||||
if (parent.type === 'VariableDeclarator' && parent.init === node && parent.parent.kind !== 'const') {
|
||||
const {sourceCode} = context;
|
||||
const [, start] = sourceCode.getRange(parent.id);
|
||||
const [, end] = sourceCode.getRange(node);
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: SUGGESTION_REMOVE_MESSAGE_ID,
|
||||
fix: fixer => fixer.removeRange([start, end]),
|
||||
},
|
||||
useUndefinedSuggestion,
|
||||
];
|
||||
return problem;
|
||||
}
|
||||
|
||||
problem.suggest = [useUndefinedSuggestion];
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
checkStrictEquality: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to check strict equality comparisons against `null`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow the use of the `null` literal.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
schema,
|
||||
defaultOptions: [{checkStrictEquality: false}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import {isFunction} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID_IDENTIFIER = 'identifier';
|
||||
const MESSAGE_ID_NON_IDENTIFIER = 'non-identifier';
|
||||
const messages = {
|
||||
[MESSAGE_ID_IDENTIFIER]: 'Do not use an object literal as default for parameter `{{parameter}}`.',
|
||||
[MESSAGE_ID_NON_IDENTIFIER]: 'Do not use an object literal as default.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('AssignmentPattern', node => {
|
||||
if (!(
|
||||
node.right.type === 'ObjectExpression'
|
||||
&& node.right.properties.length > 0
|
||||
&& isFunction(node.parent)
|
||||
&& node.parent.params.includes(node)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {left, right} = node;
|
||||
|
||||
if (left.type === 'Identifier') {
|
||||
return {
|
||||
node: left,
|
||||
messageId: MESSAGE_ID_IDENTIFIER,
|
||||
data: {parameter: left.name},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
node: right,
|
||||
messageId: MESSAGE_ID_NON_IDENTIFIER,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow the use of objects as default parameters.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import {isStaticRequire, isMethodCall, isLiteral} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-process-exit';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Only use `process.exit()` in CLI apps. Throw an error instead.',
|
||||
};
|
||||
|
||||
const isWorkerThreads = node =>
|
||||
isLiteral(node, 'node:worker_threads')
|
||||
|| isLiteral(node, 'worker_threads');
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const startsWithHashBang = context.sourceCode.lines[0].indexOf('#!') === 0;
|
||||
|
||||
if (startsWithHashBang) {
|
||||
return;
|
||||
}
|
||||
|
||||
let processEventHandler;
|
||||
|
||||
// Only report if it's outside a worker thread context. See #328.
|
||||
let requiredWorkerThreadsModule = false;
|
||||
const problemNodes = [];
|
||||
|
||||
// `require('worker_threads')`
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (
|
||||
isStaticRequire(callExpression)
|
||||
&& isWorkerThreads(callExpression.arguments[0])
|
||||
) {
|
||||
requiredWorkerThreadsModule = true;
|
||||
}
|
||||
});
|
||||
|
||||
// `import workerThreads from 'worker_threads'`
|
||||
context.on('ImportDeclaration', importDeclaration => {
|
||||
if (
|
||||
importDeclaration.source.type === 'Literal'
|
||||
&& isWorkerThreads(importDeclaration.source)
|
||||
) {
|
||||
requiredWorkerThreadsModule = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check `process.on` / `process.once` call
|
||||
context.on('CallExpression', node => {
|
||||
if (isMethodCall(node, {
|
||||
object: 'process',
|
||||
methods: ['on', 'once'],
|
||||
minimumArguments: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})) {
|
||||
processEventHandler = node;
|
||||
}
|
||||
});
|
||||
context.onExit('CallExpression', node => {
|
||||
if (node === processEventHandler) {
|
||||
processEventHandler = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Check `process.exit` call
|
||||
context.on('CallExpression', node => {
|
||||
if (
|
||||
!processEventHandler
|
||||
&& isMethodCall(node, {
|
||||
object: 'process',
|
||||
method: 'exit',
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
) {
|
||||
problemNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
context.onExit('Program', function * () {
|
||||
if (requiredWorkerThreadsModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const node of problemNodes) {
|
||||
yield {
|
||||
node,
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow `process.exit()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
import {isCommaToken} from '@eslint-community/eslint-utils';
|
||||
import {isMethodCall, isExpressionStatement} from './ast/index.js';
|
||||
import {
|
||||
getParenthesizedText,
|
||||
isParenthesized,
|
||||
needsSemicolon,
|
||||
shouldAddParenthesesToAwaitExpressionArgument,
|
||||
} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-single-promise-in-promise-methods/error';
|
||||
const MESSAGE_ID_SUGGESTION_UNWRAP = 'no-single-promise-in-promise-methods/unwrap';
|
||||
const MESSAGE_ID_SUGGESTION_SWITCH_TO_PROMISE_RESOLVE = 'no-single-promise-in-promise-methods/use-promise-resolve';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Wrapping single-element array with `Promise.{{method}}()` is unnecessary.',
|
||||
[MESSAGE_ID_SUGGESTION_UNWRAP]: 'Use the value directly.',
|
||||
[MESSAGE_ID_SUGGESTION_SWITCH_TO_PROMISE_RESOLVE]: 'Switch to `Promise.resolve(…)`.',
|
||||
};
|
||||
const METHODS = ['all', 'any', 'race'];
|
||||
|
||||
const isPromiseMethodCallWithSingleElementArray = node =>
|
||||
isMethodCall(node, {
|
||||
object: 'Promise',
|
||||
methods: METHODS,
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
argumentsLength: 1,
|
||||
})
|
||||
&& node.arguments[0].type === 'ArrayExpression'
|
||||
&& node.arguments[0].elements.length === 1
|
||||
&& node.arguments[0].elements[0]
|
||||
&& node.arguments[0].elements[0].type !== 'SpreadElement';
|
||||
|
||||
const unwrapAwaitedCallExpression = (callExpression, context) => fixer => {
|
||||
const [promiseNode] = callExpression.arguments[0].elements;
|
||||
let text = getParenthesizedText(promiseNode, context);
|
||||
|
||||
if (
|
||||
!isParenthesized(promiseNode, context)
|
||||
&& shouldAddParenthesesToAwaitExpressionArgument(promiseNode)
|
||||
) {
|
||||
text = `(${text})`;
|
||||
}
|
||||
|
||||
// The next node is already behind a `CallExpression`, there should be no ASI problem
|
||||
|
||||
return fixer.replaceText(callExpression, text);
|
||||
};
|
||||
|
||||
const unwrapNonAwaitedCallExpression = (callExpression, context) => fixer => {
|
||||
const [promiseNode] = callExpression.arguments[0].elements;
|
||||
let text = getParenthesizedText(promiseNode, context);
|
||||
|
||||
if (
|
||||
!isParenthesized(promiseNode, context)
|
||||
// Since the original call expression can be anywhere, it's hard to tell if the promise
|
||||
// need to be parenthesized, but it's safe to add parentheses
|
||||
&& !(
|
||||
// Known cases that not need parentheses
|
||||
promiseNode.type === 'Identifier'
|
||||
|| promiseNode.type === 'MemberExpression'
|
||||
)
|
||||
) {
|
||||
text = `(${text})`;
|
||||
}
|
||||
|
||||
const previousToken = context.sourceCode.getTokenBefore(callExpression);
|
||||
if (needsSemicolon(previousToken, context, text)) {
|
||||
text = `;${text}`;
|
||||
}
|
||||
|
||||
return fixer.replaceText(callExpression, text);
|
||||
};
|
||||
|
||||
const switchToPromiseResolve = (callExpression, sourceCode) => function * (fixer) {
|
||||
/*
|
||||
```
|
||||
Promise.race([promise,])
|
||||
// ^^^^ methodNameNode
|
||||
```
|
||||
*/
|
||||
const methodNameNode = callExpression.callee.property;
|
||||
yield fixer.replaceText(methodNameNode, 'resolve');
|
||||
|
||||
const [arrayExpression] = callExpression.arguments;
|
||||
/*
|
||||
```
|
||||
Promise.race([promise,])
|
||||
// ^ openingBracketToken
|
||||
```
|
||||
*/
|
||||
const openingBracketToken = sourceCode.getFirstToken(arrayExpression);
|
||||
/*
|
||||
```
|
||||
Promise.race([promise,])
|
||||
// ^ penultimateToken
|
||||
// ^ closingBracketToken
|
||||
```
|
||||
*/
|
||||
const [
|
||||
penultimateToken,
|
||||
closingBracketToken,
|
||||
] = sourceCode.getLastTokens(arrayExpression, 2);
|
||||
|
||||
yield fixer.remove(openingBracketToken);
|
||||
yield fixer.remove(closingBracketToken);
|
||||
|
||||
if (isCommaToken(penultimateToken)) {
|
||||
yield fixer.remove(penultimateToken);
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (!isPromiseMethodCallWithSingleElementArray(callExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methodName = callExpression.callee.property.name;
|
||||
|
||||
const problem = {
|
||||
node: callExpression.arguments[0],
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {
|
||||
method: methodName,
|
||||
},
|
||||
};
|
||||
|
||||
const {sourceCode} = context;
|
||||
|
||||
if (
|
||||
callExpression.parent.type === 'AwaitExpression'
|
||||
&& callExpression.parent.argument === callExpression
|
||||
&& (
|
||||
methodName !== 'all'
|
||||
|| isExpressionStatement(callExpression.parent.parent)
|
||||
)
|
||||
) {
|
||||
problem.fix = unwrapAwaitedCallExpression(callExpression, context);
|
||||
return problem;
|
||||
}
|
||||
|
||||
if (methodName === 'all') {
|
||||
return problem;
|
||||
}
|
||||
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_UNWRAP,
|
||||
fix: unwrapNonAwaitedCallExpression(callExpression, context),
|
||||
},
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_SWITCH_TO_PROMISE_RESOLVE,
|
||||
fix: switchToPromiseResolve(callExpression, sourceCode),
|
||||
},
|
||||
];
|
||||
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow passing single-element arrays to `Promise` methods.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import {isSemicolonToken} from '@eslint-community/eslint-utils';
|
||||
import getClassHeadLocation from './utils/get-class-head-location.js';
|
||||
import assertToken from './utils/assert-token.js';
|
||||
import {removeSpacesAfter} from './fix/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-static-only-class';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Use an object instead of a class with only static members.',
|
||||
};
|
||||
|
||||
const isEqualToken = ({type, value}) => type === 'Punctuator' && value === '=';
|
||||
const isDeclarationOfExportDefaultDeclaration = node =>
|
||||
node.type === 'ClassDeclaration'
|
||||
&& node.parent.type === 'ExportDefaultDeclaration'
|
||||
&& node.parent.declaration === node;
|
||||
|
||||
const isPropertyDefinition = node => node.type === 'PropertyDefinition';
|
||||
const isMethodDefinition = node => node.type === 'MethodDefinition';
|
||||
|
||||
function isStaticMember(node) {
|
||||
const {
|
||||
private: isPrivate,
|
||||
static: isStatic,
|
||||
declare: isDeclare,
|
||||
readonly: isReadonly,
|
||||
accessibility,
|
||||
decorators,
|
||||
key,
|
||||
} = node;
|
||||
|
||||
// Avoid matching unexpected node. For example: https://github.com/tc39/proposal-class-static-block
|
||||
if (!isPropertyDefinition(node) && !isMethodDefinition(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isStatic || isPrivate || key.type === 'PrivateIdentifier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TypeScript class
|
||||
if (
|
||||
isDeclare
|
||||
|| isReadonly
|
||||
|| accessibility !== undefined
|
||||
|| (Array.isArray(decorators) && decorators.length > 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function * switchClassMemberToObjectProperty(node, context, fixer) {
|
||||
const {sourceCode} = context;
|
||||
const staticToken = sourceCode.getFirstToken(node);
|
||||
assertToken(staticToken, {
|
||||
expected: {type: 'Keyword', value: 'static'},
|
||||
ruleId: 'no-static-only-class',
|
||||
});
|
||||
|
||||
yield fixer.remove(staticToken);
|
||||
yield removeSpacesAfter(staticToken, context, fixer);
|
||||
|
||||
const maybeSemicolonToken = isPropertyDefinition(node)
|
||||
? sourceCode.getLastToken(node)
|
||||
: sourceCode.getTokenAfter(node);
|
||||
const hasSemicolonToken = isSemicolonToken(maybeSemicolonToken);
|
||||
|
||||
if (isPropertyDefinition(node)) {
|
||||
const {key, value} = node;
|
||||
|
||||
if (value) {
|
||||
// Computed key may have `]` after `key`
|
||||
const equalToken = sourceCode.getTokenAfter(key, isEqualToken);
|
||||
yield fixer.replaceText(equalToken, ':');
|
||||
} else if (hasSemicolonToken) {
|
||||
yield fixer.insertTextBefore(maybeSemicolonToken, ': undefined');
|
||||
} else {
|
||||
yield fixer.insertTextAfter(node, ': undefined');
|
||||
}
|
||||
}
|
||||
|
||||
yield (
|
||||
hasSemicolonToken
|
||||
? fixer.replaceText(maybeSemicolonToken, ',')
|
||||
: fixer.insertTextAfter(node, ',')
|
||||
);
|
||||
}
|
||||
|
||||
function switchClassToObject(node, context) {
|
||||
const {
|
||||
type,
|
||||
id,
|
||||
body,
|
||||
declare: isDeclare,
|
||||
abstract: isAbstract,
|
||||
implements: classImplements,
|
||||
parent,
|
||||
} = node;
|
||||
|
||||
if (
|
||||
isDeclare
|
||||
|| isAbstract
|
||||
|| (Array.isArray(classImplements) && classImplements.length > 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ClassExpression' && id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExportDefault = isDeclarationOfExportDefaultDeclaration(node);
|
||||
|
||||
if (isExportDefault && id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
for (const node of body.body) {
|
||||
if (
|
||||
isPropertyDefinition(node)
|
||||
&& (
|
||||
node.typeAnnotation
|
||||
// This is a stupid way to check if `value` of `PropertyDefinition` uses `this`
|
||||
|| (node.value && sourceCode.getText(node.value).includes('this'))
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return function * (fixer) {
|
||||
const classToken = sourceCode.getFirstToken(node);
|
||||
/* c8 ignore next */
|
||||
assertToken(classToken, {
|
||||
expected: {type: 'Keyword', value: 'class'},
|
||||
ruleId: 'no-static-only-class',
|
||||
});
|
||||
|
||||
if (isExportDefault || type === 'ClassExpression') {
|
||||
/*
|
||||
There are comments after return, and `{` is not on same line
|
||||
|
||||
```js
|
||||
function a() {
|
||||
return class // comment
|
||||
{
|
||||
static a() {}
|
||||
}
|
||||
}
|
||||
```
|
||||
*/
|
||||
if (
|
||||
type === 'ClassExpression'
|
||||
&& parent.type === 'ReturnStatement'
|
||||
&& sourceCode.getLoc(body).start.line !== sourceCode.getLoc(parent).start.line
|
||||
&& sourceCode.text.slice(sourceCode.getRange(classToken)[1], sourceCode.getRange(body)[0]).trim()
|
||||
) {
|
||||
yield fixer.replaceText(classToken, '{');
|
||||
|
||||
const openingBraceToken = sourceCode.getFirstToken(body);
|
||||
yield fixer.remove(openingBraceToken);
|
||||
} else {
|
||||
yield fixer.replaceText(classToken, '');
|
||||
|
||||
/*
|
||||
Avoid breaking case like
|
||||
|
||||
```js
|
||||
return class
|
||||
{};
|
||||
```
|
||||
*/
|
||||
yield removeSpacesAfter(classToken, context, fixer);
|
||||
}
|
||||
|
||||
// There should not be ASI problem
|
||||
} else {
|
||||
yield fixer.replaceText(classToken, 'const');
|
||||
yield fixer.insertTextBefore(body, '= ');
|
||||
yield fixer.insertTextAfter(body, ';');
|
||||
}
|
||||
|
||||
for (const node of body.body) {
|
||||
yield switchClassMemberToObjectProperty(node, context, fixer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function create(context) {
|
||||
context.on(['ClassDeclaration', 'ClassExpression'], node => {
|
||||
if (
|
||||
node.superClass
|
||||
|| (node.decorators && node.decorators.length > 0)
|
||||
|| node.body.type !== 'ClassBody'
|
||||
|| node.body.body.length === 0
|
||||
|| node.body.body.some(node => !isStaticMember(node))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
loc: getClassHeadLocation(node, context),
|
||||
messageId: MESSAGE_ID,
|
||||
fix: switchClassToObject(node, context),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow classes that only have static members.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import {getStaticValue, getPropertyName} from '@eslint-community/eslint-utils';
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
|
||||
const MESSAGE_ID_OBJECT = 'no-thenable-object';
|
||||
const MESSAGE_ID_EXPORT = 'no-thenable-export';
|
||||
const MESSAGE_ID_CLASS = 'no-thenable-class';
|
||||
const messages = {
|
||||
[MESSAGE_ID_OBJECT]: 'Do not add `then` to an object.',
|
||||
[MESSAGE_ID_EXPORT]: 'Do not export `then`.',
|
||||
[MESSAGE_ID_CLASS]: 'Do not add `then` to a class.',
|
||||
};
|
||||
|
||||
const isStringThen = (node, context) =>
|
||||
getStaticValue(node, context.sourceCode.getScope(node))?.value === 'then';
|
||||
|
||||
const isPropertyThen = (node, context) =>
|
||||
getPropertyName(node, context.sourceCode.getScope(node)) === 'then';
|
||||
|
||||
const cases = [
|
||||
// `{then() {}}`,
|
||||
// `{get then() {}}`,
|
||||
// `{[computedKey]() {}}`,
|
||||
// `{get [computedKey]() {}}`,
|
||||
{
|
||||
selector: 'ObjectExpression',
|
||||
* getNodes(node, context) {
|
||||
for (const property of node.properties) {
|
||||
if (property.type === 'Property' && isPropertyThen(property, context)) {
|
||||
yield property.key;
|
||||
}
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_OBJECT,
|
||||
},
|
||||
// `class Foo {then}`,
|
||||
// `class Foo {static then}`,
|
||||
// `class Foo {get then() {}}`,
|
||||
// `class Foo {static get then() {}}`,
|
||||
{
|
||||
selectors: ['PropertyDefinition', 'MethodDefinition'],
|
||||
* getNodes(node, context) {
|
||||
if (getPropertyName(node, context.sourceCode.getScope(node)) === 'then') {
|
||||
yield node.key;
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_CLASS,
|
||||
},
|
||||
// `foo.then = …`
|
||||
// `foo[computedKey] = …`
|
||||
{
|
||||
selector: 'MemberExpression',
|
||||
* getNodes(node, context) {
|
||||
if (!(node.parent.type === 'AssignmentExpression' && node.parent.left === node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getPropertyName(node, context.sourceCode.getScope(node)) === 'then') {
|
||||
yield node.property;
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_OBJECT,
|
||||
},
|
||||
// `Object.defineProperty(foo, 'then', …)`
|
||||
// `Reflect.defineProperty(foo, 'then', …)`
|
||||
{
|
||||
selector: 'CallExpression',
|
||||
* getNodes(node, context) {
|
||||
if (!(
|
||||
isMethodCall(node, {
|
||||
objects: ['Object', 'Reflect'],
|
||||
method: 'defineProperty',
|
||||
minimumArguments: 3,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& node.arguments[0].type !== 'SpreadElement'
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, secondArgument] = node.arguments;
|
||||
if (isStringThen(secondArgument, context)) {
|
||||
yield secondArgument;
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_OBJECT,
|
||||
},
|
||||
// `Object.fromEntries([['then', …]])`
|
||||
{
|
||||
selector: 'CallExpression',
|
||||
* getNodes(node, context) {
|
||||
if (!(
|
||||
isMethodCall(node, {
|
||||
object: 'Object',
|
||||
method: 'fromEntries',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& node.arguments[0].type === 'ArrayExpression'
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const pairs of node.arguments[0].elements) {
|
||||
if (
|
||||
pairs?.type === 'ArrayExpression'
|
||||
&& pairs.elements[0]
|
||||
&& pairs.elements[0].type !== 'SpreadElement'
|
||||
) {
|
||||
const [key] = pairs.elements;
|
||||
|
||||
if (isStringThen(key, context)) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_OBJECT,
|
||||
},
|
||||
// `export {then}`
|
||||
{
|
||||
selector: 'Identifier',
|
||||
* getNodes(node) {
|
||||
if (
|
||||
node.name === 'then'
|
||||
&& node.parent.type === 'ExportSpecifier'
|
||||
&& node.parent.exported === node
|
||||
) {
|
||||
yield node;
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_EXPORT,
|
||||
},
|
||||
// `export function then() {}`,
|
||||
// `export class then {}`,
|
||||
{
|
||||
selector: 'Identifier',
|
||||
* getNodes(node) {
|
||||
if (
|
||||
node.name === 'then'
|
||||
&& (node.parent.type === 'FunctionDeclaration' || node.parent.type === 'ClassDeclaration')
|
||||
&& node.parent.id === node
|
||||
&& node.parent.parent.type === 'ExportNamedDeclaration'
|
||||
&& node.parent.parent.declaration === node.parent
|
||||
) {
|
||||
yield node;
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_EXPORT,
|
||||
},
|
||||
// `export const … = …`;
|
||||
{
|
||||
selector: 'VariableDeclaration',
|
||||
* getNodes(node, context) {
|
||||
if (!(node.parent.type === 'ExportNamedDeclaration' && node.parent.declaration === node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const variable of context.sourceCode.getDeclaredVariables(node)) {
|
||||
if (variable.name === 'then') {
|
||||
yield * variable.identifiers;
|
||||
}
|
||||
}
|
||||
},
|
||||
messageId: MESSAGE_ID_EXPORT,
|
||||
},
|
||||
];
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
for (const {selector, selectors, messageId, getNodes} of cases) {
|
||||
context.on(selector ?? selectors, function * (node) {
|
||||
for (const problematicNode of getNodes(node, context)) {
|
||||
yield {node: problematicNode, messageId};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow `then` property.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
const MESSAGE_ID = 'no-this-assignment';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not assign `this` to `{{name}}`.',
|
||||
};
|
||||
|
||||
function getProblem(variableNode, valueNode) {
|
||||
if (
|
||||
variableNode.type !== 'Identifier'
|
||||
|| valueNode?.type !== 'ThisExpression'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node: valueNode.parent,
|
||||
data: {name: variableNode.name},
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('VariableDeclarator', node => getProblem(node.id, node.init));
|
||||
context.on('AssignmentExpression', node => getProblem(node.left, node.right));
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow assigning `this` to a variable.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import {isLiteral} from './ast/index.js';
|
||||
import {
|
||||
addParenthesizesToReturnOrThrowExpression,
|
||||
removeSpacesAfter,
|
||||
} from './fix/index.js';
|
||||
import {
|
||||
needsSemicolon,
|
||||
isParenthesized,
|
||||
isOnSameLine,
|
||||
isUnresolvedVariable,
|
||||
} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID_ERROR = 'no-typeof-undefined/error';
|
||||
const MESSAGE_ID_SUGGESTION = 'no-typeof-undefined/suggestion';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Compare with `undefined` directly instead of using `typeof`.',
|
||||
[MESSAGE_ID_SUGGESTION]: 'Switch to `… {{operator}} undefined`.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {checkGlobalVariables} = context.options[0];
|
||||
|
||||
context.on('BinaryExpression', binaryExpression => {
|
||||
if (!(
|
||||
(
|
||||
binaryExpression.operator === '==='
|
||||
|| binaryExpression.operator === '!=='
|
||||
|| binaryExpression.operator === '=='
|
||||
|| binaryExpression.operator === '!='
|
||||
)
|
||||
&& binaryExpression.left.type === 'UnaryExpression'
|
||||
&& binaryExpression.left.operator === 'typeof'
|
||||
&& binaryExpression.left.prefix
|
||||
&& isLiteral(binaryExpression.right, 'undefined')
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {left: typeofNode, right: undefinedString, operator} = binaryExpression;
|
||||
const {sourceCode} = context;
|
||||
const valueNode = typeofNode.argument;
|
||||
const isGlobalVariable = valueNode.type === 'Identifier'
|
||||
&& (sourceCode.isGlobalReference(valueNode) || isUnresolvedVariable(valueNode, context));
|
||||
|
||||
if (!checkGlobalVariables && isGlobalVariable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [typeofToken, secondToken] = sourceCode.getFirstTokens(typeofNode, 2);
|
||||
|
||||
const fix = function * (fixer) {
|
||||
// Change `==`/`!=` to `===`/`!==`
|
||||
if (operator === '==' || operator === '!=') {
|
||||
const operatorToken = sourceCode.getTokenAfter(
|
||||
typeofNode,
|
||||
token => token.type === 'Punctuator' && token.value === operator,
|
||||
);
|
||||
|
||||
yield fixer.insertTextAfter(operatorToken, '=');
|
||||
}
|
||||
|
||||
yield fixer.replaceText(undefinedString, 'undefined');
|
||||
|
||||
yield fixer.remove(typeofToken);
|
||||
yield removeSpacesAfter(typeofToken, context, fixer);
|
||||
|
||||
const {parent} = binaryExpression;
|
||||
if (
|
||||
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
|
||||
&& parent.argument === binaryExpression
|
||||
&& !isOnSameLine(typeofToken, secondToken, context)
|
||||
&& !isParenthesized(binaryExpression, context)
|
||||
&& !isParenthesized(typeofNode, context)
|
||||
) {
|
||||
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenBefore = sourceCode.getTokenBefore(binaryExpression);
|
||||
if (needsSemicolon(tokenBefore, context, secondToken.value)) {
|
||||
yield fixer.insertTextBefore(binaryExpression, ';');
|
||||
}
|
||||
};
|
||||
|
||||
const problem = {
|
||||
node: binaryExpression,
|
||||
loc: sourceCode.getLoc(typeofToken),
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
};
|
||||
|
||||
if (isGlobalVariable) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION,
|
||||
data: {operator: operator.startsWith('!') ? '!==' : '==='},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
checkGlobalVariables: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to also check `typeof` comparisons against global variables.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow comparing `undefined` using `typeof`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
schema,
|
||||
defaultOptions: [{checkGlobalVariables: false}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
import {isMethodCall, isLiteral} from './ast/index.js';
|
||||
import {removeArgument} from './fix/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-unnecessary-array-flat-depth';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Passing `1` as the `depth` argument is unnecessary.',
|
||||
};
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', callExpression => {
|
||||
if (!(
|
||||
isMethodCall(callExpression, {
|
||||
method: 'flat',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
})
|
||||
&& isLiteral(callExpression.arguments[0], 1)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [numberOne] = callExpression.arguments;
|
||||
|
||||
return {
|
||||
node: numberOne,
|
||||
messageId: MESSAGE_ID,
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
fix: fixer => removeArgument(fixer, numberOne, context),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow using `1` as the `depth` argument of `Array#flat()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import {listen} from './shared/no-unnecessary-length-or-infinity-rule.js';
|
||||
|
||||
const MESSAGE_ID = 'no-unnecessary-array-splice-count';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Passing `{{description}}` as the `{{argumentName}}` argument is unnecessary.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create(context) {
|
||||
listen(context, {methods: ['splice', 'toSpliced'], messageId: MESSAGE_ID});
|
||||
},
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow using `.length` or `Infinity` as the `deleteCount` or `skipCount` argument of `Array#{splice,toSpliced}()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import {addParenthesizesToReturnOrThrowExpression, removeSpacesAfter} from './fix/index.js';
|
||||
import {isParenthesized, needsSemicolon, isOnSameLine} from './utils/index.js';
|
||||
|
||||
const MESSAGE_ID = 'no-unnecessary-await';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: 'Do not `await` non-promise value.',
|
||||
};
|
||||
|
||||
function notPromise(node) {
|
||||
switch (node.type) {
|
||||
case 'ArrayExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'AwaitExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'ClassExpression':
|
||||
case 'FunctionExpression':
|
||||
case 'JSXElement':
|
||||
case 'JSXFragment':
|
||||
case 'Literal':
|
||||
case 'TemplateLiteral':
|
||||
case 'UnaryExpression':
|
||||
case 'UpdateExpression': {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'SequenceExpression': {
|
||||
return notPromise(node.expressions.at(-1));
|
||||
}
|
||||
|
||||
// No default
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('AwaitExpression', node => {
|
||||
if (
|
||||
// F#-style pipeline operator, `Promise.resolve() |> await`
|
||||
!node.argument
|
||||
|| !notPromise(node.argument)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
const awaitToken = sourceCode.getFirstToken(node);
|
||||
const problem = {
|
||||
node,
|
||||
loc: sourceCode.getLoc(awaitToken),
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
|
||||
const valueNode = node.argument;
|
||||
if (
|
||||
// Removing `await` may change them to a declaration, if there is no `id` will cause SyntaxError
|
||||
valueNode.type === 'FunctionExpression'
|
||||
|| valueNode.type === 'ClassExpression'
|
||||
// `+await +1` -> `++1`
|
||||
|| (
|
||||
node.parent.type === 'UnaryExpression'
|
||||
&& valueNode.type === 'UnaryExpression'
|
||||
&& node.parent.operator === valueNode.operator
|
||||
)
|
||||
) {
|
||||
return problem;
|
||||
}
|
||||
|
||||
return Object.assign(problem, {
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
* fix(fixer) {
|
||||
if (
|
||||
!isOnSameLine(awaitToken, valueNode, context)
|
||||
&& !isParenthesized(node, context)
|
||||
) {
|
||||
yield addParenthesizesToReturnOrThrowExpression(fixer, node.parent, context);
|
||||
}
|
||||
|
||||
yield fixer.remove(awaitToken);
|
||||
yield removeSpacesAfter(awaitToken, context, fixer);
|
||||
|
||||
const nextToken = sourceCode.getTokenAfter(awaitToken);
|
||||
const tokenBefore = sourceCode.getTokenBefore(awaitToken);
|
||||
if (needsSemicolon(tokenBefore, context, nextToken.value)) {
|
||||
yield fixer.insertTextBefore(nextToken, ';');
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow awaiting non-promise values.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
import path from 'node:path';
|
||||
import coreJsCompat from 'core-js-compat';
|
||||
import {camelCase} from 'change-case';
|
||||
import isStaticRequire from './ast/is-static-require.js';
|
||||
import {readPackageJson} from './shared/package-json.js';
|
||||
|
||||
const {data: compatData, entries: coreJsEntries} = coreJsCompat;
|
||||
|
||||
const MESSAGE_ID_POLYFILL = 'unnecessaryPolyfill';
|
||||
const MESSAGE_ID_CORE_JS = 'unnecessaryCoreJsModule';
|
||||
const messages = {
|
||||
[MESSAGE_ID_POLYFILL]: 'Use built-in instead.',
|
||||
[MESSAGE_ID_CORE_JS]:
|
||||
'All polyfilled features imported from `{{coreJsModule}}` are available as built-ins. Use the built-ins instead.',
|
||||
};
|
||||
|
||||
const additionalPolyfillModules = {
|
||||
'es.promise.finally': ['p-finally'],
|
||||
'es.object.set-prototype-of': ['setprototypeof'],
|
||||
'es.string.code-point-at': ['code-point-at'],
|
||||
};
|
||||
const additionalPolyfillPatterns = Object.fromEntries(Object.entries(additionalPolyfillModules).map(([feature, modules]) => [feature, `|(${modules.join('|')})`]));
|
||||
|
||||
const prefixes = '(mdn-polyfills/|polyfill-)';
|
||||
const suffixes = '(-polyfill)';
|
||||
const delimiter = String.raw`(\.|-|\.prototype\.|/)?`;
|
||||
const moduleDelimiter = /[./-]/u;
|
||||
|
||||
const getFirstSegment = value => {
|
||||
const [firstSegment = ''] = value.split(moduleDelimiter);
|
||||
return firstSegment;
|
||||
};
|
||||
|
||||
const stripPolyfillPrefix = value => {
|
||||
if (value.startsWith('polyfill-')) {
|
||||
return value.slice('polyfill-'.length);
|
||||
}
|
||||
|
||||
if (value.startsWith('mdn-polyfills/')) {
|
||||
return value.slice('mdn-polyfills/'.length);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function addPolyfillToken(tokens, value) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lowercaseValue = value.toLowerCase();
|
||||
tokens.add(lowercaseValue);
|
||||
tokens.add(getFirstSegment(lowercaseValue));
|
||||
|
||||
const camelCasedValue = camelCase(value).toLowerCase();
|
||||
tokens.add(camelCasedValue);
|
||||
tokens.add(getFirstSegment(camelCasedValue));
|
||||
}
|
||||
|
||||
const polyfills = Object.keys(compatData).map(feature => {
|
||||
const [rawEcmaVersion, rawConstructorName, rawMethodName = ''] = feature.split('.');
|
||||
let ecmaVersion = rawEcmaVersion;
|
||||
let constructorName = rawConstructorName;
|
||||
let methodName = rawMethodName;
|
||||
|
||||
if (ecmaVersion === 'es') {
|
||||
ecmaVersion = String.raw`(es\d*)`;
|
||||
}
|
||||
|
||||
constructorName = `(${constructorName}|${camelCase(constructorName)})`;
|
||||
methodName &&= `(${methodName}|${camelCase(methodName)})`;
|
||||
|
||||
const methodOrConstructor = methodName || constructorName;
|
||||
|
||||
const patterns = [
|
||||
`^((${prefixes}?(`,
|
||||
methodName && `(${ecmaVersion}${delimiter}${constructorName}${delimiter}${methodName})|`, // Ex: es6-array-copy-within
|
||||
methodName && `(${constructorName}${delimiter}${methodName})|`, // Ex: array-copy-within
|
||||
`(${ecmaVersion}${delimiter}${constructorName}))`, // Ex: es6-array
|
||||
`${suffixes}?)|`,
|
||||
`(${prefixes}${methodOrConstructor}|${methodOrConstructor}${suffixes})`, // Ex: polyfill-copy-within / polyfill-promise
|
||||
`${additionalPolyfillPatterns[feature] || ''})$`,
|
||||
];
|
||||
|
||||
return {
|
||||
feature,
|
||||
pattern: new RegExp(patterns.join(''), 'i'),
|
||||
tokens: (() => {
|
||||
const tokens = new Set();
|
||||
|
||||
if (rawEcmaVersion === 'es') {
|
||||
tokens.add('es');
|
||||
} else {
|
||||
addPolyfillToken(tokens, rawEcmaVersion);
|
||||
}
|
||||
|
||||
addPolyfillToken(tokens, rawConstructorName);
|
||||
addPolyfillToken(tokens, rawMethodName);
|
||||
|
||||
for (const module of additionalPolyfillModules[feature] || []) {
|
||||
addPolyfillToken(tokens, module);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
})(),
|
||||
};
|
||||
});
|
||||
const polyfillsByToken = new Map();
|
||||
const polyfillTokensByFirstCharacter = new Map();
|
||||
const esConstructorTokens = new Set();
|
||||
|
||||
for (const polyfill of polyfills) {
|
||||
const [ecmaVersion, constructorName] = polyfill.feature.split('.');
|
||||
if (ecmaVersion === 'es') {
|
||||
esConstructorTokens.add(constructorName.toLowerCase());
|
||||
esConstructorTokens.add(camelCase(constructorName).toLowerCase());
|
||||
}
|
||||
|
||||
for (const token of polyfill.tokens) {
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (polyfillsByToken.has(token)) {
|
||||
polyfillsByToken.get(token).push(polyfill);
|
||||
} else {
|
||||
polyfillsByToken.set(token, [polyfill]);
|
||||
}
|
||||
|
||||
const firstCharacter = token[0];
|
||||
if (polyfillTokensByFirstCharacter.has(firstCharacter)) {
|
||||
polyfillTokensByFirstCharacter.get(firstCharacter).add(token);
|
||||
} else {
|
||||
polyfillTokensByFirstCharacter.set(firstCharacter, new Set([token]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasEsConstructorPrefix = value => {
|
||||
for (const token of esConstructorTokens) {
|
||||
if (value.startsWith(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isPotentialEsPrefix = importedModule => {
|
||||
if (!importedModule.startsWith('es')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let constructorIndex = 2;
|
||||
while (
|
||||
constructorIndex < importedModule.length
|
||||
&& importedModule[constructorIndex] >= '0'
|
||||
&& importedModule[constructorIndex] <= '9'
|
||||
) {
|
||||
constructorIndex++;
|
||||
}
|
||||
|
||||
if (importedModule.startsWith('.prototype.', constructorIndex)) {
|
||||
constructorIndex += '.prototype.'.length;
|
||||
} else if (['.', '-', '/'].includes(importedModule[constructorIndex])) {
|
||||
constructorIndex++;
|
||||
}
|
||||
|
||||
return hasEsConstructorPrefix(importedModule.slice(constructorIndex));
|
||||
};
|
||||
|
||||
const getPolyfillCandidates = importedModule => {
|
||||
const normalizedImportedModule = stripPolyfillPrefix(importedModule);
|
||||
if (!normalizedImportedModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstCharacter = normalizedImportedModule[0];
|
||||
const tokens = polyfillTokensByFirstCharacter.get(firstCharacter);
|
||||
if (!tokens) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = new Set();
|
||||
const firstSegment = getFirstSegment(normalizedImportedModule);
|
||||
if (firstSegment === normalizedImportedModule) {
|
||||
for (const token of tokens) {
|
||||
if (token === 'es') {
|
||||
if (!isPotentialEsPrefix(normalizedImportedModule)) {
|
||||
continue;
|
||||
}
|
||||
} else if (!normalizedImportedModule.startsWith(token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const polyfill of polyfillsByToken.get(token)) {
|
||||
candidates.add(polyfill);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const token of tokens) {
|
||||
if (
|
||||
token === 'es'
|
||||
|| !firstSegment.startsWith(token)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const polyfill of polyfillsByToken.get(token)) {
|
||||
candidates.add(polyfill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPotentialEsPrefix(normalizedImportedModule)) {
|
||||
for (const polyfill of polyfillsByToken.get('es') || []) {
|
||||
candidates.add(polyfill);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
};
|
||||
|
||||
function getTargets(options, dirname) {
|
||||
if (options?.targets) {
|
||||
return options.targets;
|
||||
}
|
||||
|
||||
const packageJsonResult = readPackageJson(dirname);
|
||||
|
||||
if (!packageJsonResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {browserslist, engines} = packageJsonResult.packageJson;
|
||||
return browserslist ?? engines;
|
||||
}
|
||||
|
||||
function create(context) {
|
||||
const targets = getTargets(context.options[0], path.dirname(context.filename));
|
||||
if (!targets) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unavailableFeatures;
|
||||
try {
|
||||
unavailableFeatures = coreJsCompat({targets}).list;
|
||||
} catch {
|
||||
// This can happen if the targets are invalid or use unsupported syntax like `{node:'*'}`.
|
||||
return;
|
||||
}
|
||||
|
||||
const unavailableFeatureSet = new Set(unavailableFeatures);
|
||||
|
||||
// When core-js graduates a feature from `esnext` to `es`, the entries list both (e.g. `['es.regexp.escape', 'esnext.regexp.escape']`),
|
||||
// but `coreJsCompat` only includes the `es` version in its unavailable list, making the `esnext` version appear "available".
|
||||
// To avoid false positives, treat `esnext.*` features as unavailable when their `es.*` counterpart is already in the list.
|
||||
const checkFeatures = features => !features.every(feature =>
|
||||
unavailableFeatureSet.has(feature)
|
||||
|| (feature.startsWith('esnext.') && features.includes(feature.replace('esnext.', 'es.'))));
|
||||
|
||||
context.on('Literal', node => {
|
||||
if (
|
||||
!(
|
||||
(['ImportDeclaration', 'ImportExpression'].includes(node.parent.type) && node.parent.source === node)
|
||||
|| (isStaticRequire(node.parent) && node.parent.arguments[0] === node)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importedModule = node.value;
|
||||
if (typeof importedModule !== 'string' || ['.', '/'].includes(importedModule[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreJsModuleFeatures = coreJsEntries[importedModule.replace('core-js-pure', 'core-js')];
|
||||
|
||||
if (coreJsModuleFeatures) {
|
||||
if (coreJsModuleFeatures.length > 1) {
|
||||
if (checkFeatures(coreJsModuleFeatures)) {
|
||||
return {
|
||||
node,
|
||||
messageId: MESSAGE_ID_CORE_JS,
|
||||
data: {
|
||||
coreJsModule: importedModule,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (!unavailableFeatureSet.has(coreJsModuleFeatures[0])) {
|
||||
return {node, messageId: MESSAGE_ID_POLYFILL};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const polyfillCandidates = getPolyfillCandidates(importedModule.toLowerCase());
|
||||
if (!polyfillCandidates) {
|
||||
return;
|
||||
}
|
||||
|
||||
const polyfill = polyfillCandidates.find(({pattern}) => pattern.test(importedModule));
|
||||
if (polyfill) {
|
||||
const [, namespace, method = ''] = polyfill.feature.split('.');
|
||||
const features = coreJsEntries[`core-js/full/${namespace}${method && '/'}${method}`];
|
||||
|
||||
if (features && checkFeatures(features)) {
|
||||
return {node, messageId: MESSAGE_ID_POLYFILL};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['targets'],
|
||||
properties: {
|
||||
targets: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: 'A browserslist query string.',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
description: 'An array of browserslist query strings.',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
description: 'A browserslist targets object.',
|
||||
},
|
||||
],
|
||||
description: 'The target environments.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce the use of built-in methods instead of unnecessary polyfills.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
schema,
|
||||
// eslint-disable-next-line eslint-plugin/require-meta-default-options
|
||||
defaultOptions: [],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user