line_push/node_modules/eslint-plugin-unicorn/rules/prevent-abbreviations.js
2022-07-17 13:16:16 +08:00

802 lines
17 KiB
JavaScript

'use strict';
const path = require('path');
const astUtils = require('eslint-ast-utils');
const {defaultsDeep, upperFirst, lowerFirst} = require('lodash');
const getDocumentationUrl = require('./utils/get-documentation-url');
const avoidCapture = require('./utils/avoid-capture');
const cartesianProductSamples = require('./utils/cartesian-product-samples');
const isShorthandPropertyIdentifier = require('./utils/is-shorthand-property-identifier');
const isShorthandImportIdentifier = require('./utils/is-shorthand-import-identifier');
const getVariableIdentifiers = require('./utils/get-variable-identifiers');
const renameIdentifier = require('./utils/rename-identifier');
const isUpperCase = string => string === string.toUpperCase();
const isUpperFirst = string => isUpperCase(string[0]);
// Keep this alphabetically sorted for easier maintenance
const defaultReplacements = {
acc: {
accumulator: true
},
arg: {
argument: true
},
args: {
arguments: true
},
arr: {
array: true
},
attr: {
attribute: true
},
attrs: {
attributes: true
},
btn: {
button: true
},
cb: {
callback: true
},
conf: {
config: true
},
ctx: {
context: true
},
cur: {
current: true
},
curr: {
current: true
},
db: {
database: true
},
dest: {
destination: true
},
dev: {
development: true
},
dir: {
direction: true,
directory: true
},
dirs: {
directories: true
},
doc: {
document: true
},
docs: {
documentation: true,
documents: true
},
e: {
error: true,
event: true
},
el: {
element: true
},
elem: {
element: true
},
env: {
environment: true
},
envs: {
environments: true
},
err: {
error: true
},
evt: {
event: true
},
ext: {
extension: true
},
exts: {
extensions: true
},
len: {
length: true
},
lib: {
library: true
},
mod: {
module: true
},
msg: {
message: true
},
num: {
number: true
},
obj: {
object: true
},
opts: {
options: true
},
param: {
parameter: true
},
params: {
parameters: true
},
pkg: {
package: true
},
prev: {
previous: true
},
prod: {
production: true
},
prop: {
property: true
},
props: {
properties: true
},
ref: {
reference: true
},
refs: {
references: true
},
rel: {
related: true,
relationship: true,
relative: true
},
req: {
request: true
},
res: {
response: true,
result: true
},
ret: {
returnValue: true
},
retval: {
returnValue: true
},
sep: {
separator: true
},
src: {
source: true
},
stdDev: {
standardDeviation: true
},
str: {
string: true
},
tbl: {
table: true
},
temp: {
temporary: true
},
tit: {
title: true
},
tmp: {
temporary: true
},
val: {
value: true
}
};
const defaultWhitelist = {
// React PropTypes
// https://reactjs.org/docs/typechecking-with-proptypes.html
propTypes: true,
// React.Component Class property
// https://reactjs.org/docs/react-component.html#defaultprops
defaultProps: true,
// React.Component static method
// https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
getDerivedStateFromProps: true,
// Ember class name
// https://api.emberjs.com/ember/3.10/classes/Ember.EmberENV/properties
EmberENV: true,
// `package.json` field
// https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file
devDependencies: true,
// Jest configuration
// https://jestjs.io/docs/en/configuration#setupfilesafterenv-array
setupFilesAfterEnv: true,
// Next.js function
// https://nextjs.org/learn/basics/fetching-data-for-pages
getInitialProps: true
};
const prepareOptions = ({
checkProperties = false,
checkVariables = true,
checkDefaultAndNamespaceImports = 'internal',
checkShorthandImports = 'internal',
checkShorthandProperties = false,
checkFilenames = true,
extendDefaultReplacements = true,
replacements = {},
extendDefaultWhitelist = true,
whitelist = {}
} = {}) => {
const mergedReplacements = extendDefaultReplacements ?
defaultsDeep({}, replacements, defaultReplacements) :
replacements;
const mergedWhitelist = extendDefaultWhitelist ?
defaultsDeep({}, whitelist, defaultWhitelist) :
whitelist;
return {
checkProperties,
checkVariables,
checkDefaultAndNamespaceImports,
checkShorthandImports,
checkShorthandProperties,
checkFilenames,
replacements: new Map(
Object.entries(mergedReplacements).map(
([discouragedName, replacements]) =>
[discouragedName, new Map(Object.entries(replacements))]
)
),
whitelist: new Map(Object.entries(mergedWhitelist))
};
};
const getWordReplacements = (word, {replacements, whitelist}) => {
// Skip constants and whitelist
if (isUpperCase(word) || whitelist.get(word)) {
return [];
}
const replacement = replacements.get(lowerFirst(word)) ||
replacements.get(word) ||
replacements.get(upperFirst(word));
let wordReplacement = [];
if (replacement) {
const transform = isUpperFirst(word) ? upperFirst : lowerFirst;
wordReplacement = [...replacement.keys()]
.filter(name => replacement.get(name))
.map(name => transform(name));
}
return wordReplacement.length > 0 ? wordReplacement.sort() : [];
};
const getNameReplacements = (name, options, limit = 3) => {
const {whitelist} = options;
// Skip constants and whitelist
if (isUpperCase(name) || whitelist.get(name)) {
return {total: 0};
}
// Find exact replacements
const exactReplacements = getWordReplacements(name, options);
if (exactReplacements.length > 0) {
return {
total: exactReplacements.length,
samples: exactReplacements.slice(0, limit)
};
}
// Split words
const words = name.split(/(?=[^a-z])|(?<=[^A-Za-z])/).filter(Boolean);
let hasReplacements = false;
const combinations = words.map(word => {
const wordReplacements = getWordReplacements(word, options);
if (wordReplacements.length > 0) {
hasReplacements = true;
return wordReplacements;
}
return [word];
});
// No replacements for any word
if (!hasReplacements) {
return {total: 0};
}
const {
total,
samples
} = cartesianProductSamples(combinations, limit);
return {
total,
samples: samples.map(words => words.join(''))
};
};
const anotherNameMessage = 'A more descriptive name will do too.';
const formatMessage = (discouragedName, replacements, nameTypeText) => {
const message = [];
const {total, samples = []} = replacements;
if (total === 1) {
message.push(`The ${nameTypeText} \`${discouragedName}\` should be named \`${samples[0]}\`.`);
} else {
let replacementsText = samples
.map(replacement => `\`${replacement}\``)
.join(', ');
const omittedReplacementsCount = total - samples.length;
if (omittedReplacementsCount > 0) {
replacementsText += `, ... (${omittedReplacementsCount > 99 ? '99+' : omittedReplacementsCount} more omitted)`;
}
message.push(`Please rename the ${nameTypeText} \`${discouragedName}\`.`);
message.push(`Suggested names are: ${replacementsText}.`);
}
message.push(anotherNameMessage);
return message.join(' ');
};
const isExportedIdentifier = identifier => {
if (
identifier.parent.type === 'VariableDeclarator' &&
identifier.parent.id === identifier
) {
return (
identifier.parent.parent.type === 'VariableDeclaration' &&
identifier.parent.parent.parent.type === 'ExportNamedDeclaration'
);
}
if (
identifier.parent.type === 'FunctionDeclaration' &&
identifier.parent.id === identifier
) {
return identifier.parent.parent.type === 'ExportNamedDeclaration';
}
if (
identifier.parent.type === 'ClassDeclaration' &&
identifier.parent.id === identifier
) {
return identifier.parent.parent.type === 'ExportNamedDeclaration';
}
return false;
};
const shouldFix = variable => {
return !getVariableIdentifiers(variable).some(identifier => isExportedIdentifier(identifier));
};
const isDefaultOrNamespaceImportName = identifier => {
if (
identifier.parent.type === 'ImportDefaultSpecifier' &&
identifier.parent.local === identifier
) {
return true;
}
if (
identifier.parent.type === 'ImportNamespaceSpecifier' &&
identifier.parent.local === identifier
) {
return true;
}
if (
identifier.parent.type === 'ImportSpecifier' &&
identifier.parent.local === identifier &&
identifier.parent.imported.type === 'Identifier' &&
identifier.parent.imported.name === 'default'
) {
return true;
}
if (
identifier.parent.type === 'VariableDeclarator' &&
identifier.parent.id === identifier &&
astUtils.isStaticRequire(identifier.parent.init)
) {
return true;
}
return false;
};
const isClassVariable = variable => {
if (variable.defs.length !== 1) {
return false;
}
const [definition] = variable.defs;
return definition.type === 'ClassName';
};
const shouldReportIdentifierAsProperty = identifier => {
if (
identifier.parent.type === 'MemberExpression' &&
identifier.parent.property === identifier &&
!identifier.parent.computed &&
identifier.parent.parent.type === 'AssignmentExpression' &&
identifier.parent.parent.left === identifier.parent
) {
return true;
}
if (
identifier.parent.type === 'Property' &&
identifier.parent.key === identifier &&
!identifier.parent.computed &&
!identifier.parent.shorthand && // Shorthand properties are reported and fixed as variables
identifier.parent.parent.type === 'ObjectExpression'
) {
return true;
}
if (
identifier.parent.type === 'ExportSpecifier' &&
identifier.parent.exported === identifier &&
identifier.parent.local !== identifier // Same as shorthand properties above
) {
return true;
}
if (
identifier.parent.type === 'MethodDefinition' &&
identifier.parent.key === identifier &&
!identifier.parent.computed
) {
return true;
}
if (
identifier.parent.type === 'ClassProperty' &&
identifier.parent.key === identifier &&
!identifier.parent.computed
) {
return true;
}
return false;
};
const isInternalImport = node => {
let source = '';
if (node.type === 'Variable') {
source = node.node.init.arguments[0].value;
} else if (node.type === 'ImportBinding') {
source = node.parent.source.value;
}
return (
!source.includes('node_modules') &&
(source.startsWith('.') || source.startsWith('/'))
);
};
const create = context => {
const {ecmaVersion} = context.parserOptions;
const options = prepareOptions(context.options[0]);
const filenameWithExtension = context.getFilename();
const sourceCode = context.getSourceCode();
// A `class` declaration produces two variables in two scopes:
// the inner class scope, and the outer one (whereever the class is declared).
// This map holds the outer ones to be later processed when the inner one is encountered.
// For why this is not a eslint issue see https://github.com/eslint/eslint-scope/issues/48#issuecomment-464358754
const identifierToOuterClassVariable = new WeakMap();
const checkPossiblyWeirdClassVariable = variable => {
if (isClassVariable(variable)) {
if (variable.scope.type === 'class') { // The inner class variable
const [definition] = variable.defs;
const outerClassVariable = identifierToOuterClassVariable.get(definition.name);
if (!outerClassVariable) {
return checkVariable(variable);
}
// Create a normal-looking variable (like a `var` or a `function`)
// For which a single `variable` holds all references, unline with `class`
const combinedReferencesVariable = {
name: variable.name,
scope: variable.scope,
defs: variable.defs,
identifiers: variable.identifiers,
references: variable.references.concat(outerClassVariable.references)
};
// Call the common checker with the newly forged normalized class variable
return checkVariable(combinedReferencesVariable);
}
// The outer class variable, we save it for later, when it's inner counterpart is encountered
const [definition] = variable.defs;
identifierToOuterClassVariable.set(definition.name, variable);
return;
}
return checkVariable(variable);
};
// Holds a map from a `Scope` to a `Set` of new variable names generated by our fixer.
// Used to avoid generating duplicate names, see for instance `let errCb, errorCb` test.
const scopeToNamesGeneratedByFixer = new WeakMap();
const isSafeName = (name, scopes) => scopes.every(scope => {
const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
return !generatedNames || !generatedNames.has(name);
});
const checkVariable = variable => {
if (variable.defs.length === 0) {
return;
}
const [definition] = variable.defs;
if (isDefaultOrNamespaceImportName(definition.name)) {
if (!options.checkDefaultAndNamespaceImports) {
return;
}
if (
options.checkDefaultAndNamespaceImports === 'internal' &&
!isInternalImport(definition)
) {
return;
}
}
if (isShorthandImportIdentifier(definition.name)) {
if (!options.checkShorthandImports) {
return;
}
if (
options.checkShorthandImports === 'internal' &&
!isInternalImport(definition)
) {
return;
}
}
if (
!options.checkShorthandProperties &&
isShorthandPropertyIdentifier(definition.name)
) {
return;
}
const variableReplacements = getNameReplacements(variable.name, options);
if (variableReplacements.total === 0) {
return;
}
const scopes = variable.references.map(reference => reference.from).concat(variable.scope);
variableReplacements.samples = variableReplacements.samples.map(
name => avoidCapture(name, scopes, ecmaVersion, isSafeName)
);
const problem = {
node: definition.name,
message: formatMessage(definition.name.name, variableReplacements, 'variable')
};
if (variableReplacements.total === 1 && shouldFix(variable)) {
const [replacement] = variableReplacements.samples;
for (const scope of scopes) {
if (!scopeToNamesGeneratedByFixer.has(scope)) {
scopeToNamesGeneratedByFixer.set(scope, new Set());
}
const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
generatedNames.add(replacement);
}
problem.fix = fixer => {
return getVariableIdentifiers(variable)
.map(identifier => renameIdentifier(identifier, replacement, fixer, sourceCode));
};
}
context.report(problem);
};
const checkVariables = scope => {
scope.variables.forEach(variable => checkPossiblyWeirdClassVariable(variable));
};
const checkChildScopes = scope => {
scope.childScopes.forEach(scope => checkScope(scope));
};
const checkScope = scope => {
checkVariables(scope);
return checkChildScopes(scope);
};
return {
Identifier(node) {
if (!options.checkProperties) {
return;
}
if (node.name === '__proto__') {
return;
}
const identifierReplacements = getNameReplacements(node.name, options);
if (identifierReplacements.total === 0) {
return;
}
if (!shouldReportIdentifierAsProperty(node)) {
return;
}
const problem = {
node,
message: formatMessage(node.name, identifierReplacements, 'property')
};
context.report(problem);
},
Program(node) {
if (!options.checkFilenames) {
return;
}
if (
filenameWithExtension === '<input>' ||
filenameWithExtension === '<text>'
) {
return;
}
const extension = path.extname(filenameWithExtension);
const filename = path.basename(filenameWithExtension, extension);
const filenameReplacements = getNameReplacements(filename, options);
if (filenameReplacements.total === 0) {
return;
}
filenameReplacements.samples = filenameReplacements.samples.map(replacement => `${replacement}${extension}`);
context.report({
node,
message: formatMessage(filenameWithExtension, filenameReplacements, 'filename')
});
},
'Program:exit'() {
if (!options.checkVariables) {
return;
}
checkScope(context.getScope());
}
};
};
const schema = [
{
type: 'object',
properties: {
checkProperties: {
type: 'boolean'
},
checkVariables: {
type: 'boolean'
},
checkDefaultAndNamespaceImports: {
type: [
'boolean',
'string'
],
pattern: 'internal'
},
checkShorthandImports: {
type: [
'boolean',
'string'
],
pattern: 'internal'
},
checkShorthandProperties: {
type: 'boolean'
},
checkFilenames: {
type: 'boolean'
},
extendDefaultReplacements: {
type: 'boolean'
},
replacements: {
$ref: '#/items/0/definitions/abbreviations'
},
extendDefaultWhitelist: {
type: 'boolean'
},
whitelist: {
$ref: '#/items/0/definitions/booleanObject'
}
},
additionalProperties: false,
definitions: {
abbreviations: {
type: 'object',
additionalProperties: {
$ref: '#/items/0/definitions/replacements'
}
},
replacements: {
anyOf: [
{
enum: [
false
]
},
{
$ref: '#/items/0/definitions/booleanObject'
}
]
},
booleanObject: {
type: 'object',
additionalProperties: {
type: 'boolean'
}
}
}
}
];
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
schema
}
};