Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

RFC: Common format to specify a type or value as a rule option#6017

Closed
Discussion options

Edit: this was implemented! See#4436 ->https://typescript-eslint.io/rules/prefer-readonly-parameter-types.


Overview

There are quite a few times when users have a real need to change or disable a rule for specifically some package exports:

This RFC proposes a unified format that all rules can use to specify some types or values from the global namespace and/or packages.

Prior Art

Some rules have previously created name-based overrides, such astypesToIgnore fromno-unnecessary-type-assertion. Those are sub-optimal:

  • Names have conflicts. If you configure a rule for, say, all functions of a particular name that a package exports, you might also happen to ignore the rule for your internal functions of the same name.
  • It may be necessary to configure a rule for all values of a particular type, regardless of their name.

For example,@typescript-eslint/no-floating-promises even has avoid operator to get around needing to disable the rule for specific cases. It's the workaround we currently recommend for issues such as#5231.

Proposal

The current proposal is for specifying the following types of sources, in increasing order of distance from a line of code:

  • File: the current file or another specific file in your package (often exported as a module, but not necessarily)
  • Lib: globals such as fromlib.d.ts orlib.dom.d.ts
  • Package: an export from a package, such as"lodash"

Additionally, you can specify multiple of these at the same time with typemany.

interfaceFileSpecifier{from:"file";name:string|string[];source?:string;}interfaceLibSpecifier{from:"lib";name:string|string[];}interfacePackageSpecifier{from:"package";name:string|string[];source:string;}interfaceManySpecifiers{from:EntitySpecifierFrom[];name:string;}typeEntitySpecifierFrom="file"|"lib"|"package";typeEntitySpecifier=|string|FileSpecifier|LibSpecifier|PackageSpecifier|ManySpecifiers;

An example of how this might look in practice:

