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