407 lines
11 KiB
JavaScript
407 lines
11 KiB
JavaScript
/**
|
|
* @fileoverview Rule to flag non-camelcased identifiers
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Requirements
|
|
//------------------------------------------------------------------------------
|
|
|
|
const astUtils = require('./utils/ast-utils');
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//------------------------------------------------------------------------------
|
|
|
|
/** @type {import('../types').Rule.RuleModule} */
|
|
module.exports = {
|
|
meta: {
|
|
type: 'suggestion',
|
|
|
|
defaultOptions: [
|
|
{
|
|
allow: [],
|
|
ignoreDestructuring: false,
|
|
ignoreGlobals: false,
|
|
ignoreImports: false,
|
|
properties: 'always',
|
|
},
|
|
],
|
|
|
|
docs: {
|
|
description: 'Enforce camelcase naming convention',
|
|
recommended: false,
|
|
frozen: true,
|
|
url: 'https://eslint.org/docs/latest/rules/camelcase',
|
|
},
|
|
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
ignoreDestructuring: {
|
|
type: 'boolean',
|
|
},
|
|
ignoreImports: {
|
|
type: 'boolean',
|
|
},
|
|
ignoreGlobals: {
|
|
type: 'boolean',
|
|
},
|
|
properties: {
|
|
enum: ['always', 'never'],
|
|
},
|
|
allow: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
},
|
|
minItems: 0,
|
|
uniqueItems: true,
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
|
|
messages: {
|
|
notCamelCase: "Identifier '{{name}}' is not in camel case.",
|
|
notCamelCasePrivate: '#{{name}} is not in camel case.',
|
|
},
|
|
},
|
|
|
|
create(context) {
|
|
const [
|
|
{ allow, ignoreDestructuring, ignoreGlobals, ignoreImports, properties },
|
|
] = context.options;
|
|
const sourceCode = context.sourceCode;
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Helpers
|
|
//--------------------------------------------------------------------------
|
|
|
|
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
|
|
const reported = new Set();
|
|
|
|
/**
|
|
* Checks if a string contains an underscore and isn't all upper-case
|
|
* @param {string} name The string to check.
|
|
* @returns {boolean} if the string is underscored
|
|
* @private
|
|
*/
|
|
function isUnderscored(name) {
|
|
const nameBody = name.replace(/^_+|_+$/gu, '');
|
|
|
|
// if there's an underscore, it might be A_CONSTANT, which is okay
|
|
return nameBody.includes('_') && nameBody !== nameBody.toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Checks if a string match the ignore list
|
|
* @param {string} name The string to check.
|
|
* @returns {boolean} if the string is ignored
|
|
* @private
|
|
*/
|
|
function isAllowed(name) {
|
|
return allow.some(
|
|
(entry) => name === entry || name.match(new RegExp(entry, 'u'))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if a given name is good or not.
|
|
* @param {string} name The name to check.
|
|
* @returns {boolean} `true` if the name is good.
|
|
* @private
|
|
*/
|
|
function isGoodName(name) {
|
|
return !isUnderscored(name) || isAllowed(name);
|
|
}
|
|
|
|
/**
|
|
* Checks if a given identifier reference or member expression is an assignment
|
|
* target.
|
|
* @param {ASTNode} node The node to check.
|
|
* @returns {boolean} `true` if the node is an assignment target.
|
|
*/
|
|
function isAssignmentTarget(node) {
|
|
const parent = node.parent;
|
|
|
|
switch (parent.type) {
|
|
case 'AssignmentExpression':
|
|
case 'AssignmentPattern':
|
|
return parent.left === node;
|
|
|
|
case 'Property':
|
|
return (
|
|
parent.parent.type === 'ObjectPattern' && parent.value === node
|
|
);
|
|
case 'ArrayPattern':
|
|
case 'RestElement':
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a given binding identifier uses the original name as-is.
|
|
* - If it's in object destructuring or object expression, the original name is its property name.
|
|
* - If it's in import declaration, the original name is its exported name.
|
|
* @param {ASTNode} node The `Identifier` node to check.
|
|
* @returns {boolean} `true` if the identifier uses the original name as-is.
|
|
*/
|
|
function equalsToOriginalName(node) {
|
|
const localName = node.name;
|
|
const valueNode =
|
|
node.parent.type === 'AssignmentPattern' ? node.parent : node;
|
|
const parent = valueNode.parent;
|
|
|
|
switch (parent.type) {
|
|
case 'Property':
|
|
return (
|
|
(parent.parent.type === 'ObjectPattern' ||
|
|
parent.parent.type === 'ObjectExpression') &&
|
|
parent.value === valueNode &&
|
|
!parent.computed &&
|
|
parent.key.type === 'Identifier' &&
|
|
parent.key.name === localName
|
|
);
|
|
|
|
case 'ImportSpecifier':
|
|
return (
|
|
parent.local === node &&
|
|
astUtils.getModuleExportName(parent.imported) === localName
|
|
);
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reports an AST node as a rule violation.
|
|
* @param {ASTNode} node The node to report.
|
|
* @returns {void}
|
|
* @private
|
|
*/
|
|
function report(node) {
|
|
if (reported.has(node.range[0])) {
|
|
return;
|
|
}
|
|
reported.add(node.range[0]);
|
|
|
|
// Report it.
|
|
context.report({
|
|
node,
|
|
messageId:
|
|
node.type === 'PrivateIdentifier' ?
|
|
'notCamelCasePrivate'
|
|
: 'notCamelCase',
|
|
data: { name: node.name },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reports an identifier reference or a binding identifier.
|
|
* @param {ASTNode} node The `Identifier` node to report.
|
|
* @returns {void}
|
|
*/
|
|
function reportReferenceId(node) {
|
|
/*
|
|
* For backward compatibility, if it's in callings then ignore it.
|
|
* Not sure why it is.
|
|
*/
|
|
if (
|
|
node.parent.type === 'CallExpression' ||
|
|
node.parent.type === 'NewExpression'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* For backward compatibility, if it's a default value of
|
|
* destructuring/parameters then ignore it.
|
|
* Not sure why it is.
|
|
*/
|
|
if (
|
|
node.parent.type === 'AssignmentPattern' &&
|
|
node.parent.right === node
|
|
) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* The `ignoreDestructuring` flag skips the identifiers that uses
|
|
* the property name as-is.
|
|
*/
|
|
if (ignoreDestructuring && equalsToOriginalName(node)) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Import attribute keys are always ignored
|
|
*/
|
|
if (astUtils.isImportAttributeKey(node)) {
|
|
return;
|
|
}
|
|
|
|
report(node);
|
|
}
|
|
|
|
return {
|
|
// Report camelcase of global variable references ------------------
|
|
Program(node) {
|
|
const scope = sourceCode.getScope(node);
|
|
|
|
if (!ignoreGlobals) {
|
|
// Defined globals in config files or directive comments.
|
|
for (const variable of scope.variables) {
|
|
if (variable.identifiers.length > 0 || isGoodName(variable.name)) {
|
|
continue;
|
|
}
|
|
for (const reference of variable.references) {
|
|
/*
|
|
* For backward compatibility, this rule reports read-only
|
|
* references as well.
|
|
*/
|
|
reportReferenceId(reference.identifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Undefined globals.
|
|
for (const reference of scope.through) {
|
|
const id = reference.identifier;
|
|
|
|
if (isGoodName(id.name) || astUtils.isImportAttributeKey(id)) {
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* For backward compatibility, this rule reports read-only
|
|
* references as well.
|
|
*/
|
|
reportReferenceId(id);
|
|
}
|
|
},
|
|
|
|
// Report camelcase of declared variables --------------------------
|
|
[[
|
|
'VariableDeclaration',
|
|
'FunctionDeclaration',
|
|
'FunctionExpression',
|
|
'ArrowFunctionExpression',
|
|
'ClassDeclaration',
|
|
'ClassExpression',
|
|
'CatchClause',
|
|
]](node) {
|
|
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
|
if (isGoodName(variable.name)) {
|
|
continue;
|
|
}
|
|
const id = variable.identifiers[0];
|
|
|
|
// Report declaration.
|
|
if (!(ignoreDestructuring && equalsToOriginalName(id))) {
|
|
report(id);
|
|
}
|
|
|
|
/*
|
|
* For backward compatibility, report references as well.
|
|
* It looks unnecessary because declarations are reported.
|
|
*/
|
|
for (const reference of variable.references) {
|
|
if (reference.init) {
|
|
continue; // Skip the write references of initializers.
|
|
}
|
|
reportReferenceId(reference.identifier);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Report camelcase in properties ----------------------------------
|
|
[[
|
|
'ObjectExpression > Property[computed!=true] > Identifier.key',
|
|
'MethodDefinition[computed!=true] > Identifier.key',
|
|
'PropertyDefinition[computed!=true] > Identifier.key',
|
|
'MethodDefinition > PrivateIdentifier.key',
|
|
'PropertyDefinition > PrivateIdentifier.key',
|
|
]](node) {
|
|
if (
|
|
properties === 'never' ||
|
|
astUtils.isImportAttributeKey(node) ||
|
|
isGoodName(node.name)
|
|
) {
|
|
return;
|
|
}
|
|
report(node);
|
|
},
|
|
'MemberExpression[computed!=true] > Identifier.property'(node) {
|
|
if (
|
|
properties === 'never' ||
|
|
!isAssignmentTarget(node.parent) || // ← ignore read-only references.
|
|
isGoodName(node.name)
|
|
) {
|
|
return;
|
|
}
|
|
report(node);
|
|
},
|
|
|
|
// Report camelcase in import --------------------------------------
|
|
ImportDeclaration(node) {
|
|
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
|
if (isGoodName(variable.name)) {
|
|
continue;
|
|
}
|
|
const id = variable.identifiers[0];
|
|
|
|
// Report declaration.
|
|
if (!(ignoreImports && equalsToOriginalName(id))) {
|
|
report(id);
|
|
}
|
|
|
|
/*
|
|
* For backward compatibility, report references as well.
|
|
* It looks unnecessary because declarations are reported.
|
|
*/
|
|
for (const reference of variable.references) {
|
|
reportReferenceId(reference.identifier);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Report camelcase in re-export -----------------------------------
|
|
[[
|
|
'ExportAllDeclaration > Identifier.exported',
|
|
'ExportSpecifier > Identifier.exported',
|
|
]](node) {
|
|
if (isGoodName(node.name)) {
|
|
return;
|
|
}
|
|
report(node);
|
|
},
|
|
|
|
// Report camelcase in labels --------------------------------------
|
|
[[
|
|
'LabeledStatement > Identifier.label',
|
|
|
|
/*
|
|
* For backward compatibility, report references as well.
|
|
* It looks unnecessary because declarations are reported.
|
|
*/
|
|
'BreakStatement > Identifier.label',
|
|
'ContinueStatement > Identifier.label',
|
|
]](node) {
|
|
if (isGoodName(node.name)) {
|
|
return;
|
|
}
|
|
report(node);
|
|
},
|
|
};
|
|
},
|
|
};
|