1 | 'use strict';
|
2 | const path = require('path');
|
3 | const astUtils = require('eslint-ast-utils');
|
4 | const {defaultsDeep, upperFirst, lowerFirst} = require('lodash');
|
5 |
|
6 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
7 | const avoidCapture = require('./utils/avoid-capture');
|
8 | const cartesianProductSamples = require('./utils/cartesian-product-samples');
|
9 | const isShorthandPropertyIdentifier = require('./utils/is-shorthand-property-identifier');
|
10 | const isShorthandImportIdentifier = require('./utils/is-shorthand-import-identifier');
|
11 | const getVariableIdentifiers = require('./utils/get-variable-identifiers');
|
12 | const renameIdentifier = require('./utils/rename-identifier');
|
13 |
|
14 | const isUpperCase = string => string === string.toUpperCase();
|
15 | const isUpperFirst = string => isUpperCase(string[0]);
|
16 |
|
17 |
|
18 | const defaultReplacements = {
|
19 | acc: {
|
20 | accumulator: true
|
21 | },
|
22 | arg: {
|
23 | argument: true
|
24 | },
|
25 | args: {
|
26 | arguments: true
|
27 | },
|
28 | arr: {
|
29 | array: true
|
30 | },
|
31 | attr: {
|
32 | attribute: true
|
33 | },
|
34 | attrs: {
|
35 | attributes: true
|
36 | },
|
37 | btn: {
|
38 | button: true
|
39 | },
|
40 | cb: {
|
41 | callback: true
|
42 | },
|
43 | conf: {
|
44 | config: true
|
45 | },
|
46 | ctx: {
|
47 | context: true
|
48 | },
|
49 | cur: {
|
50 | current: true
|
51 | },
|
52 | curr: {
|
53 | current: true
|
54 | },
|
55 | db: {
|
56 | database: true
|
57 | },
|
58 | dest: {
|
59 | destination: true
|
60 | },
|
61 | dev: {
|
62 | development: true
|
63 | },
|
64 | dir: {
|
65 | direction: true,
|
66 | directory: true
|
67 | },
|
68 | dirs: {
|
69 | directories: true
|
70 | },
|
71 | doc: {
|
72 | document: true
|
73 | },
|
74 | docs: {
|
75 | documentation: true,
|
76 | documents: true
|
77 | },
|
78 | e: {
|
79 | error: true,
|
80 | event: true
|
81 | },
|
82 | el: {
|
83 | element: true
|
84 | },
|
85 | elem: {
|
86 | element: true
|
87 | },
|
88 | env: {
|
89 | environment: true
|
90 | },
|
91 | envs: {
|
92 | environments: true
|
93 | },
|
94 | err: {
|
95 | error: true
|
96 | },
|
97 | ev: {
|
98 | event: true
|
99 | },
|
100 | evt: {
|
101 | event: true
|
102 | },
|
103 | ext: {
|
104 | extension: true
|
105 | },
|
106 | exts: {
|
107 | extensions: true
|
108 | },
|
109 | len: {
|
110 | length: true
|
111 | },
|
112 | lib: {
|
113 | library: true
|
114 | },
|
115 | mod: {
|
116 | module: true
|
117 | },
|
118 | msg: {
|
119 | message: true
|
120 | },
|
121 | num: {
|
122 | number: true
|
123 | },
|
124 | obj: {
|
125 | object: true
|
126 | },
|
127 | opts: {
|
128 | options: true
|
129 | },
|
130 | param: {
|
131 | parameter: true
|
132 | },
|
133 | params: {
|
134 | parameters: true
|
135 | },
|
136 | pkg: {
|
137 | package: true
|
138 | },
|
139 | prev: {
|
140 | previous: true
|
141 | },
|
142 | prod: {
|
143 | production: true
|
144 | },
|
145 | prop: {
|
146 | property: true
|
147 | },
|
148 | props: {
|
149 | properties: true
|
150 | },
|
151 | ref: {
|
152 | reference: true
|
153 | },
|
154 | refs: {
|
155 | references: true
|
156 | },
|
157 | rel: {
|
158 | related: true,
|
159 | relationship: true,
|
160 | relative: true
|
161 | },
|
162 | req: {
|
163 | request: true
|
164 | },
|
165 | res: {
|
166 | response: true,
|
167 | result: true
|
168 | },
|
169 | ret: {
|
170 | returnValue: true
|
171 | },
|
172 | retval: {
|
173 | returnValue: true
|
174 | },
|
175 | sep: {
|
176 | separator: true
|
177 | },
|
178 | src: {
|
179 | source: true
|
180 | },
|
181 | stdDev: {
|
182 | standardDeviation: true
|
183 | },
|
184 | str: {
|
185 | string: true
|
186 | },
|
187 | tbl: {
|
188 | table: true
|
189 | },
|
190 | temp: {
|
191 | temporary: true
|
192 | },
|
193 | tit: {
|
194 | title: true
|
195 | },
|
196 | tmp: {
|
197 | temporary: true
|
198 | },
|
199 | val: {
|
200 | value: true
|
201 | }
|
202 | };
|
203 |
|
204 | const defaultWhitelist = {
|
205 |
|
206 |
|
207 | propTypes: true,
|
208 |
|
209 |
|
210 | defaultProps: true,
|
211 |
|
212 |
|
213 | getDerivedStateFromProps: true,
|
214 |
|
215 |
|
216 | EmberENV: true,
|
217 |
|
218 |
|
219 | devDependencies: true,
|
220 |
|
221 |
|
222 | setupFilesAfterEnv: true,
|
223 |
|
224 |
|
225 | getInitialProps: true
|
226 | };
|
227 |
|
228 | const prepareOptions = ({
|
229 | checkProperties = false,
|
230 | checkVariables = true,
|
231 |
|
232 | checkDefaultAndNamespaceImports = 'internal',
|
233 | checkShorthandImports = 'internal',
|
234 | checkShorthandProperties = false,
|
235 |
|
236 | checkFilenames = true,
|
237 |
|
238 | extendDefaultReplacements = true,
|
239 | replacements = {},
|
240 |
|
241 | extendDefaultWhitelist = true,
|
242 | whitelist = {}
|
243 | } = {}) => {
|
244 | const mergedReplacements = extendDefaultReplacements ?
|
245 | defaultsDeep({}, replacements, defaultReplacements) :
|
246 | replacements;
|
247 |
|
248 | const mergedWhitelist = extendDefaultWhitelist ?
|
249 | defaultsDeep({}, whitelist, defaultWhitelist) :
|
250 | whitelist;
|
251 |
|
252 | return {
|
253 | checkProperties,
|
254 | checkVariables,
|
255 |
|
256 | checkDefaultAndNamespaceImports,
|
257 | checkShorthandImports,
|
258 | checkShorthandProperties,
|
259 |
|
260 | checkFilenames,
|
261 |
|
262 | replacements: new Map(
|
263 | Object.entries(mergedReplacements).map(
|
264 | ([discouragedName, replacements]) =>
|
265 | [discouragedName, new Map(Object.entries(replacements))]
|
266 | )
|
267 | ),
|
268 | whitelist: new Map(Object.entries(mergedWhitelist))
|
269 | };
|
270 | };
|
271 |
|
272 | const getWordReplacements = (word, {replacements, whitelist}) => {
|
273 |
|
274 | if (isUpperCase(word) || whitelist.get(word)) {
|
275 | return [];
|
276 | }
|
277 |
|
278 | const replacement = replacements.get(lowerFirst(word)) ||
|
279 | replacements.get(word) ||
|
280 | replacements.get(upperFirst(word));
|
281 |
|
282 | let wordReplacement = [];
|
283 | if (replacement) {
|
284 | const transform = isUpperFirst(word) ? upperFirst : lowerFirst;
|
285 | wordReplacement = [...replacement.keys()]
|
286 | .filter(name => replacement.get(name))
|
287 | .map(name => transform(name));
|
288 | }
|
289 |
|
290 | return wordReplacement.length > 0 ? wordReplacement.sort() : [];
|
291 | };
|
292 |
|
293 | const getNameReplacements = (name, options, limit = 3) => {
|
294 | const {whitelist} = options;
|
295 |
|
296 |
|
297 | if (isUpperCase(name) || whitelist.get(name)) {
|
298 | return {total: 0};
|
299 | }
|
300 |
|
301 |
|
302 | const exactReplacements = getWordReplacements(name, options);
|
303 |
|
304 | if (exactReplacements.length > 0) {
|
305 | return {
|
306 | total: exactReplacements.length,
|
307 | samples: exactReplacements.slice(0, limit)
|
308 | };
|
309 | }
|
310 |
|
311 |
|
312 | const words = name.split(/(?=[^a-z])|(?<=[^A-Za-z])/).filter(Boolean);
|
313 |
|
314 | let hasReplacements = false;
|
315 | const combinations = words.map(word => {
|
316 | const wordReplacements = getWordReplacements(word, options);
|
317 |
|
318 | if (wordReplacements.length > 0) {
|
319 | hasReplacements = true;
|
320 | return wordReplacements;
|
321 | }
|
322 |
|
323 | return [word];
|
324 | });
|
325 |
|
326 |
|
327 | if (!hasReplacements) {
|
328 | return {total: 0};
|
329 | }
|
330 |
|
331 | const {
|
332 | total,
|
333 | samples
|
334 | } = cartesianProductSamples(combinations, limit);
|
335 |
|
336 | return {
|
337 | total,
|
338 | samples: samples.map(words => words.join(''))
|
339 | };
|
340 | };
|
341 |
|
342 | const anotherNameMessage = 'A more descriptive name will do too.';
|
343 |
|
344 | const formatMessage = (discouragedName, replacements, nameTypeText) => {
|
345 | const message = [];
|
346 | const {total, samples = []} = replacements;
|
347 |
|
348 | if (total === 1) {
|
349 | message.push(`The ${nameTypeText} \`${discouragedName}\` should be named \`${samples[0]}\`.`);
|
350 | } else {
|
351 | let replacementsText = samples
|
352 | .map(replacement => `\`${replacement}\``)
|
353 | .join(', ');
|
354 |
|
355 | const omittedReplacementsCount = total - samples.length;
|
356 | if (omittedReplacementsCount > 0) {
|
357 | replacementsText += `, ... (${omittedReplacementsCount > 99 ? '99+' : omittedReplacementsCount} more omitted)`;
|
358 | }
|
359 |
|
360 | message.push(`Please rename the ${nameTypeText} \`${discouragedName}\`.`);
|
361 | message.push(`Suggested names are: ${replacementsText}.`);
|
362 | }
|
363 |
|
364 | message.push(anotherNameMessage);
|
365 |
|
366 | return message.join(' ');
|
367 | };
|
368 |
|
369 | const isExportedIdentifier = identifier => {
|
370 | if (
|
371 | identifier.parent.type === 'VariableDeclarator' &&
|
372 | identifier.parent.id === identifier
|
373 | ) {
|
374 | return (
|
375 | identifier.parent.parent.type === 'VariableDeclaration' &&
|
376 | identifier.parent.parent.parent.type === 'ExportNamedDeclaration'
|
377 | );
|
378 | }
|
379 |
|
380 | if (
|
381 | identifier.parent.type === 'FunctionDeclaration' &&
|
382 | identifier.parent.id === identifier
|
383 | ) {
|
384 | return identifier.parent.parent.type === 'ExportNamedDeclaration';
|
385 | }
|
386 |
|
387 | if (
|
388 | identifier.parent.type === 'ClassDeclaration' &&
|
389 | identifier.parent.id === identifier
|
390 | ) {
|
391 | return identifier.parent.parent.type === 'ExportNamedDeclaration';
|
392 | }
|
393 |
|
394 | return false;
|
395 | };
|
396 |
|
397 | const shouldFix = variable => {
|
398 | return !getVariableIdentifiers(variable).some(identifier => isExportedIdentifier(identifier));
|
399 | };
|
400 |
|
401 | const isDefaultOrNamespaceImportName = identifier => {
|
402 | if (
|
403 | identifier.parent.type === 'ImportDefaultSpecifier' &&
|
404 | identifier.parent.local === identifier
|
405 | ) {
|
406 | return true;
|
407 | }
|
408 |
|
409 | if (
|
410 | identifier.parent.type === 'ImportNamespaceSpecifier' &&
|
411 | identifier.parent.local === identifier
|
412 | ) {
|
413 | return true;
|
414 | }
|
415 |
|
416 | if (
|
417 | identifier.parent.type === 'ImportSpecifier' &&
|
418 | identifier.parent.local === identifier &&
|
419 | identifier.parent.imported.type === 'Identifier' &&
|
420 | identifier.parent.imported.name === 'default'
|
421 | ) {
|
422 | return true;
|
423 | }
|
424 |
|
425 | if (
|
426 | identifier.parent.type === 'VariableDeclarator' &&
|
427 | identifier.parent.id === identifier &&
|
428 | astUtils.isStaticRequire(identifier.parent.init)
|
429 | ) {
|
430 | return true;
|
431 | }
|
432 |
|
433 | return false;
|
434 | };
|
435 |
|
436 | const isClassVariable = variable => {
|
437 | if (variable.defs.length !== 1) {
|
438 | return false;
|
439 | }
|
440 |
|
441 | const [definition] = variable.defs;
|
442 |
|
443 | return definition.type === 'ClassName';
|
444 | };
|
445 |
|
446 | const shouldReportIdentifierAsProperty = identifier => {
|
447 | if (
|
448 | identifier.parent.type === 'MemberExpression' &&
|
449 | identifier.parent.property === identifier &&
|
450 | !identifier.parent.computed &&
|
451 | identifier.parent.parent.type === 'AssignmentExpression' &&
|
452 | identifier.parent.parent.left === identifier.parent
|
453 | ) {
|
454 | return true;
|
455 | }
|
456 |
|
457 | if (
|
458 | identifier.parent.type === 'Property' &&
|
459 | identifier.parent.key === identifier &&
|
460 | !identifier.parent.computed &&
|
461 | !identifier.parent.shorthand &&
|
462 | identifier.parent.parent.type === 'ObjectExpression'
|
463 | ) {
|
464 | return true;
|
465 | }
|
466 |
|
467 | if (
|
468 | identifier.parent.type === 'ExportSpecifier' &&
|
469 | identifier.parent.exported === identifier &&
|
470 | identifier.parent.local !== identifier
|
471 | ) {
|
472 | return true;
|
473 | }
|
474 |
|
475 | if (
|
476 | identifier.parent.type === 'MethodDefinition' &&
|
477 | identifier.parent.key === identifier &&
|
478 | !identifier.parent.computed
|
479 | ) {
|
480 | return true;
|
481 | }
|
482 |
|
483 | if (
|
484 | identifier.parent.type === 'ClassProperty' &&
|
485 | identifier.parent.key === identifier &&
|
486 | !identifier.parent.computed
|
487 | ) {
|
488 | return true;
|
489 | }
|
490 |
|
491 | return false;
|
492 | };
|
493 |
|
494 | const isInternalImport = node => {
|
495 | let source = '';
|
496 |
|
497 | if (node.type === 'Variable') {
|
498 | source = node.node.init.arguments[0].value;
|
499 | } else if (node.type === 'ImportBinding') {
|
500 | source = node.parent.source.value;
|
501 | }
|
502 |
|
503 | return (
|
504 | !source.includes('node_modules') &&
|
505 | (source.startsWith('.') || source.startsWith('/'))
|
506 | );
|
507 | };
|
508 |
|
509 | const create = context => {
|
510 | const {ecmaVersion} = context.parserOptions;
|
511 | const options = prepareOptions(context.options[0]);
|
512 | const filenameWithExtension = context.getFilename();
|
513 | const sourceCode = context.getSourceCode();
|
514 |
|
515 |
|
516 |
|
517 |
|
518 |
|
519 | const identifierToOuterClassVariable = new WeakMap();
|
520 |
|
521 | const checkPossiblyWeirdClassVariable = variable => {
|
522 | if (isClassVariable(variable)) {
|
523 | if (variable.scope.type === 'class') {
|
524 | const [definition] = variable.defs;
|
525 | const outerClassVariable = identifierToOuterClassVariable.get(definition.name);
|
526 |
|
527 | if (!outerClassVariable) {
|
528 | return checkVariable(variable);
|
529 | }
|
530 |
|
531 |
|
532 |
|
533 | const combinedReferencesVariable = {
|
534 | name: variable.name,
|
535 | scope: variable.scope,
|
536 | defs: variable.defs,
|
537 | identifiers: variable.identifiers,
|
538 | references: variable.references.concat(outerClassVariable.references)
|
539 | };
|
540 |
|
541 |
|
542 | return checkVariable(combinedReferencesVariable);
|
543 | }
|
544 |
|
545 |
|
546 | const [definition] = variable.defs;
|
547 | identifierToOuterClassVariable.set(definition.name, variable);
|
548 |
|
549 | return;
|
550 | }
|
551 |
|
552 | return checkVariable(variable);
|
553 | };
|
554 |
|
555 |
|
556 |
|
557 | const scopeToNamesGeneratedByFixer = new WeakMap();
|
558 | const isSafeName = (name, scopes) => scopes.every(scope => {
|
559 | const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
|
560 | return !generatedNames || !generatedNames.has(name);
|
561 | });
|
562 |
|
563 | const checkVariable = variable => {
|
564 | if (variable.defs.length === 0) {
|
565 | return;
|
566 | }
|
567 |
|
568 | const [definition] = variable.defs;
|
569 |
|
570 | if (isDefaultOrNamespaceImportName(definition.name)) {
|
571 | if (!options.checkDefaultAndNamespaceImports) {
|
572 | return;
|
573 | }
|
574 |
|
575 | if (
|
576 | options.checkDefaultAndNamespaceImports === 'internal' &&
|
577 | !isInternalImport(definition)
|
578 | ) {
|
579 | return;
|
580 | }
|
581 | }
|
582 |
|
583 | if (isShorthandImportIdentifier(definition.name)) {
|
584 | if (!options.checkShorthandImports) {
|
585 | return;
|
586 | }
|
587 |
|
588 | if (
|
589 | options.checkShorthandImports === 'internal' &&
|
590 | !isInternalImport(definition)
|
591 | ) {
|
592 | return;
|
593 | }
|
594 | }
|
595 |
|
596 | if (
|
597 | !options.checkShorthandProperties &&
|
598 | isShorthandPropertyIdentifier(definition.name)
|
599 | ) {
|
600 | return;
|
601 | }
|
602 |
|
603 | const variableReplacements = getNameReplacements(variable.name, options);
|
604 |
|
605 | if (variableReplacements.total === 0) {
|
606 | return;
|
607 | }
|
608 |
|
609 | const scopes = variable.references.map(reference => reference.from).concat(variable.scope);
|
610 | variableReplacements.samples = variableReplacements.samples.map(
|
611 | name => avoidCapture(name, scopes, ecmaVersion, isSafeName)
|
612 | );
|
613 |
|
614 | const problem = {
|
615 | node: definition.name,
|
616 | message: formatMessage(definition.name.name, variableReplacements, 'variable')
|
617 | };
|
618 |
|
619 | if (variableReplacements.total === 1 && shouldFix(variable)) {
|
620 | const [replacement] = variableReplacements.samples;
|
621 |
|
622 | for (const scope of scopes) {
|
623 | if (!scopeToNamesGeneratedByFixer.has(scope)) {
|
624 | scopeToNamesGeneratedByFixer.set(scope, new Set());
|
625 | }
|
626 |
|
627 | const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
|
628 | generatedNames.add(replacement);
|
629 | }
|
630 |
|
631 | problem.fix = function * (fixer) {
|
632 | for (const identifier of getVariableIdentifiers(variable)) {
|
633 | yield renameIdentifier(identifier, replacement, fixer, sourceCode);
|
634 | }
|
635 | };
|
636 | }
|
637 |
|
638 | context.report(problem);
|
639 | };
|
640 |
|
641 | const checkVariables = scope => {
|
642 | scope.variables.forEach(variable => checkPossiblyWeirdClassVariable(variable));
|
643 | };
|
644 |
|
645 | const checkChildScopes = scope => {
|
646 | scope.childScopes.forEach(scope => checkScope(scope));
|
647 | };
|
648 |
|
649 | const checkScope = scope => {
|
650 | checkVariables(scope);
|
651 |
|
652 | return checkChildScopes(scope);
|
653 | };
|
654 |
|
655 | return {
|
656 | Identifier(node) {
|
657 | if (!options.checkProperties) {
|
658 | return;
|
659 | }
|
660 |
|
661 | if (node.name === '__proto__') {
|
662 | return;
|
663 | }
|
664 |
|
665 | const identifierReplacements = getNameReplacements(node.name, options);
|
666 |
|
667 | if (identifierReplacements.total === 0) {
|
668 | return;
|
669 | }
|
670 |
|
671 | if (!shouldReportIdentifierAsProperty(node)) {
|
672 | return;
|
673 | }
|
674 |
|
675 | const problem = {
|
676 | node,
|
677 | message: formatMessage(node.name, identifierReplacements, 'property')
|
678 | };
|
679 |
|
680 | context.report(problem);
|
681 | },
|
682 |
|
683 | Program(node) {
|
684 | if (!options.checkFilenames) {
|
685 | return;
|
686 | }
|
687 |
|
688 | if (
|
689 | filenameWithExtension === '<input>' ||
|
690 | filenameWithExtension === '<text>'
|
691 | ) {
|
692 | return;
|
693 | }
|
694 |
|
695 | const extension = path.extname(filenameWithExtension);
|
696 | const filename = path.basename(filenameWithExtension, extension);
|
697 |
|
698 | const filenameReplacements = getNameReplacements(filename, options);
|
699 |
|
700 | if (filenameReplacements.total === 0) {
|
701 | return;
|
702 | }
|
703 |
|
704 | filenameReplacements.samples = filenameReplacements.samples.map(replacement => `${replacement}${extension}`);
|
705 |
|
706 | context.report({
|
707 | node,
|
708 | message: formatMessage(filenameWithExtension, filenameReplacements, 'filename')
|
709 | });
|
710 | },
|
711 |
|
712 | 'Program:exit'() {
|
713 | if (!options.checkVariables) {
|
714 | return;
|
715 | }
|
716 |
|
717 | checkScope(context.getScope());
|
718 | }
|
719 | };
|
720 | };
|
721 |
|
722 | const schema = [
|
723 | {
|
724 | type: 'object',
|
725 | properties: {
|
726 | checkProperties: {
|
727 | type: 'boolean'
|
728 | },
|
729 | checkVariables: {
|
730 | type: 'boolean'
|
731 | },
|
732 | checkDefaultAndNamespaceImports: {
|
733 | type: [
|
734 | 'boolean',
|
735 | 'string'
|
736 | ],
|
737 | pattern: 'internal'
|
738 | },
|
739 | checkShorthandImports: {
|
740 | type: [
|
741 | 'boolean',
|
742 | 'string'
|
743 | ],
|
744 | pattern: 'internal'
|
745 | },
|
746 | checkShorthandProperties: {
|
747 | type: 'boolean'
|
748 | },
|
749 | checkFilenames: {
|
750 | type: 'boolean'
|
751 | },
|
752 | extendDefaultReplacements: {
|
753 | type: 'boolean'
|
754 | },
|
755 | replacements: {
|
756 | $ref: '#/items/0/definitions/abbreviations'
|
757 | },
|
758 | extendDefaultWhitelist: {
|
759 | type: 'boolean'
|
760 | },
|
761 | whitelist: {
|
762 | $ref: '#/items/0/definitions/booleanObject'
|
763 | }
|
764 | },
|
765 | additionalProperties: false,
|
766 | definitions: {
|
767 | abbreviations: {
|
768 | type: 'object',
|
769 | additionalProperties: {
|
770 | $ref: '#/items/0/definitions/replacements'
|
771 | }
|
772 | },
|
773 | replacements: {
|
774 | anyOf: [
|
775 | {
|
776 | enum: [
|
777 | false
|
778 | ]
|
779 | },
|
780 | {
|
781 | $ref: '#/items/0/definitions/booleanObject'
|
782 | }
|
783 | ]
|
784 | },
|
785 | booleanObject: {
|
786 | type: 'object',
|
787 | additionalProperties: {
|
788 | type: 'boolean'
|
789 | }
|
790 | }
|
791 | }
|
792 | }
|
793 | ];
|
794 |
|
795 | module.exports = {
|
796 | create,
|
797 | meta: {
|
798 | type: 'suggestion',
|
799 | docs: {
|
800 | url: getDocumentationUrl(__filename)
|
801 | },
|
802 | fixable: 'code',
|
803 | schema
|
804 | }
|
805 | };
|