UNPKG

17.2 kBJavaScriptView Raw
1'use strict';
2const path = require('path');
3const astUtils = require('eslint-ast-utils');
4const {defaultsDeep, upperFirst, lowerFirst} = require('lodash');
5
6const getDocumentationUrl = require('./utils/get-documentation-url');
7const avoidCapture = require('./utils/avoid-capture');
8const cartesianProductSamples = require('./utils/cartesian-product-samples');
9const isShorthandPropertyIdentifier = require('./utils/is-shorthand-property-identifier');
10const isShorthandImportIdentifier = require('./utils/is-shorthand-import-identifier');
11const getVariableIdentifiers = require('./utils/get-variable-identifiers');
12const renameIdentifier = require('./utils/rename-identifier');
13
14const isUpperCase = string => string === string.toUpperCase();
15const isUpperFirst = string => isUpperCase(string[0]);
16
17// Keep this alphabetically sorted for easier maintenance
18const 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
201const defaultWhitelist = {
202 // React PropTypes
203 // https://reactjs.org/docs/typechecking-with-proptypes.html
204 propTypes: true,
205 // React.Component Class property
206 // https://reactjs.org/docs/react-component.html#defaultprops
207 defaultProps: true,
208 // React.Component static method
209 // https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
210 getDerivedStateFromProps: true,
211 // Ember class name
212 // https://api.emberjs.com/ember/3.10/classes/Ember.EmberENV/properties
213 EmberENV: true,
214 // `package.json` field
215 // https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file
216 devDependencies: true,
217 // Jest configuration
218 // https://jestjs.io/docs/en/configuration#setupfilesafterenv-array
219 setupFilesAfterEnv: true,
220 // Next.js function
221 // https://nextjs.org/learn/basics/fetching-data-for-pages
222 getInitialProps: true
223};
224
225const 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
269const getWordReplacements = (word, {replacements, whitelist}) => {
270 // Skip constants and whitelist
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
290const getNameReplacements = (name, options, limit = 3) => {
291 const {whitelist} = options;
292
293 // Skip constants and whitelist
294 if (isUpperCase(name) || whitelist.get(name)) {
295 return {total: 0};
296 }
297
298 // Find exact replacements
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 // Split words
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 // No replacements for any word
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
339const anotherNameMessage = 'A more descriptive name will do too.';
340
341const 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
366const 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
394const shouldFix = variable => {
395 return !getVariableIdentifiers(variable).some(identifier => isExportedIdentifier(identifier));
396};
397
398const 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
433const 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
443const 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 && // Shorthand properties are reported and fixed as variables
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 // Same as shorthand properties above
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
491const 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
506const 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 // A `class` declaration produces two variables in two scopes:
513 // the inner class scope, and the outer one (whereever the class is declared).
514 // This map holds the outer ones to be later processed when the inner one is encountered.
515 // For why this is not a eslint issue see https://github.com/eslint/eslint-scope/issues/48#issuecomment-464358754
516 const identifierToOuterClassVariable = new WeakMap();
517
518 const checkPossiblyWeirdClassVariable = variable => {
519 if (isClassVariable(variable)) {
520 if (variable.scope.type === 'class') { // The inner class variable
521 const [definition] = variable.defs;
522 const outerClassVariable = identifierToOuterClassVariable.get(definition.name);
523
524 if (!outerClassVariable) {
525 return checkVariable(variable);
526 }
527
528 // Create a normal-looking variable (like a `var` or a `function`)
529 // For which a single `variable` holds all references, unline with `class`
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 // Call the common checker with the newly forged normalized class variable
539 return checkVariable(combinedReferencesVariable);
540 }
541
542 // The outer class variable, we save it for later, when it's inner counterpart is encountered
543 const [definition] = variable.defs;
544 identifierToOuterClassVariable.set(definition.name, variable);
545
546 return;
547 }
548
549 return checkVariable(variable);
550 };
551
552 // Holds a map from a `Scope` to a `Set` of new variable names generated by our fixer.
553 // Used to avoid generating duplicate names, see for instance `let errCb, errorCb` test.
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
718const 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
791module.exports = {
792 create,
793 meta: {
794 type: 'suggestion',
795 docs: {
796 url: getDocumentationUrl(__filename)
797 },
798 fixable: 'code',
799 schema
800 }
801};