• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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