exportdefault{rules:{// https://github.com/typescript-eslint/typescript-eslint/issues/5231"@typescript-eslint/no-floating-promises":["error",{"ignore":[{"from":"package","name":["it","test"],"source":"node:test"}]}],}}

See#5271 (comment) and following comments for more context.

Original format, not rich enough

I propose the format look be able to specify whether the item is from theglobal namespace (i.e.globalThis,window) or amodule (e.g."node:test","./path/to/file"):

interfaceGlobalSpecifier{name:string;}interfaceModuleSpecifier{export:string;module:string;}typeSpecifier=GlobalSpecifier|ModuleSpecifier;

Examples

For example, specifying the globallocation object:

{"name":"location"}

Instead specifying the globalwindow.location object:

{"name":"window.location"}

Specifying thetest export from thenode:test module:

{"export":"test","module":"node:test"}

Specify theSafePromise export from some~/utils/promises module (presumably configured by TSConfig paths to point to a file with a location like./src/utils/promises.ts):

{"export":"SafePromise","module":"~/utils/promises"}

Specify theSafePromise export from somemy-safe-promises (TODO: find some unused or useful name) module:

{"export":"SafePromise","module":"safe-promises"}
You must be logged in to vote

Replies: 22 comments 4 replies

Comment options

I would suggest we make it a proper discriminated union to disambiguate the types and the configuration.

Eg

interfaceGlobalSpecifier{type:'global';name:string;}interfaceModuleSpecifier{type:'module';export:string;module:string;}typeSpecifier=GlobalSpecifier|ModuleSpecifier;
You must be logged in to vote
0 replies
Comment options

The difficult thing for the module spec would be specifying exactly which module you're talking about.

Sure it works fine for npm packages - but how do you handle relative imports? How do you handle tsconfig path mapping?

Perhaps we require the module is either an exact match (eg'eslint') or an absolute path (eg'/Users/josh/project/src/Foo'). Perhaps we allow for a config option which passes in the project root and it resolves all relative paths relative to that path? Or we could just say "stuff it" and force people to userequire.resolve or joinpath.join(__dirname, './relative)`.

Path mapping is a whole other issue to think about because ideally you would want to specify the absolute path once - and not specify it plus any path mapped versions.

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Aug 1, 2022
Maintainer Author

proper discriminated union

Hmm, that would make the types easier to work with in code, and explicitly easier to read. But I wonder if it'd get annoying for consumers using it?

relative imports? ... tsconfig path mapping?

I like the "stuff it" option for now. We can always add in relativity later, once we better understand how it's used in user code.


Another thing to consider: users may want to use* wildcards in their paths. I think we should support this. Use cases likeexplicit-module-boundary-types are well served by it, such as ignoring types on all exports under./src/pages/.

You must be logged in to vote
0 replies
Comment options

But I wonder if it'd get annoying for consumers using it?

IMO verbose but clear is better for config. Esp since eslint configs are really written just once.

Use cases like explicit-module-boundary-types are well served by it, such as ignoring types on all exports under ./src/pages/.

I think that sort of usecase would be outside the scope of this RFC wouldn't it?

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Aug 1, 2022
Maintainer Author

I don't think it'd be outside of the scope, no. Additional use case: let's say a project has several variants of Promises -BluebirdPromise,JQuery2Promise,JQuery3Promise,Promise,PromiseLike-. They'll probably want to use a matcher like"*Promise*" on several rules.


I'm also wondering, maybe we should allow plain strings? Like"location" instead of{ "name": "location" }?

You must be logged in to vote
0 replies
Comment options

They'll probably want to use a matcher like "Promise" on several rules.

Personally I'd respond the same way as with the discriminated union - configure once so verbosity is better.

The problem with adding "glob" matchers like that is that if you have positive matchers, you need negative matchers too. Or else people can get stuck trying to craft the perfect string that hits all of your cases but ignoresPromise2Cheese.

This is what I found innaming-convention and is why it uses an object to specify regex matches like{regex: Regex, match: boolean}.

I don't think it'd be outside of the scope

Sorry, could you clarify how this RFC would apply to the rule? Are you suggesting we'd make the rule type-aware so that we can support not adding certain return types?

I'm also wondering, maybe we should allow plain strings?

Same as above - one way to configure things with a clear, unambiguous schema is better than allowing shorthand.
We can always add complexity later in a minor if people demand it - but you cant take complexity away without a major.

You must be logged in to vote
0 replies
Comment options

I think it's a great suggestion, and I'm also personally in the clear and verbose camp!

PS Where can I installPromise2Cheese from? 😛

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Aug 24, 2022
Maintainer Author

It just occurred to me that existing rules as mentioned in the OP essentially already use the single string form. So it'd be a breaking change to remove that option.

You must be logged in to vote
0 replies
Comment options

Hi,
I'm coming over from the perspective of#4436 and maybe don't have the full context, so sorry if I miss anything important.

Just for posterity, in#4436 we arrived at the following format (reformated to match this RFC):

interfaceLocalSpecifier{source:'local';typeName:string;}interfaceDefaultLibSpecifier{source:'default-lib';typeName:string;}interfacePackageSpecifier{source:'package';typeName:string;package:string;}typeSpecifier=LocalSpecifier|DefaultLibSpecifier|PackageSpecifier;

IMHO, discriminated unions are the better option, for the reasons outlined in previous comments.

I think the distinction ofglobal vs.module is unfortunately a poor one for several reasons:

  1. The ability to distinguish between a local type (e.g. from the same file/module) and a type from the default lib is lost, if I understand it correctly as bothMyAwesomeType andHTMLElement would useGlobalSpecifier, right? I think it would be better to split those two (I am not sure whetherGlobal would then be basicallyLocal?)
  2. Not all code uses modules. While this is gradually changing, I think support for non-module code shouldn't be dropped just yet. Infeat(eslint-plugin): [prefer-readonly-parameter-types] added an optional type allowlist #4436 we instead havepackage vs.local, but that seems too coarse if you use modules... Maybe something like this?
typeSpecifier=LocalSpecifier|ModuleSpecifier|DefaultLibSpecifier|PackageSpecifier;

Where if you don't use modules, all of your project code would beLocal? Maybe this is too complicated and would just confuse everybody? I am not sure about the solution, but the point about supporting non-module codebases stands.

Additionally, it would be really nice if the same convention as in this RFC (whatever it ends up being) were to be adopted by eslint-plugin-functional e.g. in their ruleprefer-immutable-types - CCing@RebeccaStevens as she seems to be behind that rule...

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Oct 27, 2022
Maintainer Author

@marekdedic yes! Agreed - looking back on my RFC as written, I agree it's too ambiguous between the different sources of specifiers. Just confirming I understand what you're proposing, there are the following types of sources, in increasing order of distance from a line of code:

  • Local: the current file
  • Module: another specific file in your package (often exported as a module, but not necessarily)
  • Package: an export from a package, such as"lodash"
  • Default lib: globals such as fromlib.d.ts orlib.dom.d.ts

Assuming we're on the same page, I like the discriminated type union as you've put it, with the addition ofModuleSpecifier. A few thoughts:

  • ForLocalSpecifier andModuleSpecifier, we would want to be able to specify both the name being targetedand a file/module inclusion list. Maybe,source for the actual source (file path / module name), andspecifier as the discriminant?
    • Maybesources should all be optional and default to"*"? I.e. you can ban all, say, local types/values by a name, or in specific file(s)?
    • I don't see a reason why one would want to includesource for default lib, though it could have an impact - e.g. you want to ban a specific type from justlib.dom.d.ts...? I'll leave that out for now.
    • I likepackage as the type name forPackageSpecifier, but it's nice to have the same name for all equivalent properties if possible IMO.
  • I'll nit thattypeName isn't ideal because these should be usable for values as well - maybeentity would work?
  • My instinct seeing"default lib" would be that we can simplify to"lib", since it targetslib*.d.ts files... but I can also see an argument that"lib" as a term also often refers to other node modules...
    • Context I'm thinking of primarily: TypeScript used to call its"skip checking the libs code" optionskipDefaultLibCheck, and now calls itskipLibCheck to also refer to other node modules 😕
interfaceLocalSpecifier{entity:string;source?:string;specifier:"local";}interfaceModuleSpecifier{entity:string;source?:string;specifier:"module";}interfaceDefaultLibSpecifier{entity:string;specifier:"default-lib";}interfacePackageSpecifier{entity:string;source:string;specifier:"package";}typeEntitySpecifier=|LocalSpecifier|ModuleSpecifier|DefaultLibSpecifier|PackageSpecifier;typeEntitySpecifierType=EntitySpecifier["type"];

Another complication: what if you wanted to target multiple types of specifiers -- say, all*Promise-named types? You'd have to include up to four unique objects with near-duplicateentity/source. It'd be nice to allow formats like:

// Specifies three, just not from packages{"entity":"*Promise","specifier":["local","module","default-lib"]}// Specifies all four{"entity":"*Promise","specifier":["local","module","default-lib","package"]}

...and then we also have the backwards compatibility + ergonomics issue of a lot of rules trying that kind of"select everything everywhere all at once" with just a string:

// Shorthand for the "all four" case above"*Promise";

...so I wonder if we should add in these types:

// (add in types from above)interfaceManySpecifiers{entity:string;specifier:EntitySpecifierType[];}typeEntitySpecifier=|string|LocalSpecifier|ModuleSpecifier|DefaultLibSpecifier|PackageSpecifier|ManySpecifiers;

What do you think?

It would begreat to have this in for v6, which we just started working on this week. 🚀

You must be logged in to vote
0 replies
Comment options

Hi,
I think you got the general idea right and I am glad you liked it :)

Some comments on the details:

I see that you added filename even toLocalSpecifier - Do I understand it correctly that{specifier: "local", entity: "asdf", source: "myfile.ts"} would match any local variable namedasdf only inside the filemyfile.ts? As opposed to{specifier: "local", entity: "asdf"} matching any variable namedasdf in any file, as long as it was defined in the same file where it was used? I am wondering if these are even useful and wouldn't be better served by an ignore comment? I am not sure.

I'm sorry, but I really don't like the idea of the wildcard shorthands. Like, in general. :( I only have the point of view ofprefer-readonly-parameter-types, but I would say if you want to whitelist a type from any source, you're holding it wrong.

As for naming, I used the names form my original PR, so definitely not married to them. My 2¢:

  • IfModuleSpecifier is intended for any files, not just modules, why not call itFileSpecifier (and have correspondinglyspecifier: "file")?
  • typeName isn't great and outright bad if it's supposed to be used for things other than types. I'm not a fan ofentity either, though. How about something likename oritem?
  • Let's uselib instead ofdefault-lib, agreed.
  • I am against unifying package name and filename undersource -package andfile are pretty obvious,source isn't. Understandable > simple
  • I don't likespecifier either :D How aboutlocation?

As far as v6 goes, I'm looking forward to it and kind of tacitly assumed that this big config redesign was supposed to bethe breaking change in v6 :)

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Oct 29, 2022
Maintainer Author

added filename even toLocalSpecifier ... wondering if these are even useful

Yeah, I think it's not useful for most rules, but insome it would be very useful. For example, you might want to target...

  • get(ServerSide|Static)Props functions insrc/pages/**/* (Next.js docs)
  • jest.mock calls in*.test.*

if you want to whitelist a type from any source, you're holding it wrong

Agreed 😄. I can't think of anymodern codebase reason to want this. The closest I can think of is the idea of targeting all*Promise types...

But, for the sake of the other use cases for this standard format (such as the examples above), I think we'll need to throw it in to the format.

IfModuleSpecifier is intended for any files, not just modules, why not call itFileSpecifier (and have correspondinglyspecifier: "file")?

You're totally right and my proposal is technically inaccurate - module is a subset of file!

...and now I'm wondering, what use case is there for having a delineation betweenLocalSpecifier andFileSpecifier/ModuleSpecifier? In retrospect, maybe we should merge those to justFileSpecifier? Going back down to just three variants as you proposed. Updated. 😄

How about something likename oritem?

Agreed. I thinkname is the more standard property folks would expect. Updated.

unifying package name and filename undersource ... Understandable > simple

Agreed in theory, but - I think it would be useful to support theManySpecifiers format. If we have a property inManySpecifiers used for the equivalent ofsource/file/etc., it feels odd to me that it would be different the individual formats. Maybe I'm just being overly cautious. Do you have thoughts on how we could make property name ergonomic?

I don't likespecifier either :D How aboutlocation?

Heh, my hesitation there is thatloc/location in linting land generally refers to locationwithin a file in rule complaints. Maybe ...from?

On that train of thought, words likein andwhere might be good to use...

this big config redesign was supposed to be the breaking change in v6 :)

Haha, this is definitely top of mind & high excitement from a technical perspective! I think you're right for power users like you and me (my favorite kind of user ❤️). But for marketing purposes I'm thinking#5204 and#5900 will likely end up being emphasized more. 🥲

You must be logged in to vote
0 replies
Comment options

As for the usefulness ofLocalSpecifier, I probably don't have enough context, so 🤷

Agreed smile. I can't think of anymodern codebase reason to want this. The closest I can think of is the idea of targeting all*Promise types...

But, for the sake of the other use cases for this standard format (such as the examples above), I think we'll need to throw it in to the format.

If it's supposed to be for edge-cases and outdated code, I would say it's fine to have the user specify it manually. But if you want to do shorthands anyway, I'm much more in favour of the "the specifier can be an array" approach than the "replace the whole object with just a string" approach - the second one is too opaque and I think it would encourage users to do that even if they don't need to, because it looks like thebasic format whereas IMO, it should be anedge case use.

...and now I'm wondering, what use case is there for having a delineation betweenLocalSpecifier andFileSpecifier/ModuleSpecifier? In retrospect, maybe we should merge those to justFileSpecifier? Going back down to just three variants as you proposed. Updated. smile

Ha, I thought about that as well since writing my last comment, but I arrived at the opposite conclusion - thereis a meaningful difference. See this (extremely simplified) example of a non-module file:

functionshowLessonEditView(){constlessonID=$("lesson").data("id");showLessonName(lessonID);// ... rest of function}functionshowLessonName(stringlessonID){$("body").innerHTML="This is a lesson with the ID"+lessonID;}

Even thought this is not a module, there are some variables and functions (and possibly types) that are used just in this file, likelessonID orshowLessonName(). For these, it would make sense to only be whitelisted in this file (aslessonID might be a common local variable name), so aLocalSpecifier would be great for this (or an ignore comment...). On the other hand, the functionshowLessonEditView is probably supposed to be used from outside this file, so it would make sense to useFileSpecifier for it. If this file were a module, onlyshowLessonEditView would be exported and there the same logic could apply - useFileSpecifier for entities that cross module boundaries (i.e. are exported) andLocalSpecifier for ones that don't.

unifying package name and filename undersource ... Understandable > simple

Agreed in theory, but - I think it would be useful to support theManySpecifiers format. If we have a property inManySpecifiers used for the equivalent ofsource/file/etc., it feels odd to me that it would be different the individual formats. Maybe I'm just being overly cautious. Do you have thoughts on how we could make property name ergonomic?

Hmm, you're right, it does make sense to have a single name in that format. I'm still against that format overall, but if it's that way, let's keep just one name.

I don't likespecifier either :D How aboutlocation?

Heh, my hesitation there is thatloc/location in linting land generally refers to locationwithin a file in rule complaints. Maybe ...from?

On that train of thought, words likein andwhere might be good to use...

I thinkfrom is great.{name: "HTMLElement", from: "lib"} seems very understandable to me.

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Nov 11, 2022
Maintainer Author

For these, it would make sense to only be whitelisted in this file ...
On the other hand, the functionshowLessonEditView is probably supposed to be used from outside this file, so it would make sense to use FileSpecifier for it

What worries me is that it often won't be clear which one to use. Suppose you want to banshowLessonName from the example. You could use either aFileSpecifier orModuleSpecifier [edit:LocalSpecifier] right? How would we delineate between the two?

(sorry for the delay - still very interested in this conversation!)

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Nov 11, 2022
Maintainer Author

Also mentioned in ESLint's discussions:eslint/eslint#16540

You must be logged in to vote
0 replies
Comment options

What worries me is that it often won't be clear which one to use. Suppose you want to banshowLessonName from the example. You could use either aFileSpecifier orModuleSpecifier right? How would we delineate between the two?

Hi, either you meantFileSpecifier andLocalSpecifier or we're talking past each other :) Assuming the first, the difference would be thatLocalSpecifier allows you to ban a specifier in the same file and only in that same file. OTOH,FileSpecifier would allow you to ban a specifier when used from other files/modules.

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Nov 12, 2022
Maintainer Author

Whoops, sorry - edited now 😅 to sayLocalSpecifier, thanks for the catch. Given that I'm already mistaking them for each other (which was not planned - a true mistake/typo) and they have so much overlap, I'm going to continue the suggestion that we unify to one. If there's ever a need for a rule we can always add a property likeimported?: 'never' | 'only'.

You must be logged in to vote
0 replies
Comment options

They are very similar, true. I'd lean towards having both, but it's a trade-off that depends on what you prioritize...

You must be logged in to vote
0 replies
Comment options

JoshuaKGoldberg
Dec 30, 2022
Maintainer Author

@bradzacher@JamesHenry I'm ready to consider this finalized! Any last thoughts?

You must be logged in to vote
1 reply
@JamesHenry
Comment options

I think it's great, the only thing I would ask for is that we expose the types and some utilities for working with them in rules as part of this effort so that other community plugins can benefit too (e.g. angular-eslint)

Comment options

Hi, I'm trying to implement this and encountered an issue that warrants additional design input.

In thePackageSpecifier format, there is a required fieldsource denoting the package name. However, theManySpecifiers format can includepackage as a type, without specifying thesource - so the validation cannot really verify that the type comes from a particular package, just that it comes from a package in general. That doesn't seem that useful

I see 3 possible solutions:

  1. RemoveManySpecifiers - it's too complex, at the same time limited by issues like this and unnecessary in the grand scheme of things
  2. Make thesource property optional forPackageSpecifier - this would fix the problem with validation, however such a specifier seems pretty broad to be useful
  3. MakeManySpecifiers have a fieldsource that is mandatory whenfrom containspackage - this would make it significantly more complicated to develop & use but would fix this issue
You must be logged in to vote
1 reply
@JoshuaKGoldberg
Comment options

JoshuaKGoldbergJan 11, 2023
Maintainer Author

👍 makes sense. Let's get rid ofManySpecifiers, agreed it's limited & a nice-to-have.

Comment options

Hey@JoshuaKGoldberg, is the Edit with the link at the top correct? It links to#4436 which looks unrelated (or I'm really misunderstanding 😄) I'm unsure how that solves the no-floating-promises issue fornode:test imports, for example

If anyone else stumbles across this, there seems to be more work here:#7008

You must be logged in to vote
2 replies
@RebeccaStevens
Comment options

#4436 added an allowlist which uses this proposal. It's unreleased tono-floating-promises, but uses the same common options structure.

@robcresswell
Comment options

Ah I see. Thanks@RebeccaStevens

Comment options

I should probably mention here that I created a package for determining where a type is defined:ts-declaration-location. It doesn't do any filtering on the type's name or anything like that, just where it is located.

@JoshuaKGoldberg What's your thoughts on merging that library intots-api-utils? I can see it potentially being useful to other tools outside of eslint. -Made an issue for this here

You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
RFCs
Labels
None yet
6 participants
@JoshuaKGoldberg@JamesHenry@marekdedic@robcresswell@RebeccaStevens@bradzacher
Converted from issue

This discussion was converted from issue #5271 on November 17, 2022 15:54.


[8]ページ先頭

©2009-2025 Movatter.jp