Skip to content

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

Closed

Conversation

abrahamguo
Copy link
Contributor

PR Checklist

Overview

Gracefully handle invalid flat config objects in tseslint.config.

@typescript-eslint
Copy link
Contributor

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.

Copy link

netlify bot commented Dec 31, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 373e325
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/679cd59bc8456300089377f7
😎 Deploy Preview https://deploy-preview-10576--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 92 (🔴 down 6 from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 98 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

nx-cloud bot commented Dec 31, 2024

View your CI Pipeline Execution ↗ for commit 373e325.

Command Status Duration Result
nx test visitor-keys --coverage=false ✅ Succeeded <1s View ↗
nx run types:build ✅ Succeeded <1s View ↗
nx run-many --target=build --exclude website --... ✅ Succeeded 16s View ↗
nx run-many --target=clean ✅ Succeeded 32s View ↗
nx test typescript-eslint --coverage=false ✅ Succeeded 9s View ↗
nx test scope-manager --coverage=false ✅ Succeeded <1s View ↗
nx test eslint-plugin --coverage=false ✅ Succeeded <1s View ↗
nx test type-utils --coverage=false ✅ Succeeded <1s View ↗
Additional runs (24) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2025-01-31 14:14:05 UTC

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[] =
Copy link
Contributor Author

@abrahamguo abrahamguo Dec 31, 2024

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[]

Copy link

codecov bot commented Dec 31, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 87.27%. Comparing base (a94004c) to head (373e325).
Report is 142 commits behind head on main.

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           
Flag Coverage Δ
unittest 87.27% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/typescript-eslint/src/config-helper.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

...config
}: ObjectWithExtends &
Partial<Record<'files' | 'ignores' | 'name', unknown>> =
configWithExtends as ObjectWithExtends;
Copy link
Contributor Author

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.

Copy link
Member

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.

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 error unexpected property 'extends' on user-defined config object, and ESLint never expects the property extends.

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.

Copy link
Contributor Author

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
Copy link
Contributor Author

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

@abrahamguo
Copy link
Contributor Author

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,
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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.

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Jan 2, 2025
const extendsError = (message: string) =>
new TypeError(
`tseslint.config(): Config ${
nameIsString ? `"${name}"` : '(unnamed)'
Copy link
Contributor Author

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.

@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Jan 7, 2025
@@ -247,4 +245,16 @@ describe('config helper', () => {
{ rules: { rule: 'error' } },
]);
});

it.each([undefined, null])(
Copy link
Member

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 🤷‍♂️

Suggested change
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.',
Copy link
Member

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.

Copy link
Contributor Author

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.',
Copy link
Member

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?

Copy link
Contributor Author

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 = (
Copy link
Member

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[]>(
Copy link
Member

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?

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Jan 24, 2025
@JoshuaKGoldberg
Copy link
Member

👋 @abrahamguo just checking in, did you mean to re-request review on this? Anything we can help with?

@JoshuaKGoldberg
Copy link
Member

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! ✨

@JoshuaKGoldberg JoshuaKGoldberg added the stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period label Apr 7, 2025
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 15, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
awaiting response Issues waiting for a reply from the OP or another party stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: tseslint.config hides user-friendly error messages from ESLint
3 participants