routie dev init since i didn't adhere to any proper guidance up until now

This commit is contained in:
2026-04-29 22:27:29 -06:00
commit e1dabb71e2
15301 changed files with 3562618 additions and 0 deletions
@@ -0,0 +1,53 @@
import {
isMemberExpression,
isEmptyArrayExpression,
isEmptyObjectExpression,
} from '../ast/index.js';
/**
@param {
{
object?: string,
method?: string,
methods?: string[],
}
} [options]
@returns {string}
*/
function isPrototypeProperty(node, options) {
const {
object,
property,
properties,
} = {
property: '',
properties: [],
...options,
};
if (!isMemberExpression(node, {
property,
properties,
optional: false,
})) {
return;
}
const objectNode = node.object;
return (
// `Object.prototype.method` or `Array.prototype.method`
isMemberExpression(objectNode, {
object,
property: 'prototype',
optional: false,
})
// `[].method`
|| (object === 'Array' && isEmptyArrayExpression(objectNode))
// `{}.method`
|| (object === 'Object' && isEmptyObjectExpression(objectNode))
);
}
export const isArrayPrototypeProperty = (node, options) => isPrototypeProperty(node, {...options, object: 'Array'});
export const isObjectPrototypeProperty = (node, options) => isPrototypeProperty(node, {...options, object: 'Object'});
@@ -0,0 +1,27 @@
const ISSUE_LINK_PREFIX = 'https://github.com/sindresorhus/eslint-plugin-unicorn/issues/new?';
export default function assertToken(token, {test, expected, ruleId}) {
if (test?.(token)) {
return;
}
expected = Array.isArray(expected) ? expected : [expected];
expected = expected.map(expectedToken => typeof expectedToken === 'string' ? {value: expectedToken} : expectedToken);
if (
!test
&& expected.some(expectedToken =>
Object.entries(expectedToken)
.every(([key, value]) => token[key] === value))
) {
return;
}
const actual = `'${JSON.stringify({value: token.value, type: token.type})}'`;
expected = expected.map(expectedToken => `'${JSON.stringify(expectedToken)}'`).join(' or ');
const title = `\`${ruleId}\`: Unexpected token ${actual}`;
const issueLink = `${ISSUE_LINK_PREFIX}title=${encodeURIComponent(title)}`;
const message = `Expected token ${expected}, got ${actual}.\nPlease open an issue at ${issueLink}.`;
throw new Error(message);
}
+97
View File
@@ -0,0 +1,97 @@
import isLogicalExpression from './is-logical-expression.js';
const isLogicNot = node => node?.type === 'UnaryExpression' && node.operator === '!';
const isLogicNotArgument = node => isLogicNot(node.parent) && node.parent.argument === node;
const isBooleanCallArgument = node => isBooleanCall(node.parent) && node.parent.arguments[0] === node;
const isBooleanCall = node =>
node?.type === 'CallExpression'
&& node.callee.type === 'Identifier'
&& node.callee.name === 'Boolean'
&& node.arguments.length === 1;
const isVueBooleanAttributeValue = node =>
node?.type === 'VExpressionContainer'
&& node.parent.type === 'VAttribute'
&& node.parent.directive
&& node.parent.value === node
&& node.parent.key.type === 'VDirectiveKey'
&& node.parent.key.name.type === 'VIdentifier'
&& (
node.parent.key.name.rawName === 'if'
|| node.parent.key.name.rawName === 'else-if'
|| node.parent.key.name.rawName === 'show'
);
const isDirectControlFlowTest = node =>
(
node.parent.type === 'IfStatement'
|| node.parent.type === 'ConditionalExpression'
|| node.parent.type === 'WhileStatement'
|| node.parent.type === 'DoWhileStatement'
|| node.parent.type === 'ForStatement'
)
&& node.parent.test === node;
const isDirectBooleanExpression = node =>
isLogicNot(node)
|| isLogicNotArgument(node)
|| isBooleanCall(node)
|| isBooleanCallArgument(node);
/**
Check if the expression value of `node` is a `boolean`.
@param {Node} node
@returns {boolean}
*/
export function isBooleanExpression(node) {
if (isDirectBooleanExpression(node)) {
return true;
}
if (isLogicalExpression(node.parent)) {
return isBooleanExpression(node.parent);
}
return false;
}
/**
Check if `node` is used as a control-flow test.
@param {Node} node
@returns {boolean}
*/
export function isControlFlowTest(node) {
if (isVueBooleanAttributeValue(node.parent) || isDirectControlFlowTest(node)) {
return true;
}
if (isLogicalExpression(node.parent)) {
return isControlFlowTest(node.parent);
}
return false;
}
/**
Get the boolean type-casting ancestor.
@typedef {{ node: Node, isNegative: boolean }} Result
@param {Node} node
@returns {Result}
*/
export function getBooleanAncestor(node) {
let isNegative = false;
while (true) {
if (isLogicNotArgument(node)) {
isNegative = !isNegative;
node = node.parent;
} else if (isBooleanCallArgument(node)) {
node = node.parent;
} else {
break;
}
}
return {node, isNegative};
}
+30
View File
@@ -0,0 +1,30 @@
import typedArray from '../shared/typed-array.js';
export const enforceNew = [
'Object',
'Array',
'ArrayBuffer',
'DataView',
'Date',
'Error',
'Function',
'Map',
'WeakMap',
'Set',
'WeakSet',
'Promise',
'RegExp',
'SharedArrayBuffer',
'Proxy',
'WeakRef',
'FinalizationRegistry',
...typedArray,
];
export const disallowNew = [
'BigInt',
'Boolean',
'Number',
'String',
'Symbol',
];
@@ -0,0 +1,22 @@
export default function cartesianProductSamples(combinations, length = Number.POSITIVE_INFINITY) {
const total = combinations.reduce((total, {length}) => total * length, 1);
const samples = Array.from({length: Math.min(total, length)}, (_, sampleIndex) => {
let indexRemaining = sampleIndex;
const combination = [];
for (let combinationIndex = combinations.length - 1; combinationIndex >= 0; combinationIndex--) {
const items = combinations[combinationIndex];
const {length} = items;
const index = indexRemaining % length;
indexRemaining = (indexRemaining - index) / length;
combination.unshift(items[index]);
}
return combination;
});
return {
total,
samples,
};
}
@@ -0,0 +1,34 @@
import packageJson from '../../package.json' with {type: 'json'};
import getDocumentationUrl from './get-documentation-url.js';
const repoUrl = 'https://github.com/sindresorhus/eslint-plugin-unicorn';
/** @returns {{ [ruleName: string]: import('eslint').Rule.RuleModule }} */
export default function createDeprecatedRules(rules) {
return Object.fromEntries(Object.entries(rules).map(([ruleId, deprecatedInfo]) => {
const url = `${repoUrl}/blob/v${packageJson.version}/docs/deleted-and-deprecated-rules.md#${ruleId}`;
return [
ruleId,
{
// eslint-disable-next-line internal/prefer-context-on
create: () => ({}),
meta: {
docs: {
description: deprecatedInfo.message,
url,
},
deprecated: {
message: deprecatedInfo.message,
url,
replacedBy: deprecatedInfo.replacedBy.map(replacementRuleId => ({
rule: {
name: replacementRuleId,
url: getDocumentationUrl(replacementRuleId),
},
})),
},
},
},
];
}));
}
@@ -0,0 +1,24 @@
import jsesc from 'jsesc';
/**
Escape string and wrap the result in quotes.
@param {string} string - The string to be quoted.
@param {string} [quote] - The quote character.
@returns {string} - The quoted and escaped string.
*/
export default function escapeString(string, quote = '\'') {
/* c8 ignore start */
if (typeof string !== 'string') {
throw new TypeError('Unexpected string.');
}
/* c8 ignore end */
return jsesc(string, {
quotes: quote === '"' ? 'double' : 'single',
wrap: true,
es6: true,
minimal: true,
lowercaseHex: false,
});
}
@@ -0,0 +1,6 @@
const escapeTemplateElementRaw = string => string.replaceAll(
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g,
String.raw`\$<symbol>`,
);
export default escapeTemplateElementRaw;
@@ -0,0 +1,21 @@
// https://github.com/eslint/eslint/blob/df5566f826d9f5740546e473aa6876b1f7d2f12c/lib/languages/js/source-code/source-code.js#L914-L917
const ESLINT_DISABLE_DIRECTIVE_TYPES = new Set([
'disable',
'disable-next-line',
'disable-line',
]);
function getEslintDisableDirectives(context) {
const {directives} = context.sourceCode.getDisableDirectives();
return directives.filter(({type}) => ESLINT_DISABLE_DIRECTIVE_TYPES.has(type));
}
function isEslintDisableOrEnableDirective(context, comment) {
const {directives} = context.sourceCode.getDisableDirectives();
return directives.some(directive => directive.node === comment);
}
export {
getEslintDisableDirectives,
isEslintDisableOrEnableDirective,
};
@@ -0,0 +1,15 @@
// TODO: Support more types
function getPredicate(options) {
if (typeof options === 'string') {
return node => node.type === options;
}
}
export default function getAncestor(node, options) {
const predicate = getPredicate(options);
for (; node.parent; node = node.parent) {
if (predicate(node)) {
return node;
}
}
}
@@ -0,0 +1,147 @@
import helperValidatorIdentifier from '@babel/helper-validator-identifier';
import resolveVariableName from './resolve-variable-name.js';
import getReferences from './get-references.js';
const {
isIdentifierName,
isStrictReservedWord,
isKeyword,
} = helperValidatorIdentifier;
// https://github.com/microsoft/TypeScript/issues/2536#issuecomment-87194347
const typescriptReservedWords = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'as',
'implements',
'interface',
'let',
'package',
'private',
'protected',
'public',
'static',
'yield',
'any',
'boolean',
'constructor',
'declare',
'get',
'module',
'require',
'number',
'set',
'string',
'symbol',
'type',
'from',
'of',
]);
// Copied from https://github.com/babel/babel/blob/fce35af69101c6b316557e28abf60bdbf77d6a36/packages/babel-types/src/validators/isValidIdentifier.ts#L7
// Use this function instead of `require('@babel/types').isIdentifier`, since `@babel/helper-validator-identifier` package is much smaller
const isValidIdentifier = name =>
typeof name === 'string'
&& !isKeyword(name)
&& !isStrictReservedWord(name, true)
&& isIdentifierName(name)
&& name !== 'arguments'
&& !typescriptReservedWords.has(name);
/*
Unresolved reference is probably from the global scope. We should avoid using that name.
For example, like `foo` and `bar` below.
```
function unicorn() {
return foo;
}
function unicorn() {
return function() {
return bar;
};
}
```
*/
const isUnresolvedName = (name, scope) =>
getReferences(scope).some(({identifier, resolved}) => identifier?.name === name && !resolved);
const isSafeName = (name, scopes) =>
!scopes.some(scope => resolveVariableName(name, scope) || isUnresolvedName(name, scope));
const alwaysTrue = () => true;
/**
Rule-specific name check function.
@callback isSafe
@param {string} name - The generated candidate name.
@param {Scope[]} scopes - The same list of scopes you pass to `getAvailableVariableName`.
@returns {boolean} - `true` if the `name` is ok.
*/
/**
Generates a unique name prefixed with `name` such that:
- it is not defined in any of the `scopes`,
- it is not a reserved word,
- it is not `arguments` in strict scopes (where `arguments` is not allowed),
- it does not collide with the actual `arguments` (which is always defined in function scopes).
Useful when you want to rename a variable (or create a new variable) while being sure not to shadow any other variables in the code.
@param {string} name - The desired name for a new variable.
@param {Scope[]} scopes - The list of scopes the new variable will be referenced in.
@param {isSafe} [isSafe] - Rule-specific name check function.
@returns {string} - Either `name` as is, or a string like `${name}_` suffixed with underscores to make the name unique.
*/
export default function getAvailableVariableName(name, scopes, isSafe = alwaysTrue) {
if (!isValidIdentifier(name)) {
name += '_';
if (!isValidIdentifier(name)) {
return;
}
}
while (!isSafeName(name, scopes) || !isSafe(name, scopes)) {
name += '_';
}
return name;
}
@@ -0,0 +1,5 @@
import {builtinRules} from 'eslint/use-at-your-own-risk';
export default function getBuiltinRule(id) {
return builtinRules.get(id);
}
@@ -0,0 +1,34 @@
import {getCallExpressionTokens} from './get-call-or-new-expression-tokens.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
Get the text of the arguments list of `CallExpression`.
@param {ESTree.CallExpression} callExpression - The `CallExpression` node.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {boolean} [includeTrailingComma = true] - Whether the trailing comma should be included.
@returns {string}
*/
export default function getCallExpressionArgumentsText(
context,
callExpression,
includeTrailingComma = true,
) {
const {sourceCode} = context;
const {
openingParenthesisToken,
closingParenthesisToken,
trailingCommaToken,
} = getCallExpressionTokens(callExpression, context);
const [, start] = sourceCode.getRange(openingParenthesisToken);
const [end] = sourceCode.getRange(includeTrailingComma
? closingParenthesisToken
: (trailingCommaToken ?? closingParenthesisToken));
return sourceCode.text.slice(start, end);
}
@@ -0,0 +1,67 @@
import {isOpeningParenToken, isCommaToken} from '@eslint-community/eslint-utils';
import isNewExpressionWithParentheses from './is-new-expression-with-parentheses.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@typedef {{
openingParenthesisToken: ESLint.AST.Token,
closingParenthesisToken: ESLint.AST.Token,
trailingCommaToken: ESLint.AST.Token | undefined,
}} Tokens
*/
/**
Get the `openingParenthesisToken`, `closingParenthesisToken`, and `trailingCommaToken` of `CallExpression`.
@param {ESTree.CallExpression | ESTree.NewExpression} callOrNewExpression - The `CallExpression` or `newExpression` node.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {Tokens}
*/
function getCallOrNewExpressionTokens(callOrNewExpression, context) {
const {sourceCode} = context;
const startToken = callOrNewExpression.typeArguments ?? callOrNewExpression.callee;
const openingParenthesisToken = sourceCode.getTokenAfter(startToken, isOpeningParenToken);
const [
penultimateToken,
closingParenthesisToken,
] = sourceCode.getLastTokens(callOrNewExpression, 2);
const trailingCommaToken = isCommaToken(penultimateToken) ? penultimateToken : undefined;
return {
openingParenthesisToken,
closingParenthesisToken,
trailingCommaToken,
};
}
/**
Get the `openingParenthesisToken`, `closingParenthesisToken`, and `trailingCommaToken` of `NewExpression`.
@param {ESTree.NewExpression} newExpression - The `newExpression` node.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {Tokens}
*/
function getNewExpressionTokens(newExpression, context) {
if (!isNewExpressionWithParentheses(newExpression, context)) {
return {};
}
return getCallOrNewExpressionTokens(newExpression, context);
}
/**
Get the `openingParenthesisToken`, `closingParenthesisToken`, and `trailingCommaToken` of `CallExpression`.
@param {ESTree.CallExpression} callExpression - The `callExpression` node.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {Tokens}
*/
function getCallExpressionTokens(callExpression, context) {
return getCallOrNewExpressionTokens(callExpression, context);
}
export {getCallExpressionTokens, getNewExpressionTokens};
@@ -0,0 +1,22 @@
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
Get the location of the given class node for reporting.
@param {ESTree.ClassDeclaration | ESTree.ClassExpression} node - The class node to get.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESTree.SourceLocation} The location of the class node for reporting.
*/
export default function getClassHeadLocation(node, context) {
const {sourceCode} = context;
const {body} = node;
const tokenBeforeBody = sourceCode.getTokenBefore(body);
const {start} = sourceCode.getLoc(node);
const {end} = sourceCode.getLoc(tokenBeforeBody);
return {start, end};
}
@@ -0,0 +1,9 @@
import path from 'node:path';
import packageJson from '../../package.json' with {type: 'json'};
const repoUrl = 'https://github.com/sindresorhus/eslint-plugin-unicorn';
export default function getDocumentationUrl(filename) {
const ruleName = path.basename(filename, '.js');
return `${repoUrl}/blob/v${packageJson.version}/docs/rules/${ruleName}.md`;
}
@@ -0,0 +1,18 @@
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.Node} node - The class node to get.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {string}.
*/
export default function getIndentString(node, context) {
const {sourceCode} = context;
const {start: {line, column}} = sourceCode.getLoc(node);
const lines = sourceCode.getLines();
const before = lines[line - 1].slice(0, column);
return before.match(/\s*$/)[0];
}
@@ -0,0 +1,5 @@
import getScopes from './get-scopes.js';
const getReferences = scope => [...new Set(getScopes(scope).flatMap(({references}) => references))];
export default getReferences;
+12
View File
@@ -0,0 +1,12 @@
/**
Gather a list of all Scopes starting recursively from the input Scope.
@param {Scope} scope - The Scope to start checking from.
@returns {Scope[]} - The resulting Scopes.
*/
const getScopes = scope => [
scope,
...scope.childScopes.flatMap(scope => getScopes(scope)),
];
export default getScopes;
@@ -0,0 +1,38 @@
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {1 | -1} offset
*/
function getSiblingNode(node, context, offset) {
const {parent} = node;
const visitorKeys = context.sourceCode.visitorKeys[parent.type] || Object.keys(parent);
for (const property of visitorKeys) {
const value = parent[property];
if (value !== node && Array.isArray(value)) {
const index = value.indexOf(node);
if (index !== -1) {
return value[index + offset];
}
}
}
}
/**
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
*/
export const getPreviousNode = (node, context) => getSiblingNode(node, context, -1);
/**
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
*/
export const getNextNode = (node, context) => getSiblingNode(node, context, 1);
@@ -0,0 +1,22 @@
import {isColonToken} from '@eslint-community/eslint-utils';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@typedef {line: number, column: number} Position
Get the location of the given `SwitchCase` node for reporting.
@param {ESTree.SwitchCase} node - The `SwitchCase` node to get.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {{start: Position, end: Position}} The location of the class node for reporting.
*/
export default function getSwitchCaseHeadLocation(node, context) {
const {sourceCode} = context;
const startToken = node.test || sourceCode.getFirstToken(node);
const colonToken = sourceCode.getTokenAfter(startToken, isColonToken);
return {start: sourceCode.getLoc(node).start, end: sourceCode.getLoc(colonToken).end};
}
@@ -0,0 +1,24 @@
// When parsing with `vue-eslint-parser`, we need to use `getTemplateBodyTokenStore()` to get a token inside a `<template>`, and `sourceCode` to get a token inside a `<script>`.
// https://github.com/sindresorhus/eslint-plugin-unicorn/pull/2704/files#r2209196626
/**
@param {import('eslint').Rule.RuleContext} context
@param {import('estree').Node} node
@returns {import('eslint').SourceCode}
*/
function getTokenStore(context, node) {
const {sourceCode} = context;
if (
sourceCode.parserServices.getTemplateBodyTokenStore
&& sourceCode.ast.templateBody
&& sourceCode.getRange(sourceCode.ast.templateBody)[0] <= sourceCode.getRange(node)[0]
&& sourceCode.getRange(node)[1] <= sourceCode.getRange(sourceCode.ast.templateBody)[1]
) {
return sourceCode.parserServices.getTemplateBodyTokenStore();
}
return sourceCode;
}
export default getTokenStore;
@@ -0,0 +1,7 @@
// Get identifiers of given variable
const getVariableIdentifiers = ({identifiers, references}) => [...new Set([
...identifiers,
...references.map(({identifier}) => identifier),
])];
export default getVariableIdentifiers;
@@ -0,0 +1,68 @@
import {ReferenceTracker} from '@eslint-community/eslint-utils';
const createTraceMap = (object, type) => {
let map = {[type]: true};
const path = object.split('.').toReversed();
for (const name of path) {
map = {[name]: map};
}
return map;
};
export class GlobalReferenceTracker {
#traceMap = {};
#context;
#filter;
#handle;
constructor({
object,
objects = [object],
type = ReferenceTracker.READ,
context,
filter,
handle,
}) {
for (const object of objects) {
Object.assign(this.#traceMap, createTraceMap(object, type));
}
this.#context = context;
this.#filter = filter;
this.#handle = handle;
}
* #track(globalScope, options) {
const context = options?.context ?? this.#context;
const filter = options?.filter ?? this.#filter;
const handle = options?.handle ?? this.#handle;
const tracker = new ReferenceTracker(globalScope);
for (const reference of tracker.iterateGlobalReferences(this.#traceMap)) {
if (filter && !filter(reference)) {
continue;
}
const problems = handle(reference, context);
yield problems;
}
}
listen(options) {
const context = options?.context ?? this.#context;
context.onExit(
'Program',
program => this.#track(context.sourceCode.getScope(program), options),
);
}
}
Object.assign(GlobalReferenceTracker, {
READ: ReferenceTracker.READ,
CALL: ReferenceTracker.CALL,
CONSTRUCT: ReferenceTracker.CONSTRUCT,
});
@@ -0,0 +1,17 @@
const isChainElement = node => node.type === 'MemberExpression' || node.type === 'CallExpression';
export default function hasOptionalChainElement(node) {
if (!isChainElement(node)) {
return false;
}
if (node.optional) {
return true;
}
if (node.type === 'MemberExpression') {
return hasOptionalChainElement(node.object);
}
return false;
}
@@ -0,0 +1,7 @@
const hasSameRange = (node1, node2, context) =>
node1
&& node2
&& context.sourceCode.getRange(node1)[0] === context.sourceCode.getRange(node2)[0]
&& context.sourceCode.getRange(node1)[1] === context.sourceCode.getRange(node2)[1];
export default hasSameRange;
+73
View File
@@ -0,0 +1,73 @@
export {
isParenthesized,
getParentheses,
getParenthesizedRange,
getParenthesizedText,
} from './parentheses/parentheses.js';
export {
isArrayPrototypeProperty,
isObjectPrototypeProperty,
} from './array-or-object-prototype-property.js';
export {
isNodeMatches,
isNodeMatchesNameOrPath,
} from './is-node-matches.js';
export {
isBooleanExpression,
isControlFlowTest,
getBooleanAncestor,
} from './boolean.js';
export {default as assertToken} from './assert-token.js';
export {default as cartesianProductSamples} from './cartesian-product-samples.js';
export {default as escapeString} from './escape-string.js';
export {default as getClassHeadLocation} from './get-class-head-location.js';
export {default as getAvailableVariableName} from './get-available-variable-name.js';
export {default as getCallExpressionArgumentsText} from './get-call-expression-arguments-text.js';
export {getCallExpressionTokens, getNewExpressionTokens} from './get-call-or-new-expression-tokens.js';
export {default as getIndentString} from './get-indent-string.js';
export {default as getReferences} from './get-references.js';
export {default as getScopes} from './get-scopes.js';
export {default as getTokenStore} from './get-token-store.js';
export {default as getVariableIdentifiers} from './get-variable-identifiers.js';
export {default as hasOptionalChainElement} from './has-optional-chain-element.js';
export {default as isFunctionSelfUsedInside} from './is-function-self-used-inside.js';
export {default as isLeftHandSide} from './is-left-hand-side.js';
export {default as isLogicalExpression} from './is-logical-expression.js';
export {default as isMethodNamed} from './is-method-named.js';
export {default as isNewExpressionWithParentheses} from './is-new-expression-with-parentheses.js';
export {default as isNumber} from './is-number.js';
export {default as isNodeValueNotDomNode} from './is-node-value-not-dom-node.js';
export {default as isNodeValueNotFunction} from './is-node-value-not-function.js';
export {default as isNodeContainsLexicalThis} from './is-node-contains-lexical-this.js';
export {default as isOnSameLine} from './is-on-same-line.js';
export {default as isSameIdentifier} from './is-same-identifier.js';
export {default as isSameReference} from './is-same-reference.js';
export {default as isUnresolvedVariable} from './is-unresolved-variable.js';
export {default as isShorthandImportLocal} from './is-shorthand-import-local.js';
export {default as isShorthandPropertyValue} from './is-shorthand-property-value.js';
export {default as isValueNotUsable} from './is-value-not-usable.js';
export {default as needsSemicolon} from './needs-semicolon.js';
export {
getEslintDisableDirectives,
isEslintDisableOrEnableDirective,
} from './eslint-directive.js';
export {checkVueTemplate} from './rule.js';
export {default as shouldAddParenthesesToAwaitExpressionArgument} from './should-add-parentheses-to-await-expression-argument.js';
export {default as shouldAddParenthesesToCallExpressionCallee} from './should-add-parentheses-to-call-expression-callee.js';
export {default as shouldAddParenthesesToConditionalExpressionChild} from './should-add-parentheses-to-conditional-expression-child.js';
export {default as shouldAddParenthesesToMemberExpressionObject} from './should-add-parentheses-to-member-expression-object.js';
export {default as shouldAddParenthesesToUnaryExpressionArgument} from './should-add-parentheses-to-unary-expression.js';
export {default as shouldAddParenthesesToNewExpressionCallee} from './should-add-parentheses-to-new-expression-callee.js';
export {default as shouldAddParenthesesToExpressionStatementExpression} from './should-add-parentheses-to-expression-statement-expression.js';
export {default as shouldAddParenthesesToLogicalExpressionChild} from './should-add-parentheses-to-logical-expression-child.js';
export {default as singular} from './singular.js';
export {default as toLocation} from './to-location.js';
export {default as getAncestor} from './get-ancestor.js';
export {getPreviousNode, getNextNode} from './get-sibling-node.js';
export * from './string-cases.js';
export * from './numeric.js';
export {default as getBuiltinRule} from './get-builtin-rule.js';
@@ -0,0 +1,39 @@
import {findVariable} from '@eslint-community/eslint-utils';
const getReferences = (scope, nodeOrName) => {
const {references = []} = findVariable(scope, nodeOrName) || {};
return references;
};
/**
Check if `this`, `arguments`, or the function name is used inside of itself.
@param {Node} functionNode - The function node.
@param {Scope} functionScope - The scope of the function node.
@returns {boolean}
*/
export default function isFunctionSelfUsedInside(functionNode, functionScope) {
/* c8 ignore next 3 */
if (functionScope.block !== functionNode) {
throw new Error('"functionScope" should be the scope of "functionNode".');
}
const {type, id} = functionNode;
if (type === 'ArrowFunctionExpression') {
return false;
}
if (functionScope.thisFound) {
return true;
}
if (getReferences(functionScope, 'arguments').some(({from}) => from === functionScope)) {
return true;
}
if (id && getReferences(functionScope, id).length > 0) {
return true;
}
return false;
}
@@ -0,0 +1,20 @@
const isLeftHandSide = node =>
(
(node.parent.type === 'AssignmentExpression' || node.parent.type === 'AssignmentPattern')
&& node.parent.left === node
)
|| (node.parent.type === 'UpdateExpression' && node.parent.argument === node)
|| (node.parent.type === 'ArrayPattern' && node.parent.elements.includes(node))
|| (
node.parent.type === 'Property'
&& node.parent.value === node
&& node.parent.parent.type === 'ObjectPattern'
&& node.parent.parent.properties.includes(node.parent)
)
|| (
node.parent.type === 'UnaryExpression'
&& node.parent.operator === 'delete'
&& node.parent.argument === node
);
export default isLeftHandSide;
@@ -0,0 +1,14 @@
/**
Check if the given node is a true logical expression or not.
The three binary expressions logical-or (`||`), logical-and (`&&`), and coalesce (`??`) are known as `ShortCircuitExpression`, but ESTree represents these by the `LogicalExpression` node type. This function rejects coalesce expressions of `LogicalExpression` node type.
@param {Node} node - The node to check.
@returns {boolean} `true` if the node is `&&` or `||`.
@see https://tc39.es/ecma262/#prod-ShortCircuitExpression
*/
const isLogicalExpression = node =>
node?.type === 'LogicalExpression'
&& (node.operator === '&&' || node.operator === '||');
export default isLogicalExpression;
@@ -0,0 +1,7 @@
const isMethodNamed = (node, name) =>
node.type === 'CallExpression'
&& node.callee.type === 'MemberExpression'
&& node.callee.property.type === 'Identifier'
&& node.callee.property.name === name;
export default isMethodNamed;
@@ -0,0 +1,28 @@
import {isOpeningParenToken, isClosingParenToken} from '@eslint-community/eslint-utils';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
Determine if a constructor function is newed-up with parens.
@param {ESTree.NewExpression} node - The `NewExpression` node to be checked.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {boolean} True if the constructor is called with parens.
Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/no-extra-parens.js#L252
*/
export default function isNewExpressionWithParentheses(node, context) {
if (node.arguments.length > 0) {
return true;
}
const {sourceCode} = context;
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2);
// The expression should end with its own parens, for example, `new new Foo()` is not a new expression with parens.
return isOpeningParenToken(penultimateToken)
&& isClosingParenToken(lastToken)
&& sourceCode.getRange(node.callee)[1] < sourceCode.getRange(node)[1];
}
@@ -0,0 +1,65 @@
/**
Check whether a node subtree contains lexical `this`.
This is parser-agnostic and intentionally does not use `scope.thisFound`, because that flag is inconsistent across supported parsers.
*/
const isNodeContainsLexicalThis = (node, visitorKeys) => {
if (node.type === 'ThisExpression') {
return true;
}
if (
node.type === 'FunctionDeclaration'
|| node.type === 'FunctionExpression'
) {
// `this` inside non-arrow functions is rebound and does not affect outer arrows.
return false;
}
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
// Class bodies create their own `this`, but computed keys/superclass are evaluated in outer scope.
if (node.superClass && isNodeContainsLexicalThis(node.superClass, visitorKeys)) {
return true;
}
for (const classElement of node.body.body) {
if (classElement.computed && isNodeContainsLexicalThis(classElement.key, visitorKeys)) {
return true;
}
}
return false;
}
const keys = visitorKeys[node.type];
if (!keys) {
return false;
}
for (const key of keys) {
const value = node[key];
if (!value) {
continue;
}
if (Array.isArray(value)) {
for (const childNode of value) {
if (childNode && isNodeContainsLexicalThis(childNode, visitorKeys)) {
return true;
}
}
continue;
}
if (isNodeContainsLexicalThis(value, visitorKeys)) {
return true;
}
}
return false;
};
export default isNodeContainsLexicalThis;
@@ -0,0 +1,58 @@
/**
Check if node matches object name or key path.
@param {Node} node - The AST node to check.
@param {string} nameOrPath - The object name or key path.
@returns {boolean}
*/
export function isNodeMatchesNameOrPath(node, nameOrPath) {
const names = nameOrPath.trim().split('.');
for (let index = names.length - 1; index >= 0; index--) {
const name = names[index];
if (!name) {
return false;
}
if (index === 0) {
return (
(node.type === 'Identifier' && node.name === name)
|| (name === 'this' && node.type === 'ThisExpression')
|| (name === 'super' && node.type === 'Super')
);
}
if (
index === 1
&& node.type === 'MetaProperty'
&& node.property.type === 'Identifier'
&& node.property.name === name
) {
node = node.meta;
continue;
}
if (
node.type === 'MemberExpression'
&& !node.optional
&& !node.computed
&& node.property.type === 'Identifier'
&& node.property.name === name
) {
node = node.object;
continue;
}
return false;
}
}
/**
Check if node matches any object name or key path.
@param {Node} node - The AST node to check.
@param {string[]} nameOrPaths - The object name or key paths.
@returns {boolean}
*/
export function isNodeMatches(node, nameOrPaths) {
return nameOrPaths.some(nameOrPath => isNodeMatchesNameOrPath(node, nameOrPath));
}
@@ -0,0 +1,20 @@
import {isUndefined} from '../ast/index.js';
// AST Types:
// https://github.com/eslint/espree/blob/master/lib/ast-node-types.js#L18
// Only types possible to be `callee` or `argument` are listed
const impossibleNodeTypes = new Set([
'ArrayExpression',
'ArrowFunctionExpression',
'ClassExpression',
'FunctionExpression',
'Literal',
'ObjectExpression',
'TemplateLiteral',
]);
const isNodeValueNotDomNode = node =>
impossibleNodeTypes.has(node.type)
|| isUndefined(node);
export default isNodeValueNotDomNode;
@@ -0,0 +1,40 @@
import {isUndefined, isCallExpression, isMethodCall} from '../ast/index.js';
// AST Types:
// https://github.com/eslint/espree/blob/master/lib/ast-node-types.js#L18
// Only types possible to be `argument` are listed
const impossibleNodeTypes = new Set([
'ArrayExpression',
'BinaryExpression',
'ClassExpression',
'Literal',
'ObjectExpression',
'TemplateLiteral',
'UnaryExpression',
'UpdateExpression',
]);
// Technically these nodes could be a function, but most likely not
const mostLikelyNotNodeTypes = new Set([
'AssignmentExpression',
'AwaitExpression',
'NewExpression',
'TaggedTemplateExpression',
'ThisExpression',
]);
const isNodeValueNotFunction = node => (
impossibleNodeTypes.has(node.type)
|| mostLikelyNotNodeTypes.has(node.type)
|| isUndefined(node)
|| (
isCallExpression(node)
&& !(isMethodCall(node, {
method: 'bind',
optionalCall: false,
optionalMember: false,
}))
)
);
export default isNodeValueNotFunction;
+224
View File
@@ -0,0 +1,224 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {isNumericLiteral} from '../ast/index.js';
const isStaticProperties = (node, object, properties) =>
node.type === 'MemberExpression'
&& !node.computed
&& !node.optional
&& node.object.type === 'Identifier'
&& node.object.name === object
&& node.property.type === 'Identifier'
&& properties.has(node.property.name);
const isFunctionCall = (node, functionName) => node.type === 'CallExpression'
&& !node.optional
&& node.callee.type === 'Identifier'
&& node.callee.name === functionName;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#static_properties
const mathProperties = new Set([
'E',
'LN2',
'LN10',
'LOG2E',
'LOG10E',
'PI',
'SQRT1_2',
'SQRT2',
]);
// `Math.{E,LN2,LN10,LOG2E,LOG10E,PI,SQRT1_2,SQRT2}`
const isMathProperty = node => isStaticProperties(node, 'Math', mathProperties);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#static_methods
const mathMethods = new Set([
'abs',
'acos',
'acosh',
'asin',
'asinh',
'atan',
'atanh',
'atan2',
'cbrt',
'ceil',
'clz32',
'cos',
'cosh',
'exp',
'expm1',
'floor',
'fround',
'hypot',
'imul',
'log',
'log1p',
'log10',
'log2',
'max',
'min',
'pow',
'random',
'round',
'sign',
'sin',
'sinh',
'sqrt',
'tan',
'tanh',
'trunc',
]);
// `Math.{abs, …, trunc}(…)`
const isMathMethodCall = node =>
node.type === 'CallExpression'
&& !node.optional
&& isStaticProperties(node.callee, 'Math', mathMethods);
// `Number(…)`
const isNumberCall = node => isFunctionCall(node, 'Number');
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#static_properties
const numberProperties = new Set([
'EPSILON',
'MAX_SAFE_INTEGER',
'MAX_VALUE',
'MIN_SAFE_INTEGER',
'MIN_VALUE',
'NaN',
'NEGATIVE_INFINITY',
'POSITIVE_INFINITY',
]);
const isNumberProperty = node => isStaticProperties(node, 'Number', numberProperties);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#static_methods
const numberMethods = new Set([
'parseFloat',
'parseInt',
]);
const isNumberMethodCall = node =>
node.type === 'CallExpression'
&& !node.optional
&& isStaticProperties(node.callee, 'Number', numberMethods);
const isGlobalParseToNumberFunctionCall = node => isFunctionCall(node, 'parseInt') || isFunctionCall(node, 'parseFloat');
const isStaticNumber = (node, scope) =>
typeof getStaticValue(node, scope)?.value === 'number';
const isLengthProperty = node =>
node.type === 'MemberExpression'
&& !node.computed
&& !node.optional
&& node.property.type === 'Identifier'
&& node.property.name === 'length';
// `+` and `>>>` operators are handled separately
const mathOperators = new Set(['-', '*', '/', '%', '**', '<<', '>>', '|', '^', '&']);
// eslint-disable-next-line complexity
export default function isNumber(node, scope) {
if (
isNumericLiteral(node)
|| isMathProperty(node)
|| isMathMethodCall(node)
|| isNumberCall(node)
|| isNumberProperty(node)
|| isNumberMethodCall(node)
|| isGlobalParseToNumberFunctionCall(node)
|| isLengthProperty(node)
) {
return true;
}
switch (node.type) {
case 'AssignmentExpression': {
const {operator} = node;
if (operator === '=' && isNumber(node.right, scope)) {
return true;
}
// Fall through
}
case 'BinaryExpression': {
let {operator} = node;
if (node.type === 'AssignmentExpression') {
operator = operator.slice(0, -1);
}
if (operator === '+' && isNumber(node.left, scope) && isNumber(node.right, scope)) {
return true;
}
// `>>>` (zero-fill right shift) can't use on `BigInt`
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#operators
if (operator === '>>>') {
return true;
}
// `a * b` can be `BigInt`, we need make sure at least one side is number
if (mathOperators.has(operator) && (isNumber(node.left, scope) || isNumber(node.right, scope))) {
return true;
}
break;
}
case 'UnaryExpression': {
const {operator} = node;
// `+` can't use on `BigInt`
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#operators
if (operator === '+') {
return true;
}
if ((operator === '-' || operator === '~') && isNumber(node.argument, scope)) {
return true;
}
break;
}
case 'UpdateExpression': {
if (isNumber(node.argument, scope)) {
return true;
}
break;
}
case 'ConditionalExpression': {
const isConsequentNumber = isNumber(node.consequent, scope);
const isAlternateNumber = isNumber(node.alternate, scope);
if (isConsequentNumber && isAlternateNumber) {
return true;
}
const testStaticValueResult = getStaticValue(node.test, scope);
if (
testStaticValueResult !== null
&& (
(isConsequentNumber && testStaticValueResult.value)
|| (isAlternateNumber && !testStaticValueResult.value)
)
) {
return true;
}
break;
}
case 'SequenceExpression': {
if (isNumber(node.expressions.at(-1), scope)) {
return true;
}
break;
}
// No default
}
return isStaticNumber(node, scope);
}
@@ -0,0 +1,10 @@
export default function isObjectMethod(node, object, method) {
const {callee} = node;
return (
callee.type === 'MemberExpression'
&& callee.object.type === 'Identifier'
&& callee.object.name === object
&& callee.property.type === 'Identifier'
&& callee.property.name === method
);
}
@@ -0,0 +1,7 @@
export default function isOnSameLine(nodeOrTokenA, nodeOrTokenB, context) {
const [
lineA,
lineB,
] = [nodeOrTokenA, nodeOrTokenB].map(nodeOrToken => context.sourceCode.getLoc(nodeOrToken).start.line);
return lineA === lineB;
}
@@ -0,0 +1,5 @@
const isSameIdentifier = (nodeA, nodeB) => nodeA.type === 'Identifier'
&& nodeB.type === 'Identifier'
&& nodeA.name === nodeB.name;
export default isSameIdentifier;
@@ -0,0 +1,176 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
// Copied from https://github.com/eslint/eslint/blob/94ba68d76a6940f68ff82eea7332c6505f93df76/lib/rules/utils/ast-utils.js#L392
/**
Gets the property name of a given node.
The node can be a MemberExpression, a Property, or a MethodDefinition.
If the name is dynamic, this returns `null`.
For examples:
a.b // => "b"
a["b"] // => "b"
a['b'] // => "b"
a[`b`] // => "b"
a[100] // => "100"
a[b] // => null
a["a" + "b"] // => null
a[tag`b`] // => null
a[`${b}`] // => null
let a = {b: 1} // => "b"
let a = {["b"]: 1} // => "b"
let a = {['b']: 1} // => "b"
let a = {[`b`]: 1} // => "b"
let a = {[100]: 1} // => "100"
let a = {[b]: 1} // => null
let a = {["a" + "b"]: 1} // => null
let a = {[tag`b`]: 1} // => null
let a = {[`${b}`]: 1} // => null
@param {ASTNode} node The node to get.
@returns {string|undefined} The property name if static. Otherwise, undefined.
*/
function getStaticPropertyName(node) {
let property;
switch (node?.type) {
case 'MemberExpression': {
property = node.property;
break;
}
/* c8 ignore next 2 */
case 'ChainExpression': {
return getStaticPropertyName(node.expression);
}
// Only reachable when use this to get class/object member key
/* c8 ignore next */
case 'Property':
case 'MethodDefinition': {
/* c8 ignore next 2 */
property = node.key;
break;
}
// No default
}
if (property) {
if (property.type === 'Identifier' && !node.computed) {
return property.name;
}
const staticResult = getStaticValue(property);
if (!staticResult) {
return;
}
return String(staticResult.value);
}
}
/**
Check if two literal nodes are the same value.
@param {ASTNode} left The Literal node to compare.
@param {ASTNode} right The other Literal node to compare.
@returns {boolean} `true` if the two literal nodes are the same value.
*/
function equalLiteralValue(left, right) {
// RegExp literal.
if (left.regex || right.regex) {
return Boolean(left.regex
&& right.regex
&& left.regex.pattern === right.regex.pattern
&& left.regex.flags === right.regex.flags);
}
// BigInt literal.
if (left.bigint || right.bigint) {
return left.bigint === right.bigint;
}
return left.value === right.value;
}
/**
Unwrap ChainExpression (`?.`) and TypeScript type assertion (`as`, `<Type>`, or `!`) nodes.
@param {ASTNode} node The node to unwrap.
@returns {ASTNode} The unwrapped node.
*/
function unwrapNode(node) {
if (
node.type === 'ChainExpression'
|| node.type === 'TSAsExpression'
|| node.type === 'TSTypeAssertion'
|| node.type === 'TSNonNullExpression'
) {
return unwrapNode(node.expression);
}
return node;
}
/**
Check if two expressions reference the same value. For example:
a = a
a.b = a.b
a[0] = a[0]
a['b'] = a['b']
@param {ASTNode} left The left side of the comparison.
@param {ASTNode} right The right side of the comparison.
@returns {boolean} `true` if both sides match and reference the same value.
*/
export default function isSameReference(left, right) {
left = unwrapNode(left);
right = unwrapNode(right);
if (left.type !== right.type) {
return false;
}
switch (left.type) {
case 'Super':
case 'ThisExpression': {
return true;
}
case 'Identifier':
case 'PrivateIdentifier': {
return left.name === right.name;
}
case 'Literal': {
return equalLiteralValue(left, right);
}
case 'MemberExpression': {
const nameA = getStaticPropertyName(left);
// `x.y = x["y"]`
if (nameA !== undefined) {
return (
isSameReference(left.object, right.object)
&& nameA === getStaticPropertyName(right)
);
}
/*
`x[0] = x[0]`
`x[y] = x[y]`
`x.y = x.y`
*/
return (
left.computed === right.computed
&& isSameReference(left.object, right.object)
&& isSameReference(left.property, right.property)
);
}
default: {
return false;
}
}
}
@@ -0,0 +1,8 @@
import hasSameRange from './has-same-range.js';
const isShorthandExportLocal = (node, context) => {
const {type, local, exported} = node.parent;
return type === 'ExportSpecifier' && hasSameRange(local, exported, context) && local === node;
};
export default isShorthandExportLocal;
@@ -0,0 +1,8 @@
import hasSameRange from './has-same-range.js';
const isShorthandImportLocal = (node, context) => {
const {type, local, imported} = node.parent;
return type === 'ImportSpecifier' && hasSameRange(local, imported, context) && local === node;
};
export default isShorthandImportLocal;
@@ -0,0 +1,8 @@
import isShorthandPropertyValue from './is-shorthand-property-value.js';
const isShorthandPropertyAssignmentPatternLeft = identifier =>
identifier.parent.type === 'AssignmentPattern'
&& identifier.parent.left === identifier
&& isShorthandPropertyValue(identifier.parent);
export default isShorthandPropertyAssignmentPatternLeft;
@@ -0,0 +1,6 @@
const isShorthandPropertyValue = identifier =>
identifier.parent.type === 'Property'
&& identifier.parent.shorthand
&& identifier === identifier.parent.value;
export default isShorthandPropertyValue;
@@ -0,0 +1,14 @@
import {findVariable} from '@eslint-community/eslint-utils';
/**
Checks if the given identifier node is shadowed in the given scope.
@param {string} node - The identifier node to check
@param {import('eslint').Rule.RuleContext} context - The ESLint rule context.
@returns {boolean} Whether or not the name is unresolved.
*/
export default function isUnresolvedVariable(node, context) {
const scope = context.sourceCode.getScope(node);
const variable = findVariable(scope, node);
return !variable;
}
@@ -0,0 +1,5 @@
import {isExpressionStatement} from '../ast/index.js';
const isValueNotUsable = node => isExpressionStatement(node.parent);
export default isValueNotUsable;
@@ -0,0 +1,118 @@
/* eslint-disable complexity */
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
// https://github.com/eslint/espree/blob/6b7d0b8100537dcd5c84a7fb17bbe28edcabe05d/lib/token-translator.js#L20
const tokenTypesNeedsSemicolon = new Set([
'String',
'Null',
'Boolean',
'Numeric',
'RegularExpression',
]);
const charactersMightNeedsSemicolon = new Set([
'[',
'(',
'/',
'`',
'+',
'-',
'*',
',',
'.',
]);
/**
Determines if a semicolon needs to be inserted before `code`, in order to avoid a SyntaxError.
@param {ESTree.Token} tokenBefore Token before `code`.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {String} [code] Code text to determine.
@returns {boolean} `true` if a semicolon needs to be inserted before `code`.
*/
export default function needsSemicolon(tokenBefore, context, code) {
if (
code === ''
|| (code && !charactersMightNeedsSemicolon.has(code.charAt(0)))
) {
return false;
}
if (!tokenBefore) {
return false;
}
const {sourceCode} = context;
const {type, value} = tokenBefore;
const range = sourceCode.getRange(tokenBefore);
const lastBlockNode = sourceCode.getNodeByRangeIndex(range[0]);
if (type === 'Punctuator') {
if (value === ';') {
return false;
}
if (value === ']') {
return true;
}
if (value === ')') {
switch (lastBlockNode.type) {
case 'IfStatement': {
if (sourceCode.getTokenBefore(lastBlockNode.consequent) === tokenBefore) {
return false;
}
break;
}
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WhileStatement':
case 'DoWhileStatement':
case 'WithStatement': {
if (lastBlockNode.body && sourceCode.getTokenBefore(lastBlockNode.body) === tokenBefore) {
return false;
}
break;
}
// No default
}
return true;
}
}
if (tokenTypesNeedsSemicolon.has(type)) {
return true;
}
if (type === 'Template') {
return value.endsWith('`');
}
if (lastBlockNode.type === 'ObjectExpression') {
return true;
}
if (type === 'Identifier') {
// `for...of`
if (value === 'of' && lastBlockNode.type === 'ForOfStatement') {
return false;
}
// `await`
if (value === 'await' && lastBlockNode.type === 'AwaitExpression') {
return false;
}
return true;
}
return false;
}
+49
View File
@@ -0,0 +1,49 @@
import {isNumericLiteral, isBigIntLiteral} from '../ast/index.js';
// Determine whether this node is a decimal integer literal.
// Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/utils/ast-utils.js#L1237
const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u;
export const isDecimalInteger = text => DECIMAL_INTEGER_PATTERN.test(text);
export const isDecimalIntegerNode = node => isNumericLiteral(node) && isDecimalInteger(node.raw);
export const isNumeric = node => isNumericLiteral(node) || isBigIntLiteral(node);
export const isLegacyOctal = node => isNumericLiteral(node) && /^0\d+$/.test(node.raw);
export function getPrefix(text) {
let prefix = '';
let data = text;
if (/^0[box]/i.test(text)) {
prefix = text.slice(0, 2);
data = text.slice(2);
}
return {prefix, data};
}
export function parseNumber(text) {
const {
number,
mark = '',
sign = '',
power = '',
} = text.match(/^(?<number>[\d._]*?)(?:(?<mark>[Ee])(?<sign>[+-])?(?<power>[\d_]+))?$/).groups;
return {
number,
mark,
sign,
power,
};
}
export function parseFloatNumber(text) {
const parts = text.split('.');
const [integer, fractional = ''] = parts;
const dot = parts.length === 2 ? '.' : '';
return {integer, dot, fractional};
}
@@ -0,0 +1,80 @@
import {isOpeningParenToken as isOpeningParenthesisToken} from '@eslint-community/eslint-utils';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
@import {
OpeningParenToken as OpeningParenthesisToken,
} from '@eslint-community/eslint-utils';
*/
/**
Get the opening parenthesis of the parent node syntax if it exists.
E.g., `if (a) {}` then the `(`.
@param {ESTree.Node} node The AST node to check.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {OpeningParenthesisToken | void} The left parenthesis of the parent node syntax
*/
function getParentSyntaxOpeningParenthesis(node, context) {
const {parent} = node;
switch (parent.type) {
case 'CallExpression':
case 'NewExpression': {
if (parent.arguments.length === 1 && parent.arguments[0] === node) {
return context.sourceCode.getTokenAfter(
parent.typeArguments ?? parent.callee,
isOpeningParenthesisToken,
);
}
return;
}
case 'DoWhileStatement': {
if (parent.test === node) {
return context.sourceCode.getTokenAfter(
parent.body,
isOpeningParenthesisToken,
);
}
return;
}
case 'IfStatement':
case 'WhileStatement': {
if (parent.test === node) {
return context.sourceCode.getFirstToken(parent, 1);
}
return;
}
case 'ImportExpression': {
if (parent.source === node) {
return context.sourceCode.getFirstToken(parent, 1);
}
return;
}
case 'SwitchStatement': {
if (parent.discriminant === node) {
return context.sourceCode.getFirstToken(parent, 1);
}
return;
}
case 'WithStatement': {
if (parent.object === node) {
return context.sourceCode.getFirstToken(parent, 1);
}
}
// No default
}
}
export default getParentSyntaxOpeningParenthesis;
@@ -0,0 +1,82 @@
// Based on https://github.com/eslint-community/eslint-utils/blob/1da21d4440028679f3c8d5841b85f9d97ca7f0f7/src/is-parenthesized.mjs#L1
import {
isOpeningParenToken as isOpeningParenthesisToken,
isClosingParenToken as isClosingParenthesisToken,
} from '@eslint-community/eslint-utils';
import getParentSyntaxOpeningParenthesis from './get-parent-syntax-opening-parenthesis.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
@import {
OpeningParenToken as OpeningParenthesisToken,
ClosingParenToken as ClosingParenthesisToken,
} from '@eslint-community/eslint-utils';
*/
/**
@typedef {[OpeningParenthesisToken, ClosingParenthesisToken]} ParenthesisTokenPair
*/
/**
Get surrounding parenthesis of the tokens or nodes.
@param {[ESTree.Node | OpeningParenthesisToken, ESTree.Node | ClosingParenthesisToken]} param0
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns [ParenthesisTokenPair | void]
*/
function getSurroundingParentheses([head, tail], context) {
const tokenBefore = context.sourceCode.getTokenBefore(head);
if (!tokenBefore || !isOpeningParenthesisToken(tokenBefore)) {
return;
}
const tokenAfter = context.sourceCode.getTokenAfter(tail);
if (!tokenBefore || !isClosingParenthesisToken(tokenAfter)) {
return;
}
return [tokenBefore, tokenAfter];
}
const SYNTAX_OPENING_PARENTHESIS_INITIAL_VALUE = Symbol('SYNTAX_OPENING_PARENTHESIS_INITIAL_VALUE');
/**
Iterate surrounding parenthesis of the node.
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {IterableIterator<ParenthesisTokenPair>}
*/
function * iterateSurroundingParentheses(node, context) {
if (
!node
// `Program` can't be parenthesized
|| !node.parent
// `CatchClause.param` can't be parenthesized, example `try {} catch (error) {}`
|| (node.parent.type === 'CatchClause' && node.parent.param === node)
) {
return;
}
let syntaxOpeningParenthesis = SYNTAX_OPENING_PARENTHESIS_INITIAL_VALUE;
let parentheses = [node, node];
while ((parentheses = getSurroundingParentheses(parentheses, context))) {
const [openingParenthesisToken] = parentheses;
if (syntaxOpeningParenthesis === SYNTAX_OPENING_PARENTHESIS_INITIAL_VALUE) {
syntaxOpeningParenthesis = getParentSyntaxOpeningParenthesis(node, context);
}
if (openingParenthesisToken === syntaxOpeningParenthesis) {
break;
}
yield parentheses;
}
}
export default iterateSurroundingParentheses;
@@ -0,0 +1,81 @@
import iterateSurroundingParentheses from './iterate-surrounding-parentheses.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
@import {
OpeningParenToken as OpeningParenthesisToken,
ClosingParenToken as ClosingParenthesisToken,
} from '@eslint-community/eslint-utils';
*/
/** @typedef {WeakMap<ESTree.Node, (OpeningParenthesisToken | ClosingParenthesisToken)[]>} */
const parenthesesCache = new WeakMap();
/**
Get surrounding parenthesis of the node.
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns [(OpeningParenthesisToken | ClosingParenthesisToken)[]]
*/
export function getParentheses(node, context) {
if (!node || !parenthesesCache.has(node)) {
const parenthesis = [];
for (const [openingParenthesisToken, closingParenthesisToken] of iterateSurroundingParentheses(node, context)) {
parenthesis.unshift(openingParenthesisToken);
parenthesis.push(closingParenthesisToken);
}
parenthesesCache.set(node, parenthesis);
}
return parenthesesCache.get(node);
}
/*
Get the parenthesized range of the node.
@param {ESTree.Node} node - The node to be checked.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {number[]}
*/
export function getParenthesizedRange(node, context) {
const parentheses = getParentheses(node, context);
const [start] = context.sourceCode.getRange(parentheses[0] ?? node);
const [, end] = context.sourceCode.getRange(parentheses.at(-1) ?? node);
return [start, end];
}
/*
Get the parenthesized text of the node.
@param {ESTree.Node} node - The node to be checked.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {string}
*/
export function getParenthesizedText(node, context) {
const [start, end] = getParenthesizedRange(node, context);
return context.sourceCode.text.slice(start, end);
}
/**
Check whether a given node is parenthesized or not.
@param {ESTree.Node} node The AST node to check.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {boolean} `true` if the node is parenthesized.
*/
export function isParenthesized(node, context) {
if (parenthesesCache.has(node)) {
return parenthesesCache.get(node).length > 0;
}
const isNotParenthesized = iterateSurroundingParentheses(node, context).next().done;
if (isNotParenthesized) {
parenthesesCache.set(node, []);
}
return !isNotParenthesized;
}
@@ -0,0 +1,18 @@
/**
Finds a variable named `name` in the scope `scope` (or it's parents).
@param {string} name - The variable name to be resolve.
@param {import('eslint').Scope.Scope} scope - The scope to look for the variable in.
@returns {import('eslint').Scope.Variable | void} - The found variable, if any.
*/
export default function resolveVariableName(name, scope) {
while (scope) {
const variable = scope.set.get(name);
if (variable) {
return variable;
}
scope = scope.upper;
}
}
+27
View File
@@ -0,0 +1,27 @@
import toEslintCreate, {markFunctionWrapped} from '../rule/to-eslint-create.js';
export function checkVueTemplate(unicornCreate, options) {
const {
visitScriptBlock,
} = {
visitScriptBlock: true,
...options,
};
const create = toEslintCreate(unicornCreate);
return markFunctionWrapped(context => {
const listeners = create(context);
const {parserServices} = context.sourceCode;
// `vue-eslint-parser`
if (parserServices?.defineTemplateBodyVisitor) {
return visitScriptBlock
? parserServices.defineTemplateBodyVisitor(listeners, listeners)
: parserServices.defineTemplateBodyVisitor(listeners);
}
return listeners;
});
}
@@ -0,0 +1,17 @@
/**
Check if parentheses should be added to a `node` when it's used as `argument` of `AwaitExpression`.
@param {Node} node - The AST node to check.
@returns {boolean}
*/
export default function shouldAddParenthesesToAwaitExpressionArgument(node) {
return (
node.type === 'SequenceExpression'
|| node.type === 'YieldExpression'
|| node.type === 'ArrowFunctionExpression'
|| node.type === 'ConditionalExpression'
|| node.type === 'AssignmentExpression'
|| node.type === 'LogicalExpression'
|| node.type === 'BinaryExpression'
);
}
@@ -0,0 +1,18 @@
/**
Check if parentheses should be added to a `node` when it's used as `callee` of `CallExpression`.
@param {Node} node - The AST node to check.
@returns {boolean}
*/
export default function shouldAddParenthesesToCallExpressionCallee(node) {
return node.type === 'SequenceExpression'
|| node.type === 'YieldExpression'
|| node.type === 'ArrowFunctionExpression'
|| node.type === 'ConditionalExpression'
|| node.type === 'AssignmentExpression'
|| node.type === 'LogicalExpression'
|| node.type === 'BinaryExpression'
|| node.type === 'UnaryExpression'
|| node.type === 'UpdateExpression'
|| node.type === 'NewExpression';
}
@@ -0,0 +1,15 @@
/**
Check if parentheses should be added to a `node` when it's used as child of `ConditionalExpression`.
@param {Node} node - The AST node to check.
@returns {boolean}
*/
export default function shouldAddParenthesesToConditionalExpressionChild(node) {
return node.type === 'AwaitExpression'
// Lower precedence, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
|| node.type === 'AssignmentExpression'
|| node.type === 'YieldExpression'
|| node.type === 'SequenceExpression'
|| node.type === 'TSAsExpression'
|| node.type === 'TSTypeAssertion';
}
@@ -0,0 +1,22 @@
/**
Check if parentheses should be added to a `node` when it's used as an `expression` of `ExpressionStatement`.
@param {Node} node - The AST node to check.
@param {SourceCode} sourceCode - The source code object.
@returns {boolean}
*/
export default function shouldAddParenthesesToExpressionStatementExpression(node) {
switch (node.type) {
case 'ObjectExpression': {
return true;
}
case 'AssignmentExpression': {
return node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern';
}
default: {
return false;
}
}
}
@@ -0,0 +1,43 @@
/**
Check if parentheses should be added to a `node` when it's used as child of `LogicalExpression`.
@param {Node} node - The AST node to check.
@param {{operator: string, property: string}} options - Options
@returns {boolean}
*/
export default function shouldAddParenthesesToLogicalExpressionChild(node, {operator, property}) {
// We are not using this, but we can improve this function with it
/* c8 ignore next 3 */
if (!property) {
throw new Error('`property` is required.');
}
if (
node.type === 'LogicalExpression'
&& node.operator === operator
) {
return false;
}
// Not really needed, but more readable
if (
node.type === 'AwaitExpression'
|| node.type === 'BinaryExpression'
) {
return true;
}
// Lower precedence than `LogicalExpression`
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
if (
node.type === 'LogicalExpression'
|| node.type === 'ConditionalExpression'
|| node.type === 'AssignmentExpression'
|| node.type === 'ArrowFunctionExpression'
|| node.type === 'YieldExpression'
|| node.type === 'SequenceExpression'
) {
return true;
}
return false;
}
@@ -0,0 +1,47 @@
import isNewExpressionWithParentheses from './is-new-expression-with-parentheses.js';
import {isDecimalIntegerNode} from './numeric.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
Check if parentheses should be added to a `node` when it's used as an `object` of `MemberExpression`.
@param {ESTree.Node} node - The AST node to check.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {boolean}
*/
export default function shouldAddParenthesesToMemberExpressionObject(node, context) {
switch (node.type) {
// This is not a full list. Some other nodes like `FunctionDeclaration` don't need parentheses,
// but it's not possible to be in the place we are checking at this point.
case 'Identifier':
case 'MemberExpression':
case 'CallExpression':
case 'ChainExpression':
case 'TemplateLiteral':
case 'ThisExpression':
case 'ArrayExpression':
case 'FunctionExpression': {
return false;
}
case 'NewExpression': {
return !isNewExpressionWithParentheses(node, context);
}
case 'Literal': {
/* c8 ignore next */
if (isDecimalIntegerNode(node)) {
return true;
}
return false;
}
default: {
return true;
}
}
}
@@ -0,0 +1,28 @@
// Copied from https://github.com/eslint/eslint/blob/aa87329d919f569404ca573b439934552006572f/lib/rules/no-extra-parens.js#L448
/**
Check if a member expression contains a call expression.
@param {ASTNode} node - The `MemberExpression` node to evaluate.
@returns {boolean} true if found, false if not.
*/
function doesMemberExpressionContainCallExpression(node) {
let currentNode = node.object;
let currentNodeType = node.object.type;
while (currentNodeType === 'MemberExpression') {
currentNode = currentNode.object;
currentNodeType = currentNode.type;
}
return currentNodeType === 'CallExpression';
}
/**
Check if parentheses should be added to a `node` when it's used as `callee` of `NewExpression`.
@param {Node} node - The AST node to check.
@returns {boolean}
*/
export default function shouldAddParenthesesToNewExpressionCallee(node) {
return node.type === 'MemberExpression' && doesMemberExpressionContainCallExpression(node);
}
@@ -0,0 +1,24 @@
/**
Check if parentheses should be added to a `node` when it's used as `argument` of `UnaryExpression`.
@param {Node} node - The AST node to check.
@param {string} operator - The UnaryExpression operator.
@returns {boolean}
*/
export default function shouldAddParenthesesToUnaryExpressionArgument(node, operator) {
// Only support `!` operator
if (operator !== '!') {
throw new Error('Unexpected operator');
}
return (
node.type === 'UpdateExpression'
|| node.type === 'BinaryExpression'
|| node.type === 'LogicalExpression'
|| node.type === 'ConditionalExpression'
|| node.type === 'AssignmentExpression'
|| node.type === 'ArrowFunctionExpression'
|| node.type === 'YieldExpression'
|| node.type === 'SequenceExpression'
);
}
+16
View File
@@ -0,0 +1,16 @@
import pluralize_ from 'pluralize';
/**
Singularizes a word/name, i.e. `items` to `item`.
@param {string} original - The word/name to singularize.
@returns {string|undefined} - The singularized result, or `undefined` if attempting singularization resulted in no change.
*/
const singular = original => {
const singularized = pluralize_.singular(original);
if (singularized !== original) {
return singularized;
}
};
export default singular;
@@ -0,0 +1,2 @@
export const upperFirst = string => string.charAt(0).toUpperCase() + string.slice(1);
export const lowerFirst = string => string.charAt(0).toLowerCase() + string.slice(1);
+25
View File
@@ -0,0 +1,25 @@
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
Get location info for the given node or range.
@param {ESTree.Node | ESTree.Range} nodeOrRange - The AST node or range to get the location for.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {int} [startOffset] - Start position offset.
@param {int} [endOffset] - End position offset.
@returns {ESTree.SourceLocation}
*/
function toLocation(nodeOrRange, context, startOffset = 0, endOffset = 0) {
const {sourceCode} = context;
const [start, end] = Array.isArray(nodeOrRange) ? nodeOrRange : sourceCode.getRange(nodeOrRange);
return {
start: sourceCode.getLocFromIndex(start + startOffset),
end: sourceCode.getLocFromIndex(end + endOffset),
};
}
export default toLocation;