-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix(table-core): guard range filters against non-array values #6602
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/table-core': patch | ||
| --- | ||
|
|
||
| Guard `resolveFilterValue` in `inNumberRange` and `inDateRange` against filter values that are not arrays. Previously a string was destructured per character (`'30'` became the range `[0, 3]`, filtering silently wrong) and a number, boolean or `Date` threw `TypeError: val is not iterable`. Such values now leave the range fully open and warn in development. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -280,7 +280,9 @@ export const filterFn_betweenInclusive = constructFilterFn({ | |||||||||||||||||||
| * Filter values are normalized so blank endpoints become open-ended and | ||||||||||||||||||||
| * reversed endpoints are swapped. Only real numbers can fall inside the | ||||||||||||||||||||
| * range: non-numeric row values (`null`, `undefined`, strings, booleans) | ||||||||||||||||||||
| * never match. | ||||||||||||||||||||
| * never match. A filter value that is not an array leaves the range fully | ||||||||||||||||||||
| * open and warns in development, instead of being split per character or | ||||||||||||||||||||
| * throwing. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| export const filterFn_inNumberRange = constructFilterFn({ | ||||||||||||||||||||
| filter: (dataValue: number, filterValue: [number, number]) => { | ||||||||||||||||||||
|
|
@@ -294,6 +296,10 @@ export const filterFn_inNumberRange = constructFilterFn({ | |||||||||||||||||||
| return dataValue >= min && dataValue <= max | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| resolveFilterValue: (val: [any, any]) => { | ||||||||||||||||||||
| if (!isRangeArray(val, 'inNumberRange')) { | ||||||||||||||||||||
| return [-Infinity, Infinity] as const | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const [unsafeMin, unsafeMax] = val | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const parsedMin = | ||||||||||||||||||||
|
|
@@ -326,14 +332,20 @@ export const filterFn_inNumberRange = constructFilterFn({ | |||||||||||||||||||
| * | ||||||||||||||||||||
| * Row values and range endpoints may be `Date` objects, timestamps, or | ||||||||||||||||||||
| * parseable date strings. Blank or invalid endpoints become open-ended and | ||||||||||||||||||||
| * reversed endpoints are swapped. Rows without a valid date never match. | ||||||||||||||||||||
| * reversed endpoints are swapped. Rows without a valid date never match. A | ||||||||||||||||||||
| * filter value that is not an array leaves the range fully open and warns in | ||||||||||||||||||||
| * development, instead of being split per character or throwing. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| export const filterFn_inDateRange = constructFilterFn({ | ||||||||||||||||||||
| filter: (dataValue: number, filterValue: [number, number]) => { | ||||||||||||||||||||
| const [min, max] = filterValue | ||||||||||||||||||||
| return dataValue >= min && dataValue <= max | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| resolveFilterValue: (val: [any, any]) => { | ||||||||||||||||||||
| if (!isRangeArray(val, 'inDateRange')) { | ||||||||||||||||||||
| return [-Infinity, Infinity] as const | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const [unsafeMin, unsafeMax] = val | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const parsedMin = toDateTimestamp(unsafeMin) | ||||||||||||||||||||
|
|
@@ -468,6 +480,31 @@ function testFalsy(val: any) { | |||||||||||||||||||
| return val === undefined || val === null || val === '' | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Guards a range filter value before it is destructured. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * `[any, any]` only exists at compile time: at runtime `setFilterValue()` can | ||||||||||||||||||||
| * be handed anything. Destructuring a string splits it per character (`'30'` | ||||||||||||||||||||
| * becomes `'3'` and `'0'`, a range nothing asked for), and destructuring a | ||||||||||||||||||||
| * number, boolean or `Date` throws. `autoRemove` does not catch either case, | ||||||||||||||||||||
| * since it only drops falsy values and fully blank tuples. Arrays of any | ||||||||||||||||||||
| * length pass through to the existing endpoint handling unchanged. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function isRangeArray(val: any, filterFnName: string): val is Array<any> { | ||||||||||||||||||||
| if (Array.isArray(val)) { | ||||||||||||||||||||
| return true | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (process.env.NODE_ENV === 'development') { | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '475,515p' packages/table-core/src/features/column-filtering/filterFns.ts
rg -n -C 4 'process|NODE_ENV|warn|unstub' packages/table-core/tests/unit/fns/filterFns.test.ts
git diff 8b267be6bcf0b347d12de88252ccb33e90e55e5c dd1542ac0f2af22b9d9acbb6ac5932799209197c -- packages/table-core/src/features/column-filtering/filterFns.ts packages/table-core/tests/unit/fns/filterFns.test.tsRepository: TanStack/table Length of output: 6099 🏁 Script executed: sed -n '285,360p' packages/table-core/src/features/column-filtering/filterFns.ts
rg -n -C 5 'resolveFilterValue|filterFn_in(Number|Date)Range|setFilterValue' packages/table-core/src packages/table-core/tests/unit/fns/filterFns.test.ts
sed -n '1,120p' packages/table-core/package.json
sed -n '1,80p' packages/table-core/src/index.tsRepository: TanStack/table Length of output: 41991 Keep the environment check safe without disabling development warnings. When a process-free browser consumer sets a range filter to a non-array value, A 🐛 Suggested fix- if (process.env.NODE_ENV === 'development') {
+ let isDevelopment = false
+ try {
+ isDevelopment = process.env.NODE_ENV === 'development'
+ } catch {
+ // `process` is absent in untransformed browser ESM.
+ }
+
+ if (isDevelopment) {This preserves development warnings in replaced bundles and the open-range fallback in process-free published ESM. The fix is localized. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| console.warn( | ||||||||||||||||||||
| `filterFn '${filterFnName}' expects a [min, max] tuple, received:`, | ||||||||||||||||||||
| val, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return false | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function testValueEmpty(dataValue: any) { | ||||||||||||||||||||
| return dataValue == null || String(dataValue).trim() === '' | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.