Skip to content

Rule proposal: Type-based naming rule #722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ark120202 opened this issue Jul 18, 2019 · 8 comments · Fixed by #1318
Closed

Rule proposal: Type-based naming rule #722

ark120202 opened this issue Jul 18, 2019 · 8 comments · Fixed by #1318
Labels
enhancement: new plugin rule New rule request for eslint-plugin has pr there is a PR raised to close this package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@ark120202
Copy link

I'd like to have a rule that enforces some strict conventions on identifier names. camelcase also allows PascalCase and with id-match it's impossible to enforce this:

// Valid
const foo = getString();
// Invalid
const Foo = getString();
// Invalid, also conflicts with new-cap
const foo = createClass(); // function createClass(): new (...args: any[]) => any
// Invalid
const foo: React.FC = ...;
// Any name is [in]valid?
const foo = Math.random() < 0.5 ? createClass() : createString();

It also was a limitation for tslint, so there are few relevant issues:

I'm not really sure what kind of configuration fits this rule (or multiple rules?). For me it would be enough to have a stricter version of camelcase that keeps all classes and functions returning JSX.Element in PascalCase and everything else in camelCase, but for some more complex cases it may be worth to have extensible configuration, similar to naming-convention from tslint-consistent-codestyle, which also would cover #515.

@ark120202 ark120202 added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Jul 18, 2019
@bradzacher bradzacher added enhancement: new plugin rule New rule request for eslint-plugin enhancement: plugin rule option New rule option for an existing eslint-plugin rule and removed triage Waiting for team members to take a look labels Jul 18, 2019
@glen-84
Copy link
Contributor

glen-84 commented Nov 20, 2019

