1import { 2 AST_NODE_TYPES, 3 JSONSchema, 4 TSESLint, 5 TSESTree, 6} from '@typescript-eslint/experimental-utils'; 7import * as ts from 'typescript'; 8import * as util from '../util'; 9 10type MessageIds = 11 | 'unexpectedUnderscore' 12 | 'missingUnderscore' 13 | 'missingAffix' 14 | 'satisfyCustom' 15 | 'doesNotMatchFormat' 16 | 'doesNotMatchFormatTrimmed'; 17 18// #region Options Type Config 19 20enum PredefinedFormats { 21 camelCase = 1 << 0, 22 strictCamelCase = 1 << 1, 23 PascalCase = 1 << 2, 24 StrictPascalCase = 1 << 3, 25 snake_case = 1 << 4, 26 UPPER_CASE = 1 << 5, 27} 28type PredefinedFormatsString = keyof typeof PredefinedFormats; 29 30enum UnderscoreOptions { 31 forbid = 1 << 0, 32 allow = 1 << 1, 33 require = 1 << 2, 34} 35type UnderscoreOptionsString = keyof typeof UnderscoreOptions; 36 37enum Selectors { 38 // variableLike 39 variable = 1 << 0, 40 function = 1 << 1, 41 parameter = 1 << 2, 42 43 // memberLike 44 property = 1 << 3, 45 parameterProperty = 1 << 4, 46 method = 1 << 5, 47 accessor = 1 << 6, 48 enumMember = 1 << 7, 49 50 // typeLike 51 class = 1 << 8, 52 interface = 1 << 9, 53 typeAlias = 1 << 10, 54 enum = 1 << 11, 55 typeParameter = 1 << 12, 56} 57type SelectorsString = keyof typeof Selectors; 58const SELECTOR_COUNT = util.getEnumNames(Selectors).length; 59 60enum MetaSelectors { 61 default = -1, 62 variableLike = 0 | 63 Selectors.variable | 64 Selectors.function | 65 Selectors.parameter, 66 memberLike = 0 | 67 Selectors.property | 68 Selectors.parameterProperty | 69 Selectors.enumMember | 70 Selectors.method | 71 Selectors.accessor, 72 typeLike = 0 | 73 Selectors.class | 74 Selectors.interface | 75 Selectors.typeAlias | 76 Selectors.enum | 77 Selectors.typeParameter, 78} 79type MetaSelectorsString = keyof typeof MetaSelectors; 80type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString; 81 82enum Modifiers { 83 const = 1 << 0, 84 readonly = 1 << 1, 85 static = 1 << 2, 86 public = 1 << 3, 87 protected = 1 << 4, 88 private = 1 << 5, 89 abstract = 1 << 6, 90} 91type ModifiersString = keyof typeof Modifiers; 92 93enum TypeModifiers { 94 boolean = 1 << 10, 95 string = 1 << 11, 96 number = 1 << 12, 97 function = 1 << 13, 98 array = 1 << 14, 99} 100type TypeModifiersString = keyof typeof TypeModifiers; 101 102interface Selector { 103 // format options 104 format: PredefinedFormatsString[] | null; 105 custom?: { 106 regex: string; 107 match: boolean; 108 }; 109 leadingUnderscore?: UnderscoreOptionsString; 110 trailingUnderscore?: UnderscoreOptionsString; 111 prefix?: string[]; 112 suffix?: string[]; 113 // selector options 114 selector: 115 | IndividualAndMetaSelectorsString 116 | IndividualAndMetaSelectorsString[]; 117 modifiers?: ModifiersString[]; 118 types?: TypeModifiersString[]; 119 filter?: 120 | string 121 | { 122 regex: string; 123 match: boolean; 124 }; 125} 126interface NormalizedSelector { 127 // format options 128 format: PredefinedFormats[] | null; 129 custom: { 130 regex: RegExp; 131 match: boolean; 132 } | null; 133 leadingUnderscore: UnderscoreOptions | null; 134 trailingUnderscore: UnderscoreOptions | null; 135 prefix: string[] | null; 136 suffix: string[] | null; 137 // selector options 138 selector: Selectors | MetaSelectors; 139 modifiers: Modifiers[] | null; 140 types: TypeModifiers[] | null; 141 filter: { 142 regex: RegExp; 143 match: boolean; 144 } | null; 145 // calculated ordering weight based on modifiers 146 modifierWeight: number; 147} 148 149// Note that this intentionally does not strictly type the modifiers/types properties. 150// This is because doing so creates a huge headache, as the rule's code doesn't need to care. 151// The JSON Schema strictly types these properties, so we know the user won't input invalid config. 152type Options = Selector[]; 153 154// #endregion Options Type Config 155 156// #region Schema Config 157 158const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { 159 type: 'string', 160 enum: util.getEnumNames(UnderscoreOptions), 161}; 162const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { 163 type: 'array', 164 items: { 165 type: 'string', 166 minLength: 1, 167 }, 168 additionalItems: false, 169}; 170const MATCH_REGEX_SCHEMA: JSONSchema.JSONSchema4 = { 171 type: 'object', 172 properties: { 173 match: { type: 'boolean' }, 174 regex: { type: 'string' }, 175 }, 176 required: ['match', 'regex'], 177}; 178type JSONSchemaProperties = Record<string, JSONSchema.JSONSchema4>; 179const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { 180 format: { 181 oneOf: [ 182 { 183 type: 'array', 184 items: { 185 type: 'string', 186 enum: util.getEnumNames(PredefinedFormats), 187 }, 188 additionalItems: false, 189 }, 190 { 191 type: 'null', 192 }, 193 ], 194 }, 195 custom: MATCH_REGEX_SCHEMA, 196 leadingUnderscore: UNDERSCORE_SCHEMA, 197 trailingUnderscore: UNDERSCORE_SCHEMA, 198 prefix: PREFIX_SUFFIX_SCHEMA, 199 suffix: PREFIX_SUFFIX_SCHEMA, 200}; 201function selectorSchema( 202 selectorString: IndividualAndMetaSelectorsString, 203 allowType: boolean, 204 modifiers?: ModifiersString[], 205): JSONSchema.JSONSchema4[] { 206 const selector: JSONSchemaProperties = { 207 filter: { 208 oneOf: [ 209 { 210 type: 'string', 211 minLength: 1, 212 }, 213 MATCH_REGEX_SCHEMA, 214 ], 215 }, 216 selector: { 217 type: 'string', 218 enum: [selectorString], 219 }, 220 }; 221 if (modifiers && modifiers.length > 0) { 222 selector.modifiers = { 223 type: 'array', 224 items: { 225 type: 'string', 226 enum: modifiers, 227 }, 228 additionalItems: false, 229 }; 230 } 231 if (allowType) { 232 selector.types = { 233 type: 'array', 234 items: { 235 type: 'string', 236 enum: util.getEnumNames(TypeModifiers), 237 }, 238 additionalItems: false, 239 }; 240 } 241 242 return [ 243 { 244 type: 'object', 245 properties: { 246 ...FORMAT_OPTIONS_PROPERTIES, 247 ...selector, 248 }, 249 required: ['selector', 'format'], 250 additionalProperties: false, 251 }, 252 ]; 253} 254 255function selectorsSchema(): JSONSchema.JSONSchema4 { 256 return { 257 type: 'object', 258 properties: { 259 ...FORMAT_OPTIONS_PROPERTIES, 260 ...{ 261 filter: { 262 oneOf: [ 263 { 264 type: 'string', 265 minLength: 1, 266 }, 267 MATCH_REGEX_SCHEMA, 268 ], 269 }, 270 selector: { 271 type: 'array', 272 items: { 273 type: 'string', 274 enum: [ 275 ...util.getEnumNames(MetaSelectors), 276 ...util.getEnumNames(Selectors), 277 ], 278 }, 279 additionalItems: false, 280 }, 281 modifiers: { 282 type: 'array', 283 items: { 284 type: 'string', 285 enum: util.getEnumNames(Modifiers), 286 }, 287 additionalItems: false, 288 }, 289 types: { 290 type: 'array', 291 items: { 292 type: 'string', 293 enum: util.getEnumNames(TypeModifiers), 294 }, 295 additionalItems: false, 296 }, 297 }, 298 }, 299 required: ['selector', 'format'], 300 additionalProperties: false, 301 }; 302} 303 304const SCHEMA: JSONSchema.JSONSchema4 = { 305 type: 'array', 306 items: { 307 oneOf: [ 308 selectorsSchema(), 309 ...selectorSchema('default', false, util.getEnumNames(Modifiers)), 310 311 ...selectorSchema('variableLike', false), 312 ...selectorSchema('variable', true, ['const']), 313 ...selectorSchema('function', false), 314 ...selectorSchema('parameter', true), 315 316 ...selectorSchema('memberLike', false, [ 317 'private', 318 'protected', 319 'public', 320 'static', 321 'readonly', 322 'abstract', 323 ]), 324 ...selectorSchema('property', true, [ 325 'private', 326 'protected', 327 'public', 328 'static', 329 'readonly', 330 'abstract', 331 ]), 332 ...selectorSchema('parameterProperty', true, [ 333 'private', 334 'protected', 335 'public', 336 'readonly', 337 ]), 338 ...selectorSchema('method', false, [ 339 'private', 340 'protected', 341 'public', 342 'static', 343 'abstract', 344 ]), 345 ...selectorSchema('accessor', true, [ 346 'private', 347 'protected', 348 'public', 349 'static', 350 'abstract', 351 ]), 352 ...selectorSchema('enumMember', false), 353 354 ...selectorSchema('typeLike', false, ['abstract']), 355 ...selectorSchema('class', false, ['abstract']), 356 ...selectorSchema('interface', false), 357 ...selectorSchema('typeAlias', false), 358 ...selectorSchema('enum', false), 359 ...selectorSchema('typeParameter', false), 360 ], 361 }, 362 additionalItems: false, 363}; 364 365// #endregion Schema Config 366 367// This essentially mirrors ESLint's `camelcase` rule 368// note that that rule ignores leading and trailing underscores and only checks those in the middle of a variable name 369const defaultCamelCaseAllTheThingsConfig: Options = [ 370 { 371 selector: 'default', 372 format: ['camelCase'], 373 leadingUnderscore: 'allow', 374 trailingUnderscore: 'allow', 375 }, 376 377 { 378 selector: 'variable', 379 format: ['camelCase', 'UPPER_CASE'], 380 leadingUnderscore: 'allow', 381 trailingUnderscore: 'allow', 382 }, 383 384 { 385 selector: 'typeLike', 386 format: ['PascalCase'], 387 }, 388]; 389 390export default util.createRule<Options, MessageIds>({ 391 name: 'naming-convention', 392 meta: { 393 docs: { 394 category: 'Variables', 395 description: 396 'Enforces naming conventions for everything across a codebase', 397 recommended: false, 398 // technically only requires type checking if the user uses "type" modifiers 399 requiresTypeChecking: true, 400 }, 401 type: 'suggestion', 402 messages: { 403 unexpectedUnderscore: 404 '{{type}} name `{{name}}` must not have a {{position}} underscore.', 405 missingUnderscore: 406 '{{type}} name `{{name}}` must have a {{position}} underscore.', 407 missingAffix: 408 '{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}', 409 satisfyCustom: 410 '{{type}} name `{{name}}` must {{regexMatch}} the RegExp: {{regex}}', 411 doesNotMatchFormat: 412 '{{type}} name `{{name}}` must match one of the following formats: {{formats}}', 413 doesNotMatchFormatTrimmed: 414 '{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}', 415 }, 416 schema: SCHEMA, 417 }, 418 defaultOptions: defaultCamelCaseAllTheThingsConfig, 419 create(contextWithoutDefaults) { 420 const context: Context = 421 contextWithoutDefaults.options && 422 contextWithoutDefaults.options.length > 0 423 ? contextWithoutDefaults 424 : // only apply the defaults when the user provides no config 425 Object.setPrototypeOf( 426 { 427 options: defaultCamelCaseAllTheThingsConfig, 428 }, 429 contextWithoutDefaults, 430 ); 431 432 const validators = parseOptions(context); 433 434 function handleMember( 435 validator: ValidatorFunction | null, 436 node: 437 | TSESTree.PropertyNonComputedName 438 | TSESTree.ClassPropertyNonComputedName 439 | TSESTree.TSAbstractClassPropertyNonComputedName 440 | TSESTree.TSPropertySignatureNonComputedName 441 | TSESTree.MethodDefinitionNonComputedName 442 | TSESTree.TSAbstractMethodDefinitionNonComputedName 443 | TSESTree.TSMethodSignatureNonComputedName, 444 modifiers: Set<Modifiers>, 445 ): void { 446 if (!validator) { 447 return; 448 } 449 450 const key = node.key; 451 validator(key, modifiers); 452 } 453 454 function getMemberModifiers( 455 node: 456 | TSESTree.ClassProperty 457 | TSESTree.TSAbstractClassProperty 458 | TSESTree.MethodDefinition 459 | TSESTree.TSAbstractMethodDefinition 460 | TSESTree.TSParameterProperty, 461 ): Set<Modifiers> { 462 const modifiers = new Set<Modifiers>(); 463 if (node.accessibility) { 464 modifiers.add(Modifiers[node.accessibility]); 465 } else { 466 modifiers.add(Modifiers.public); 467 } 468 if (node.static) { 469 modifiers.add(Modifiers.static); 470 } 471 if ('readonly' in node && node.readonly) { 472 modifiers.add(Modifiers.readonly); 473 } 474 if ( 475 node.type === AST_NODE_TYPES.TSAbstractClassProperty || 476 node.type === AST_NODE_TYPES.TSAbstractMethodDefinition 477 ) { 478 modifiers.add(Modifiers.abstract); 479 } 480 481 return modifiers; 482 } 483 484 return { 485 // #region variable 486 487 VariableDeclarator(node: TSESTree.VariableDeclarator): void { 488 const validator = validators.variable; 489 if (!validator) { 490 return; 491 } 492 493 const identifiers: TSESTree.Identifier[] = []; 494 getIdentifiersFromPattern(node.id, identifiers); 495 496 const modifiers = new Set<Modifiers>(); 497 const parent = node.parent; 498 if ( 499 parent && 500 parent.type === AST_NODE_TYPES.VariableDeclaration && 501 parent.kind === 'const' 502 ) { 503 modifiers.add(Modifiers.const); 504 } 505 506 identifiers.forEach(i => { 507 validator(i, modifiers); 508 }); 509 }, 510 511 // #endregion 512 513 // #region function 514 515 'FunctionDeclaration, TSDeclareFunction, FunctionExpression'( 516 node: 517 | TSESTree.FunctionDeclaration 518 | TSESTree.TSDeclareFunction 519 | TSESTree.FunctionExpression, 520 ): void { 521 const validator = validators.function; 522 if (!validator || node.id === null) { 523 return; 524 } 525 526 validator(node.id); 527 }, 528 529 // #endregion function 530 531 // #region parameter 532 'FunctionDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, FunctionExpression, ArrowFunctionExpression'( 533 node: 534 | TSESTree.FunctionDeclaration 535 | TSESTree.TSDeclareFunction 536 | TSESTree.TSEmptyBodyFunctionExpression 537 | TSESTree.FunctionExpression 538 | TSESTree.ArrowFunctionExpression, 539 ): void { 540 const validator = validators.parameter; 541 if (!validator) { 542 return; 543 } 544 545 node.params.forEach(param => { 546 if (param.type === AST_NODE_TYPES.TSParameterProperty) { 547 return; 548 } 549 550 const identifiers: TSESTree.Identifier[] = []; 551 getIdentifiersFromPattern(param, identifiers); 552 553 identifiers.forEach(i => { 554 validator(i); 555 }); 556 }); 557 }, 558 559 // #endregion parameter 560 561 // #region parameterProperty 562 563 TSParameterProperty(node): void { 564 const validator = validators.parameterProperty; 565 if (!validator) { 566 return; 567 } 568 569 const modifiers = getMemberModifiers(node); 570 571 const identifiers: TSESTree.Identifier[] = []; 572 getIdentifiersFromPattern(node.parameter, identifiers); 573 574 identifiers.forEach(i => { 575 validator(i, modifiers); 576 }); 577 }, 578 579 // #endregion parameterProperty 580 581 // #region property 582 583 ':not(ObjectPattern) > Property[computed = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( 584 node: TSESTree.PropertyNonComputedName, 585 ): void { 586 const modifiers = new Set<Modifiers>([Modifiers.public]); 587 handleMember(validators.property, node, modifiers); 588 }, 589 590 ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( 591 node: 592 | TSESTree.ClassPropertyNonComputedName 593 | TSESTree.TSAbstractClassPropertyNonComputedName, 594 ): void { 595 const modifiers = getMemberModifiers(node); 596 handleMember(validators.property, node, modifiers); 597 }, 598 599 'TSPropertySignature[computed = false]'( 600 node: TSESTree.TSPropertySignatureNonComputedName, 601 ): void { 602 const modifiers = new Set<Modifiers>([Modifiers.public]); 603 if (node.readonly) { 604 modifiers.add(Modifiers.readonly); 605 } 606 607 handleMember(validators.property, node, modifiers); 608 }, 609 610 // #endregion property 611 612 // #region method 613 614 [[ 615 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', 616 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', 617 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', 618 'TSMethodSignature[computed = false]', 619 ].join(', ')]( 620 node: 621 | TSESTree.PropertyNonComputedName 622 | TSESTree.TSMethodSignatureNonComputedName, 623 ): void { 624 const modifiers = new Set<Modifiers>([Modifiers.public]); 625 handleMember(validators.method, node, modifiers); 626 }, 627 628 [[ 629 ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "ArrowFunctionExpression"]', 630 ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "FunctionExpression"]', 631 ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', 632 ':matches(MethodDefinition, TSAbstractMethodDefinition)[computed = false][kind = "method"]', 633 ].join(', ')]( 634 node: 635 | TSESTree.ClassPropertyNonComputedName 636 | TSESTree.TSAbstractClassPropertyNonComputedName 637 | TSESTree.MethodDefinitionNonComputedName 638 | TSESTree.TSAbstractMethodDefinitionNonComputedName, 639 ): void { 640 const modifiers = getMemberModifiers(node); 641 handleMember(validators.method, node, modifiers); 642 }, 643 644 // #endregion method 645 646 // #region accessor 647 648 'Property[computed = false]:matches([kind = "get"], [kind = "set"])'( 649 node: TSESTree.PropertyNonComputedName, 650 ): void { 651 const modifiers = new Set<Modifiers>([Modifiers.public]); 652 handleMember(validators.accessor, node, modifiers); 653 }, 654 655 'MethodDefinition[computed = false]:matches([kind = "get"], [kind = "set"])'( 656 node: TSESTree.MethodDefinitionNonComputedName, 657 ): void { 658 const modifiers = getMemberModifiers(node); 659 handleMember(validators.accessor, node, modifiers); 660 }, 661 662 // #endregion accessor 663 664 // #region enumMember 665 666 // computed is optional, so can't do [computed = false] 667 'TSEnumMember[computed != true]'( 668 node: TSESTree.TSEnumMemberNonComputedName, 669 ): void { 670 const validator = validators.enumMember; 671 if (!validator) { 672 return; 673 } 674 675 const id = node.id; 676 validator(id); 677 }, 678 679 // #endregion enumMember 680 681 // #region class 682 683 'ClassDeclaration, ClassExpression'( 684 node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, 685 ): void { 686 const validator = validators.class; 687 if (!validator) { 688 return; 689 } 690 691 const id = node.id; 692 if (id === null) { 693 return; 694 } 695 696 const modifiers = new Set<Modifiers>(); 697 if (node.abstract) { 698 modifiers.add(Modifiers.abstract); 699 } 700 701 validator(id, modifiers); 702 }, 703 704 // #endregion class 705 706 // #region interface 707 708 TSInterfaceDeclaration(node): void { 709 const validator = validators.interface; 710 if (!validator) { 711 return; 712 } 713 714 validator(node.id); 715 }, 716 717 // #endregion interface 718 719 // #region typeAlias 720 721 TSTypeAliasDeclaration(node): void { 722 const validator = validators.typeAlias; 723 if (!validator) { 724 return; 725 } 726 727 validator(node.id); 728 }, 729 730 // #endregion typeAlias 731 732 // #region enum 733 734 TSEnumDeclaration(node): void { 735 const validator = validators.enum; 736 if (!validator) { 737 return; 738 } 739 740 validator(node.id); 741 }, 742 743 // #endregion enum 744 745 // #region typeParameter 746 747 'TSTypeParameterDeclaration > TSTypeParameter'( 748 node: TSESTree.TSTypeParameter, 749 ): void { 750 const validator = validators.typeParameter; 751 if (!validator) { 752 return; 753 } 754 755 validator(node.name); 756 }, 757 758 // #endregion typeParameter 759 }; 760 }, 761}); 762 763function getIdentifiersFromPattern( 764 pattern: TSESTree.DestructuringPattern, 765 identifiers: TSESTree.Identifier[], 766): void { 767 switch (pattern.type) { 768 case AST_NODE_TYPES.Identifier: 769 identifiers.push(pattern); 770 break; 771 772 case AST_NODE_TYPES.ArrayPattern: 773 pattern.elements.forEach(element => { 774 if (element !== null) { 775 getIdentifiersFromPattern(element, identifiers); 776 } 777 }); 778 break; 779 780 case AST_NODE_TYPES.ObjectPattern: 781 pattern.properties.forEach(property => { 782 if (property.type === AST_NODE_TYPES.RestElement) { 783 getIdentifiersFromPattern(property, identifiers); 784 } else { 785 // this is a bit weird, but it's because ESTree doesn't have a new node type 786 // for object destructuring properties - it just reuses Property... 787 // https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#L206-L211 788 // However, the parser guarantees this is safe (and there is error handling) 789 getIdentifiersFromPattern( 790 property.value as TSESTree.DestructuringPattern, 791 identifiers, 792 ); 793 } 794 }); 795 break; 796 797 case AST_NODE_TYPES.RestElement: 798 getIdentifiersFromPattern(pattern.argument, identifiers); 799 break; 800 801 case AST_NODE_TYPES.AssignmentPattern: 802 getIdentifiersFromPattern(pattern.left, identifiers); 803 break; 804 805 case AST_NODE_TYPES.MemberExpression: 806 // ignore member expressions, as the everything must already be defined 807 break; 808 809 default: 810 // https://github.com/typescript-eslint/typescript-eslint/issues/1282 811 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 812 throw new Error(`Unexpected pattern type ${pattern!.type}`); 813 } 814} 815 816type ValidatorFunction = ( 817 node: TSESTree.Identifier | TSESTree.Literal, 818 modifiers?: Set<Modifiers>, 819) => void; 820type ParsedOptions = Record<SelectorsString, null | ValidatorFunction>; 821type Context = Readonly<TSESLint.RuleContext<MessageIds, Options>>; 822 823function parseOptions(context: Context): ParsedOptions { 824 const normalizedOptions = context.options 825 .map(opt => normalizeOption(opt)) 826 .reduce((acc, val) => acc.concat(val), []); 827 return util.getEnumNames(Selectors).reduce((acc, k) => { 828 acc[k] = createValidator(k, context, normalizedOptions); 829 return acc; 830 }, {} as ParsedOptions); 831} 832 833function createValidator( 834 type: SelectorsString, 835 context: Context, 836 allConfigs: NormalizedSelector[], 837): (node: TSESTree.Identifier | TSESTree.Literal) => void { 838 // make sure the "highest priority" configs are checked first 839 const selectorType = Selectors[type]; 840 const configs = allConfigs 841 // gather all of the applicable selectors 842 .filter( 843 c => 844 (c.selector & selectorType) !== 0 || 845 c.selector === MetaSelectors.default, 846 ) 847 .sort((a, b) => { 848 if (a.selector === b.selector) { 849 // in the event of the same selector, order by modifier weight 850 // sort descending - the type modifiers are "more important" 851 return b.modifierWeight - a.modifierWeight; 852 } 853 854 /* 855 meta selectors will always be larger numbers than the normal selectors they contain, as they are the sum of all 856 of the selectors that they contain. 857 to give normal selectors a higher priority, shift them all SELECTOR_COUNT bits to the left before comparison, so 858 they are instead always guaranteed to be larger than the meta selectors. 859 */ 860 const aSelector = isMetaSelector(a.selector) 861 ? a.selector 862 : a.selector << SELECTOR_COUNT; 863 const bSelector = isMetaSelector(b.selector) 864 ? b.selector 865 : b.selector << SELECTOR_COUNT; 866 867 // sort descending - the meta selectors are "least important" 868 return bSelector - aSelector; 869 }); 870 871 return ( 872 node: TSESTree.Identifier | TSESTree.Literal, 873 modifiers: Set<Modifiers> = new Set<Modifiers>(), 874 ): void => { 875 const originalName = 876 node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; 877 878 // return will break the loop and stop checking configs 879 // it is only used when the name is known to have failed or succeeded a config. 880 for (const config of configs) { 881 if (config.filter?.regex.test(originalName) !== config.filter?.match) { 882 // name does not match the filter 883 continue; 884 } 885 886 if (config.modifiers?.some(modifier => !modifiers.has(modifier))) { 887 // does not have the required modifiers 888 continue; 889 } 890 891 if (!isCorrectType(node, config, context)) { 892 // is not the correct type 893 continue; 894 } 895 896 let name: string | null = originalName; 897 898 name = validateUnderscore('leading', config, name, node, originalName); 899 if (name === null) { 900 // fail 901 return; 902 } 903 904 name = validateUnderscore('trailing', config, name, node, originalName); 905 if (name === null) { 906 // fail 907 return; 908 } 909 910 name = validateAffix('prefix', config, name, node, originalName); 911 if (name === null) { 912 // fail 913 return; 914 } 915 916 name = validateAffix('suffix', config, name, node, originalName); 917 if (name === null) { 918 // fail 919 return; 920 } 921 922 if (!validateCustom(config, name, node, originalName)) { 923 // fail 924 return; 925 } 926 927 if (!validatePredefinedFormat(config, name, node, originalName)) { 928 // fail 929 return; 930 } 931 932 // it's valid for this config, so we don't need to check any more configs 933 return; 934 } 935 }; 936 937 // centralizes the logic for formatting the report data 938 function formatReportData({ 939 affixes, 940 formats, 941 originalName, 942 processedName, 943 position, 944 custom, 945 }: { 946 affixes?: string[]; 947 formats?: PredefinedFormats[]; 948 originalName: string; 949 processedName?: string; 950 position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; 951 custom?: NonNullable<NormalizedSelector['custom']>; 952 }): Record<string, unknown> { 953 return { 954 type: selectorTypeToMessageString(type), 955 name: originalName, 956 processedName, 957 position, 958 affixes: affixes?.join(', '), 959 formats: formats?.map(f => PredefinedFormats[f]).join(', '), 960 regex: custom?.regex?.toString(), 961 regexMatch: 962 custom?.match === true 963 ? 'match' 964 : custom?.match === false 965 ? 'not match' 966 : null, 967 }; 968 } 969 970 /** 971 * @returns the name with the underscore removed, if it is valid according to the specified underscore option, null otherwise 972 */ 973 function validateUnderscore( 974 position: 'leading' | 'trailing', 975 config: NormalizedSelector, 976 name: string, 977 node: TSESTree.Identifier | TSESTree.Literal, 978 originalName: string, 979 ): string | null { 980 const option = 981 position === 'leading' 982 ? config.leadingUnderscore 983 : config.trailingUnderscore; 984 if (!option) { 985 return name; 986 } 987 988 const hasUnderscore = 989 position === 'leading' ? name.startsWith('_') : name.endsWith('_'); 990 const trimUnderscore = 991 position === 'leading' 992 ? (): string => name.slice(1) 993 : (): string => name.slice(0, -1); 994 995 switch (option) { 996 case UnderscoreOptions.allow: 997 // no check - the user doesn't care if it's there or not 998 break; 999 1000 case UnderscoreOptions.forbid: 1001 if (hasUnderscore) { 1002 context.report({ 1003 node, 1004 messageId: 'unexpectedUnderscore', 1005 data: formatReportData({ 1006 originalName, 1007 position, 1008 }), 1009 }); 1010 return null; 1011 } 1012 break; 1013 1014 case UnderscoreOptions.require: 1015 if (!hasUnderscore) { 1016 context.report({ 1017 node, 1018 messageId: 'missingUnderscore', 1019 data: formatReportData({ 1020 originalName, 1021 position, 1022 }), 1023 }); 1024 return null; 1025 } 1026 } 1027 1028 return hasUnderscore ? trimUnderscore() : name; 1029 } 1030 1031 /** 1032 * @returns the name with the affix removed, if it is valid according to the specified affix option, null otherwise 1033 */ 1034 function validateAffix( 1035 position: 'prefix' | 'suffix', 1036 config: NormalizedSelector, 1037 name: string, 1038 node: TSESTree.Identifier | TSESTree.Literal, 1039 originalName: string, 1040 ): string | null { 1041 const affixes = config[position]; 1042 if (!affixes || affixes.length === 0) { 1043 return name; 1044 } 1045 1046 for (const affix of affixes) { 1047 const hasAffix = 1048 position === 'prefix' ? name.startsWith(affix) : name.endsWith(affix); 1049 const trimAffix = 1050 position === 'prefix' 1051 ? (): string => name.slice(affix.length) 1052 : (): string => name.slice(0, -affix.length); 1053 1054 if (hasAffix) { 1055 // matches, so trim it and return 1056 return trimAffix(); 1057 } 1058 } 1059 1060 context.report({ 1061 node, 1062 messageId: 'missingAffix', 1063 data: formatReportData({ 1064 originalName, 1065 position, 1066 affixes, 1067 }), 1068 }); 1069 return null; 1070 } 1071 1072 /** 1073 * @returns true if the name is valid according to the `regex` option, false otherwise 1074 */ 1075 function validateCustom( 1076 config: NormalizedSelector, 1077 name: string, 1078 node: TSESTree.Identifier | TSESTree.Literal, 1079 originalName: string, 1080 ): boolean { 1081 const custom = config.custom; 1082 if (!custom) { 1083 return true; 1084 } 1085 1086 const result = custom.regex.test(name); 1087 if (custom.match && result) { 1088 return true; 1089 } 1090 if (!custom.match && !result) { 1091 return true; 1092 } 1093 1094 context.report({ 1095 node, 1096 messageId: 'satisfyCustom', 1097 data: formatReportData({ 1098 originalName, 1099 custom, 1100 }), 1101 }); 1102 return false; 1103 } 1104 1105 /** 1106 * @returns true if the name is valid according to the `format` option, false otherwise 1107 */ 1108 function validatePredefinedFormat( 1109 config: NormalizedSelector, 1110 name: string, 1111 node: TSESTree.Identifier | TSESTree.Literal, 1112 originalName: string, 1113 ): boolean { 1114 const formats = config.format; 1115 if (formats === null || formats.length === 0) { 1116 return true; 1117 } 1118 1119 for (const format of formats) { 1120 const checker = PredefinedFormatToCheckFunction[format]; 1121 if (checker(name)) { 1122 return true; 1123 } 1124 } 1125 1126 context.report({ 1127 node, 1128 messageId: 1129 originalName === name 1130 ? 'doesNotMatchFormat' 1131 : 'doesNotMatchFormatTrimmed', 1132 data: formatReportData({ 1133 originalName, 1134 processedName: name, 1135 formats, 1136 }), 1137 }); 1138 return false; 1139 } 1140} 1141 1142// #region Predefined Format Functions 1143 1144/* 1145These format functions are taken from `tslint-consistent-codestyle/naming-convention`: 1146https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/rules/namingConventionRule.ts#L603-L645 1147 1148The licence for the code can be viewed here: 1149https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/LICENSE 1150*/ 1151 1152/* 1153Why not regex here? Because it's actually really, really difficult to create a regex to handle 1154all of the unicode cases, and we have many non-english users that use non-english characters. 1155https://gist.github.com/mathiasbynens/6334847 1156*/ 1157 1158function isPascalCase(name: string): boolean { 1159 return ( 1160 name.length === 0 || 1161 (name[0] === name[0].toUpperCase() && !name.includes('_')) 1162 ); 1163} 1164function isStrictPascalCase(name: string): boolean { 1165 return ( 1166 name.length === 0 || 1167 (name[0] === name[0].toUpperCase() && hasStrictCamelHumps(name, true)) 1168 ); 1169} 1170 1171function isCamelCase(name: string): boolean { 1172 return ( 1173 name.length === 0 || 1174 (name[0] === name[0].toLowerCase() && !name.includes('_')) 1175 ); 1176} 1177function isStrictCamelCase(name: string): boolean { 1178 return ( 1179 name.length === 0 || 1180 (name[0] === name[0].toLowerCase() && hasStrictCamelHumps(name, false)) 1181 ); 1182} 1183 1184function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { 1185 function isUppercaseChar(char: string): boolean { 1186 return char === char.toUpperCase() && char !== char.toLowerCase(); 1187 } 1188 1189 if (name.startsWith('_')) { 1190 return false; 1191 } 1192 for (let i = 1; i < name.length; ++i) { 1193 if (name[i] === '_') { 1194 return false; 1195 } 1196 if (isUpper === isUppercaseChar(name[i])) { 1197 if (isUpper) { 1198 return false; 1199 } 1200 } else { 1201 isUpper = !isUpper; 1202 } 1203 } 1204 return true; 1205} 1206 1207function isSnakeCase(name: string): boolean { 1208 return ( 1209 name.length === 0 || 1210 (name === name.toLowerCase() && validateUnderscores(name)) 1211 ); 1212} 1213 1214function isUpperCase(name: string): boolean { 1215 return ( 1216 name.length === 0 || 1217 (name === name.toUpperCase() && validateUnderscores(name)) 1218 ); 1219} 1220 1221/** Check for leading trailing and adjacent underscores */ 1222function validateUnderscores(name: string): boolean { 1223 if (name.startsWith('_')) { 1224 return false; 1225 } 1226 let wasUnderscore = false; 1227 for (let i = 1; i < name.length; ++i) { 1228 if (name[i] === '_') { 1229 if (wasUnderscore) { 1230 return false; 1231 } 1232 wasUnderscore = true; 1233 } else { 1234 wasUnderscore = false; 1235 } 1236 } 1237 return !wasUnderscore; 1238} 1239 1240const PredefinedFormatToCheckFunction: Readonly<Record< 1241 PredefinedFormats, 1242 (name: string) => boolean 1243>> = { 1244 [PredefinedFormats.PascalCase]: isPascalCase, 1245 [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, 1246 [PredefinedFormats.camelCase]: isCamelCase, 1247 [PredefinedFormats.strictCamelCase]: isStrictCamelCase, 1248 [PredefinedFormats.UPPER_CASE]: isUpperCase, 1249 [PredefinedFormats.snake_case]: isSnakeCase, 1250}; 1251 1252// #endregion Predefined Format Functions 1253 1254function selectorTypeToMessageString(selectorType: SelectorsString): string { 1255 const notCamelCase = selectorType.replace(/([A-Z])/g, ' $1'); 1256 return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); 1257} 1258 1259function isMetaSelector( 1260 selector: IndividualAndMetaSelectorsString | Selectors | MetaSelectors, 1261): selector is MetaSelectorsString { 1262 return selector in MetaSelectors; 1263} 1264 1265function normalizeOption(option: Selector): NormalizedSelector[] { 1266 let weight = 0; 1267 option.modifiers?.forEach(mod => { 1268 weight |= Modifiers[mod]; 1269 }); 1270 option.types?.forEach(mod => { 1271 weight |= TypeModifiers[mod]; 1272 }); 1273 1274 // give selectors with a filter the _highest_ priority 1275 if (option.filter) { 1276 weight |= 1 << 30; 1277 } 1278 1279 const normalizedOption = { 1280 // format options 1281 format: option.format ? option.format.map(f => PredefinedFormats[f]) : null, 1282 custom: option.custom 1283 ? { 1284 regex: new RegExp(option.custom.regex, 'u'), 1285 match: option.custom.match, 1286 } 1287 : null, 1288 leadingUnderscore: 1289 option.leadingUnderscore !== undefined 1290 ? UnderscoreOptions[option.leadingUnderscore] 1291 : null, 1292 trailingUnderscore: 1293 option.trailingUnderscore !== undefined 1294 ? UnderscoreOptions[option.trailingUnderscore] 1295 : null, 1296 prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null, 1297 suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null, 1298 modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, 1299 types: option.types?.map(m => TypeModifiers[m]) ?? null, 1300 filter: 1301 option.filter !== undefined 1302 ? typeof option.filter === 'string' 1303 ? { regex: new RegExp(option.filter, 'u'), match: true } 1304 : { 1305 regex: new RegExp(option.filter.regex, 'u'), 1306 match: option.filter.match, 1307 } 1308 : null, 1309 // calculated ordering weight based on modifiers 1310 modifierWeight: weight, 1311 }; 1312 1313 const selectors = Array.isArray(option.selector) 1314 ? option.selector 1315 : [option.selector]; 1316 1317 const selectorsAllowedToHaveTypes: (Selectors | MetaSelectors)[] = [ 1318 Selectors.variable, 1319 Selectors.parameter, 1320 Selectors.property, 1321 Selectors.parameterProperty, 1322 Selectors.accessor, 1323 ]; 1324 1325 const config: NormalizedSelector[] = []; 1326 selectors 1327 .map(selector => 1328 isMetaSelector(selector) ? MetaSelectors[selector] : Selectors[selector], 1329 ) 1330 .forEach(selector => 1331 selectorsAllowedToHaveTypes.includes(selector) 1332 ? config.push({ selector: selector, ...normalizedOption }) 1333 : config.push({ 1334 selector: selector, 1335 ...normalizedOption, 1336 types: null, 1337 }), 1338 ); 1339 1340 return config; 1341} 1342 1343function isCorrectType( 1344 node: TSESTree.Node, 1345 config: NormalizedSelector, 1346 context: Context, 1347): boolean { 1348 if (config.types === null) { 1349 return true; 1350 } 1351 1352 const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); 1353 const checker = program.getTypeChecker(); 1354 const tsNode = esTreeNodeToTSNodeMap.get(node); 1355 const type = checker 1356 .getTypeAtLocation(tsNode) 1357 // remove null and undefined from the type, as we don't care about it here 1358 .getNonNullableType(); 1359 1360 for (const allowedType of config.types) { 1361 switch (allowedType) { 1362 case TypeModifiers.array: 1363 if ( 1364 isAllTypesMatch( 1365 type, 1366 t => checker.isArrayType(t) || checker.isTupleType(t), 1367 ) 1368 ) { 1369 return true; 1370 } 1371 break; 1372 1373 case TypeModifiers.function: 1374 if (isAllTypesMatch(type, t => t.getCallSignatures().length > 0)) { 1375 return true; 1376 } 1377 break; 1378 1379 case TypeModifiers.boolean: 1380 case TypeModifiers.number: 1381 case TypeModifiers.string: { 1382 const typeString = checker.typeToString( 1383 // this will resolve things like true => boolean, 'a' => string and 1 => number 1384 checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), 1385 ); 1386 const allowedTypeString = TypeModifiers[allowedType]; 1387 if (typeString === allowedTypeString) { 1388 return true; 1389 } 1390 break; 1391 } 1392 } 1393 } 1394 1395 return false; 1396} 1397 1398/** 1399 * @returns `true` if the type (or all union types) in the given type return true for the callback 1400 */ 1401function isAllTypesMatch( 1402 type: ts.Type, 1403 cb: (type: ts.Type) => boolean, 1404): boolean { 1405 if (type.isUnion()) { 1406 return type.types.every(t => cb(t)); 1407 } 1408 1409 return cb(type); 1410} 1411 1412export { 1413 MessageIds, 1414 Options, 1415 PredefinedFormatsString, 1416 Selector, 1417 selectorTypeToMessageString, 1418}; 1419