routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
Generated
Vendored
+53
@@ -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'});
|
||||
+27
@@ -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
@@ -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
@@ -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',
|
||||
];
|
||||
Generated
Vendored
+22
@@ -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,
|
||||
};
|
||||
}
|
||||
Generated
Vendored
+34
@@ -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),
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}));
|
||||
}
|
||||
+24
@@ -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,
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
const escapeTemplateElementRaw = string => string.replaceAll(
|
||||
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g,
|
||||
String.raw`\$<symbol>`,
|
||||
);
|
||||
|
||||
export default escapeTemplateElementRaw;
|
||||
+21
@@ -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,
|
||||
};
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+147
@@ -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;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import {builtinRules} from 'eslint/use-at-your-own-risk';
|
||||
|
||||
export default function getBuiltinRule(id) {
|
||||
return builtinRules.get(id);
|
||||
}
|
||||
Generated
Vendored
+34
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+67
@@ -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};
|
||||
Generated
Vendored
+22
@@ -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};
|
||||
}
|
||||
+9
@@ -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`;
|
||||
}
|
||||
+18
@@ -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];
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import getScopes from './get-scopes.js';
|
||||
|
||||
const getReferences = scope => [...new Set(getScopes(scope).flatMap(({references}) => references))];
|
||||
|
||||
export default getReferences;
|
||||
+12
@@ -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;
|
||||
+38
@@ -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);
|
||||
Generated
Vendored
+22
@@ -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};
|
||||
}
|
||||
+24
@@ -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;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
// Get identifiers of given variable
|
||||
const getVariableIdentifiers = ({identifiers, references}) => [...new Set([
|
||||
...identifiers,
|
||||
...references.map(({identifier}) => identifier),
|
||||
])];
|
||||
|
||||
export default getVariableIdentifiers;
|
||||
Generated
Vendored
+68
@@ -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,
|
||||
});
|
||||
Generated
Vendored
+17
@@ -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;
|
||||
}
|
||||
+7
@@ -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
@@ -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';
|
||||
Generated
Vendored
+39
@@ -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;
|
||||
}
|
||||
+20
@@ -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;
|
||||
+14
@@ -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;
|
||||
+7
@@ -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;
|
||||
Generated
Vendored
+28
@@ -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];
|
||||
}
|
||||
Generated
Vendored
+65
@@ -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;
|
||||
+58
@@ -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));
|
||||
}
|
||||
Generated
Vendored
+20
@@ -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;
|
||||
Generated
Vendored
+40
@@ -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
@@ -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);
|
||||
}
|
||||
+10
@@ -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
|
||||
);
|
||||
}
|
||||
+7
@@ -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;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
const isSameIdentifier = (nodeA, nodeB) => nodeA.type === 'Identifier'
|
||||
&& nodeB.type === 'Identifier'
|
||||
&& nodeA.name === nodeB.name;
|
||||
|
||||
export default isSameIdentifier;
|
||||
+176
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+8
@@ -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;
|
||||
Generated
Vendored
+8
@@ -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;
|
||||
Generated
Vendored
+8
@@ -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;
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
const isShorthandPropertyValue = identifier =>
|
||||
identifier.parent.type === 'Property'
|
||||
&& identifier.parent.shorthand
|
||||
&& identifier === identifier.parent.value;
|
||||
|
||||
export default isShorthandPropertyValue;
|
||||
Generated
Vendored
+14
@@ -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;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import {isExpressionStatement} from '../ast/index.js';
|
||||
|
||||
const isValueNotUsable = node => isExpressionStatement(node.parent);
|
||||
|
||||
export default isValueNotUsable;
|
||||
+118
@@ -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
@@ -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};
|
||||
}
|
||||
Generated
Vendored
+80
@@ -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;
|
||||
Generated
Vendored
+82
@@ -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;
|
||||
Generated
Vendored
+81
@@ -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;
|
||||
}
|
||||
+18
@@ -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
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Generated
Vendored
+17
@@ -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'
|
||||
);
|
||||
}
|
||||
Generated
Vendored
+18
@@ -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';
|
||||
}
|
||||
Generated
Vendored
+15
@@ -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';
|
||||
}
|
||||
Generated
Vendored
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+43
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+28
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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
@@ -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;
|
||||
+2
@@ -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
@@ -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;
|
||||
Reference in New Issue
Block a user