(copied from #871)

I was actually thinking of opening an issue the other day to suggest something like id-match, but more powerful in that you could set regular expressions for:

  • class names
  • interface names
  • function names
  • method names
  • variable names
  • field names
  • type names
  • generic type names
  • etc.

Each with optional modifiers (static/instance/abstract, public/protected/private, readonly, etc.), and each with a custom message. Something along the lines of:

"id-naming": ["error", {
    check: [
        {
            tokens: ["function", "method"],
            match: /camelCaseRegexHere/,
            message: "Please, m'kay?"
        },
        {
            tokens: ["private-field"],
            match: /^(?!_)/,
            message: "We don't do that here.",
        },
        {
            tokens: ["private-backing-field"], // #816 :-)
            match: /^_/,
            message: "Okay, sure."
        }
    ]
}]

(without putting too much thought into it)

This rule would then replace:

  • camelcase
  • member-naming
  • id-match
  • generic-type-naming
  • interface-name-prefix
  • class-name-casing
  • etc.

It seems unnecessary to have so many rules when it's all just about naming of identifiers.

@bradzacher
Copy link
Member

bradzacher commented Nov 21, 2019

I was thinking something like this?

type BuiltinNamingSchemes =
  | 'camelCase'
  | 'PascalCase'
  | 'snake_case'
  | 'Upper_Snake_Case'
  | 'CONSTANT_CASE';
  
const BUILTIN_NAMING_SCHEMES: Record<BuiltinNamingSchemes, RegExp> = {
  camelCase: /^[a-z][^_]*$/,
  PascalCase: /^[A-Z][^_]*$/,
  snake_case: /^[a-z][a-zA-Z_]*$/,
  Upper_Snake_Case: /^[A-Z][a-zA-Z]*(_[A-Z][a-zA-Z]*)*$/,
  CONSTANT_CASE: /^[A-Z_]+$/,
};

type MatchBase = {
  match: BuiltinNamingSchemes | BuiltinNamingSchemes[] | string | string[];
};

type Options = [
  {
    variables?: MatchBase & {
      ignoreObjectDestructuring: boolean;
      allowLeadingUnderscore: boolean;
    };
    functions?: {
      functionDeclarationName?: MatchBase;
      functionExpressionName?: MatchBase;
      arrowFunctionExpressionName?: MatchBase;
      arguments?: MatchBase & {
        ignoreObjectDestructuring: boolean;
        allowLeadingUnderscore: boolean;
      };
    };
    objectProperties?: MatchBase;
    class?: {
      className?: MatchBase;
      privateMemberName?: MatchBase;
      protectedMemberName?: MatchBase;
      publicMemberName?: MatchBase;
    };
    interfaces?: MatchBase;
    typeDeclaration?: MatchBase;
    typeParameterDeclaration?: MatchBase;
    objectTypeMember?: MatchBase;
    enums?: {
      enumName?: MatchBase;
      members?: MatchBase;
    };

    // allow the user to specify any esquery selector
    custom?: {
      [selector: string]: MatchBase;
    };
  },
];

Maybe it's too complicated / too flexible?

@bradzacher
Copy link
Member

Thinking about this, part of me wonders if it makes more sense to split this out into a suite of rules, maybe a new plugin.

Just looking at the above config makes me cringe thinking about the implementation complexity.
If we split it out into one rule per "type", then you can decouple things and separate the implementations easier.

@typescript-eslint/eslint-plugin-naming, (which would be used in an eslint config as @typescript-eslint/naming).

{
  "plugins": ["@typescript-eslint/naming"],
  "rules": {
    "@typescript-eslint/naming/class-name": ["error", "PascalCase"],
    "@typescript-eslint/naming/class-member-name": [
      "error",
      "camelCase",
      { "modifier": "private", "match": "camelCase", "prefix": "_" },
    ],
    // ...
  }
}

@glen-84
Copy link
Contributor

glen-84 commented Nov 21, 2019

I was thinking something like this?

That looks pretty good to me. Some random feedback:

  1. As mentioned by the OP in [camelcase] The camelcase rule allows PascalCase #871, we'd like to have stricter camel/pascal case varieties where convertToHTML should instead be convertToHtml.
  2. The snake_case regex doesn't look correct. I don't think a lower-case letter can be followed by an upper-case letter. (ref)
  3. There should maybe also be a lower_snake_case option, where an initial capital letter is not allowed.
  4. Options.functions.arguments should probably be Options.functions.parameters.
  5. It would be great if objectProperties and class.*MemberName had allowLeadingUnderscore and allowLeadingUnderscoreBackingFields options. ([naming-convention] Allow underscore prefix for backing fields #816)
  6. It seems somewhat less flexible than my suggestion – how would you (for example) specify a pattern for static readonly class fields? Or abstract methods, etc.

Maybe it's too complicated / too flexible?

I don't think so, it's possibly even not flexible enough. With solid (conventional) defaults, the user should not have to configure much.

Thinking about this, part of me wonders if it makes more sense to split this out into a suite of rules, maybe a new plugin.

I don't think that it should be necessary to include a plugin for such a basic rule(s).

Just looking at the above config makes me cringe thinking about the implementation complexity.

I can't really comment on implementation complexity (no experience). If a single rule gets too complex, I guess you could have rules like:

  • class-naming
  • variable-naming
  • type-naming
  • interface-naming
  • object-naming
  • enum-naming
  • etc.

... but then you have 6+ rules to configure instead of just one, and you're not able to share configuration – for example, with a single rule, setting variables to camelCase can act as a default for class and object fields as well.

@ark120202
Copy link
Author

@bradzacher Have you checked out naming-convention configuration? I think it should work quite well, being very flexible, yet not requiring too many configuration for simpler cases

@glen-84
Copy link
Contributor

glen-84 commented Nov 21, 2019

@ark120202 That rule looks really good actually, I hadn't seen it before.

@bradzacher bradzacher removed the enhancement: plugin rule option New rule option for an existing eslint-plugin rule label Nov 22, 2019
@bradzacher
Copy link
Member

we'd like to have stricter camel/pascal case varieties where convertToHTML should instead be convertToHtml

The problem is that this is kind of undetectable without false positives or a dictionary of words because there are valid cases where you want to capitals directly next to each other (IFoo, TypeOfACar). Potentially could be handled by banning 3 or more capitals next to one another, but someone will probably have a case for that.. so might be better to just have a "banned casing" section or something to enforce certain substrings follow certain casing?

The snake_case regex doesn't look correct

The regexes were just examples I cobbled together based on a quick think, I doubt any of them will actually work correctly :)

Options.functions.arguments should probably be Options.functions.parameters

I see from googling that an argument is the data you pass in to a function, but a parameter is the declaration of the variable in the function.

It seems somewhat less flexible than my suggestion – how would you (for example) specify a pattern for static readonly class fields? Or abstract methods, etc.

My question for people is always "what do you gain from having a naming convention for a static readonly abstract class method?" and the answer is usually "nothing, but I want it anyway".
Case in point the I prefix for interfaces. Nobody has a good reason apart from "I know this convention from other languages, so I use it".

At some point I think that you don't need that level of granularity. A lot of naming conventions come from the javascript world - a world without a type checker, or old, old school compiled languages, where your compilation cycles were prohibitively expensive, and IDE tooling was non-existant yet, so you relied on naming to communicate context.

In the world of vscode and typescript, IMO 99% of these naming conventions are unnecessary...

/rant

naming-convention

That's an interesting rule for sure. Seems to do a lot of what people want. We could model off of it.

@glen-84
Copy link
Contributor

glen-84 commented Nov 22, 2019

The problem is that this is kind of undetectable without false positives or a dictionary of words ...

True, but I don't think that these cases occur frequently, and they can often be worked around:

  • IFoo – can be handled with the prefix option in the naming-convention rule (the prefix is sliced off before matching with the regex).
  • TypeOfACar – can be changed to CarType, TypeOfTheCar, etc. (I know it's just an example).

The naming-convention rule handles this with strict options, strictCamelCase and StrictPascalCase.

My question for people is always "what do you gain from having a naming convention for a static readonly abstract class method?"

I struggle sometimes to convince less than 5 developers that certain conventions make sense – I doubt that you'll be able to convince the authors of 350k public repositories, and countless private ones. 🙂

I usually UPPER_CASE public static readonly properties and other global constants, to make them stand out.

That's an interesting rule for sure. Seems to do a lot of what people want. We could model off of it.

It seems to be highly configurable, and I really like the inheritance model. One thing that I'd like to see would be a backing/backingField/allowBackingField option for the leadingUnderscore formatting rule, which would allow a leading underscore if there was a getter or setter with the same name (sans the leading underscore).

@bradzacher bradzacher added the has pr there is a PR raised to close this label Dec 9, 2019
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement: new plugin rule New rule request for eslint-plugin has pr there is a PR raised to close this package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants