-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix(typescript-eslint): gracefully handle invalid flat config objects in tseslint.config
#10576
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
fix(typescript-eslint): gracefully handle invalid flat config objects in tseslint.config
#10576
Conversation
Thanks for the PR, @abrahamguo! typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community. The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately. Thanks again! 🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint. |
✅ Deploy Preview for typescript-eslint ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
View your CI Pipeline Execution ↗ for commit 373e325.
☁️ Nx Cloud last updated this comment at |
the arguments passed to this function at runtime. If any argument is an unexpected type, we should not cause a | ||
`TypeError`, and instead, we should silently return the unexpected argument unchanged, because ESLint will later | ||
validate the types. */ | ||
const flattened: unknown[] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the key of this PR: treat the array internally as unknown[]
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #10576 +/- ##
=======================================
Coverage 87.26% 87.27%
=======================================
Files 450 450
Lines 15715 15722 +7
Branches 4601 4604 +3
=======================================
+ Hits 13714 13721 +7
Misses 1645 1645
Partials 356 356
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
...config | ||
}: ObjectWithExtends & | ||
Partial<Record<'files' | 'ignores' | 'name', unknown>> = | ||
configWithExtends as ObjectWithExtends; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: I am following the approach here that any properties that get sent to ESLint (files | ignores | name
) are typed as unknown
, so that we assume nothing, and ESLint does the validation. What about the extends
property, which is unique to tseslint, and is not sent to ESLint?
A. Do we assume that extends
always obeys its TS type (i.e. it will always be an array)?
As I mentioned in #10549, there could be several scenarios where a user is not type-checking their config file. This would cause a user-unfriendly error if, for example, they passed a string to extend
:`"flat" is not a method on string", and it is inconsistent with all other settings in the config file, which have user-friendly error messages generated by ESLint.
B. Do we pass along invalid values for extends
to ESLint?
This doesn't make much sense, because then ESLint would display an error unexpected property 'extends' on user-defined config object
, and ESLint never expects the property extends
.
C. Do we silently ignore/remove invalid values for extends
?
It's inconsistent that this one property in the config file fails silently, whereas any other property with an invalid value would cause an ESLint-generated error message.
D. Do we manufacture an error message consistent with ESLint's error message?
The user would see Key "extends": Expected value to be an array
, which would be a consistent user experience as compared to any other property in the config file. Downside is obviously that we have to keep our error message in sync with ESLint's.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: I am following the approach here that any properties that get sent to ESLint (
files | ignores | name
) are typed asunknown
, so that we assume nothing, and ESLint does the validation. What about theextends
property, which is unique to tseslint, and is not sent to ESLint?A. Do we assume that
extends
always obeys its TS type (i.e. it will always be an array)? As I mentioned in #10549, there could be several scenarios where a user is not type-checking their config file. This would cause a user-unfriendly error if, for example, they passed a string toextend
:`"flat" is not a method on string", and it is inconsistent with all other settings in the config file, which have user-friendly error messages generated by ESLint.
No; we should not assume that the extends
property has valid shape
B. Do we pass along invalid values for
extends
to ESLint? This doesn't make much sense, because then ESLint would display an errorunexpected property 'extends' on user-defined config object
, and ESLint never expects the propertyextends
.
Agreed - no.
C. Do we silently ignore/remove invalid values for
extends
? It's inconsistent that this one property in the config file fails silently, whereas any other property with an invalid value would cause an ESLint-generated error message.D. Do we manufacture an error message consistent with ESLint's error message? The user would see
Key "extends": Expected value to be an array
, which would be a consistent user experience as compared to any other property in the config file. Downside is obviously that we have to keep our error message in sync with ESLint's.
Yes, I think we should report a user-friendly error for invalid values. I don't think it's particularly essential that it remain identical to eslint core's error message wording (in other words, no need to stress about minor drift), but I do think it's essential that we conspicuously identify it as coming from the tseslint.config()
function, not from eslint core.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kirkwaiblinger I've updated both the existing error message about invalid values inside extends
, as well as the new error message about invalid extends itself, to be consistent and to match ESLint. What do you think?
config.name != null | ||
? `, named "${ | ||
// eslint-disable-next-line @typescript-eslint/no-base-to-string,@typescript-eslint/restrict-template-expressions | ||
config.name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though config.name
is unknown
, it should still be fine to concatenate it
Ready for review/discussion! |
...(name && { name }), | ||
}; | ||
}), | ||
config, | ||
// `config` could be any object, but we'll assume it's a `Config` object for TS purposes. | ||
config as Config, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inside this function, config
should be unknown
, but outside this function, config
should be Config
even if it's really an invalid value
return flattened.flatMap((configWithExtends, configIndex) => { | ||
const { extends: extendsArr, ...config } = configWithExtends; | ||
if (extendsArr == null || extendsArr.length === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null
check was updated to use in
, to make TS happy.
length
check was unnecessary.
const extendsError = (message: string) => | ||
new TypeError( | ||
`tseslint.config(): Config ${ | ||
nameIsString ? `"${name}"` : '(unnamed)' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this matches ESLint's logic, where it only uses the name if it is a string.
@@ -247,4 +245,16 @@ describe('config helper', () => { | |||
{ rules: { rule: 'error' } }, | |||
]); | |||
}); | |||
|
|||
it.each([undefined, null])( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(testing) maybe add also like one string and/or number or something 🤷♂️
it.each([undefined, null])( | |
it.each([undefined, null, "invalid config object", 42])( |
@@ -81,8 +81,7 @@ describe('config helper', () => { | |||
}, | |||
), | |||
).toThrow( | |||
'Your config at index 1, named "my-config-2", contains undefined ' + | |||
'extensions at the following indices: 0, 2', | |||
'tseslint.config(): Config "my-config-2": Key "extends": Expected array to only contain objects at user-defined index 1.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a regression, right?
- We have lost the index of
my-config-2
in the error message extension
, at index 1, is an object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, not a regression. I was trying to match ESLint's error message format exactly.
The index of my-config-2
(1
) was moved to the end of the message to be consistent with ESLint.
However, I can revert this.
@@ -107,8 +106,7 @@ describe('config helper', () => { | |||
}, | |||
), | |||
).toThrow( | |||
'Your config at index 1 (anonymous) contains undefined extensions at ' + | |||
'the following indices: 0, 2', | |||
'tseslint.config(): Config (unnamed): Key "extends": Expected array to only contain objects at user-defined index 1.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have we changed the naming of (anonymous)
to (unnamed)
? Revert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this to more closely match ESLint's error convention, but I'm happy to revert it.
const { extends: extendsArr, ...config } = configWithExtends; | ||
if (extendsArr == null || extendsArr.length === 0) { | ||
return config; | ||
const isObject = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this and any other inner functions that don't capture variables to be top-level named variables. This should look like
function isObject(value: unknown): value is object {
// eslint-disable-next-line @typescript-eslint/internal/eqeq-nullish, eqeqeq
return typeof value === 'object' && value !== null;
}
Technically a correct type-predicate here would also have typeof value === 'function'
since const o: object = () => {}
is valid, but probably not worth thinking about.
Infinity, | ||
) as ConfigWithExtends[]; | ||
|
||
const undefinedExtensions = extendsArrFlattened.reduce<number[]>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just change this to be invalidExtensions
and report more errors than just the indices?
…onfig-helper-unknown
…onfig-helper-unknown
👋 @abrahamguo just checking in, did you mean to re-request review on this? Anything we can help with? |
Closing this PR as it's been stale for a couple of months without activity. Feel free to reopen @abrahamguo if you have time - but no worries if not! If anybody wants to drive it forward, please do post your own PR. If you use this as a start, please add a co-author attribution at the end. Cheers! ✨ |
PR Checklist
tseslint.config
hides user-friendly error messages from ESLint #10549Overview
Gracefully handle invalid flat config objects in
tseslint.config
.