From 9a9d45ebed149c82a4d3e9110cecdb1eab8e4c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 13 Nov 2026 11:42:29 -0500 Subject: [PATCH] fix(lint): Fix bugs found reviewing custom oxlint rules Fixes false positives, false negatives, broken autofixes, and over-wide report ranges across the sentry and scraps oxlint plugins, removes dead code, and adds the test cases that expose each. --- .../lint-fix/references/fix-patterns.md | 2 +- oxlint.config.ts | 1 + .../arithmeticBuilder/token/deleteButton.tsx | 2 +- .../interfaces/frame/deprecatedLine.tsx | 2 +- .../events/interfaces/nativeFrame.tsx | 2 +- .../feedbackItem/feedbackItemSection.tsx | 2 +- .../feedback/list/feedbackListItem.tsx | 2 +- .../externalIssuesList/linkedIssueRows.tsx | 2 +- .../interactionStateLayer/index.tsx | 2 +- .../components/onboardingWizard/content.tsx | 2 +- .../preprod/preprodBuildsSnapshotTable.tsx | 2 +- .../preprod/preprodBuildsTableCommon.tsx | 2 +- .../list/__stories__/replayListItem.tsx | 2 +- .../components/replays/table/replayTable.tsx | 2 +- .../searchQueryBuilder/tokens/boolean.tsx | 2 +- .../tokens/deletableToken.tsx | 2 +- .../tokens/filter/aggregateKey.tsx | 2 +- .../tokens/filter/filter.tsx | 2 +- .../tokens/filter/filterKey.tsx | 2 +- .../tokens/filter/filterOperator.tsx | 2 +- .../tokens/filterKeyListBox/index.tsx | 2 +- static/app/components/stream/group.tsx | 2 +- .../app/components/tables/dataGrid/index.tsx | 2 +- .../tables/simpleTable/index.stories.tsx | 2 +- .../components/tables/simpleTable/index.tsx | 2 +- .../tokenizedInput/token/deletableToken.tsx | 2 +- static/app/oxlint-suppressions.json | 18 + .../automations/components/actions/slack.tsx | 4 +- .../connectedDetectors.tsx | 2 +- .../components/visualize/selectRow.tsx | 62 +- .../detectorListConnectedAutomations.tsx | 2 +- .../replays/selectors/exampleReplaysList.tsx | 2 +- static/app/views/inbox/inboxItemList.tsx | 2 +- .../app/views/issueList/newViewEmptyState.tsx | 2 +- .../app/views/issueList/pages/inbox/index.tsx | 2 +- .../components/fileDiffViewer.tsx | 2 +- .../sentryApplicationDashboard/requestLog.tsx | 2 +- .../organizationTeams/otherTeamsTable.tsx | 2 +- .../organizationTeams/yourTeamsTable.tsx | 2 +- static/gsApp/views/seerAutomation/trial.tsx | 2 +- .../src/ast/extractor/css.ts | 78 +- .../src/ast/extractor/cssProp.ts | 135 --- .../src/ast/extractor/index.ts | 250 ++++-- .../src/ast/extractor/styleProp.ts | 90 -- .../src/ast/extractor/styled.ts | 178 ---- .../src/ast/extractor/types.ts | 234 ----- .../src/ast/extractor/valueDecomposer.ts | 189 ---- .../src/ast/tracker/imports.spec.ts | 97 ++- .../src/ast/tracker/imports.ts | 96 +-- .../src/ast/tracker/theme.ts | 139 --- .../src/ast/utils/normalizePropertyName.ts | 24 +- .../src/ast/utils/removeJsxAttributes.ts | 16 + .../src/ast/utils/renderFunction.ts | 14 +- .../src/ast/utils/styled.spec.ts | 44 +- .../src/ast/utils/styled.ts | 32 +- .../src/config/tokenRules.ts | 4 +- .../src/rules/noCoreImport.spec.ts | 4 + .../src/rules/noCoreImport.ts | 35 +- .../src/rules/noDomCoupling.spec.ts | 258 ++++++ .../src/rules/noDomCoupling.ts | 63 +- .../rules/noDoubleDollarInterpolation.spec.ts | 4 + .../src/rules/noDoubleDollarInterpolation.ts | 5 - .../src/rules/noManualTable.spec.ts | 300 ++++++- .../src/rules/noManualTable.ts | 71 +- .../src/rules/noRestrictedModuleMocks.spec.ts | 7 + .../src/rules/noRestrictedModuleMocks.ts | 16 +- .../src/rules/noTokenImport.spec.ts | 14 +- .../src/rules/noTokenImport.ts | 37 +- .../preferContainerResponsiveProps.spec.ts | 108 +++ .../rules/preferContainerResponsiveProps.ts | 49 +- .../src/rules/preferInfoText.spec.ts | 394 ++++++++- .../src/rules/preferInfoText.ts | 223 +++-- .../src/rules/preferPrimitives.spec.ts | 107 +++ .../src/rules/preferPrimitives.ts | 6 +- .../rules/preferStackForColumnFlex.spec.ts | 127 +++ .../src/rules/preferStackForColumnFlex.ts | 122 +-- .../src/rules/requireFieldComponent.spec.ts | 38 + .../src/rules/requireFieldComponent.ts | 2 - .../src/rules/requireRenderPropSpread.spec.ts | 261 ++++++ .../src/rules/requireRenderPropSpread.ts | 174 ++-- .../src/rules/restrictJsxSlotChildren.spec.ts | 124 ++- .../src/rules/restrictJsxSlotChildren.ts | 57 +- .../src/rules/useSemanticToken.spec.ts | 344 +++++++- .../src/rules/useSemanticToken.ts | 90 +- .../eslintPluginSentry/emotionRules.spec.ts | 98 ++- .../oxlint/eslintPluginSentry/emotionRules.ts | 91 +- .../eslintPluginSentry/fixtures/cycleA.ts | 1 + .../eslintPluginSentry/fixtures/cycleB.ts | 1 + .../fixtures/defaultArrow.ts | 1 + .../fixtures/defaultBarrel.ts | 8 + .../fixtures/defaultClass.ts | 7 + .../fixtures/defaultFunction.ts | 3 + .../fixtures/defaultIdentifier.ts | 3 + .../fixtures/defaultOverloaded.ts | 4 + .../fixtures/importedDefaultKinds.ts | 136 +++ .../namingConvention.spec.ts | 20 + .../eslintPluginSentry/namingConvention.ts | 6 +- .../noCallingComponentsAsFunctions.spec.ts | 109 ++- .../noCallingComponentsAsFunctions.ts | 152 ++-- .../noDefaultExports.spec.ts | 81 ++ .../eslintPluginSentry/noDefaultExports.ts | 37 +- .../eslintPluginSentry/noDigitsInTn.spec.ts | 5 + .../oxlint/eslintPluginSentry/noDigitsInTn.ts | 2 +- .../noDynamicTranslations.spec.ts | 15 + .../eslintPluginSentry/noFlagComments.spec.ts | 1 + .../eslintPluginSentry/noFlagComments.ts | 2 +- .../eslintPluginSentry/noGlobalReact.spec.ts | 5 + .../noQueryDataTypeParameters.spec.ts | 2 + .../noQueryDataTypeParameters.ts | 10 +- .../noRawCssInStyled.spec.ts | 20 + .../eslintPluginSentry/noRawCssInStyled.ts | 24 +- .../noRedundantDefaultArgument.spec.ts | 811 ++++++++++++++++++ .../noRedundantDefaultArgument.ts | 147 ++-- .../noRelativeImportPaths.spec.ts | 51 ++ .../noRelativeImportPaths.ts | 13 +- .../noStaticTranslations.spec.ts | 52 ++ .../noStaticTranslations.ts | 46 +- .../noStyledShortcut.spec.ts | 21 +- .../eslintPluginSentry/noStyledShortcut.ts | 7 +- .../noUnnecessaryUseCallback.spec.ts | 396 +++++++++ .../noUnnecessaryUseCallback.ts | 178 ++-- ...noUselessCssInterpolationSemicolon.spec.ts | 46 + .../noUselessCssInterpolationSemicolon.ts | 28 +- .../preferFakeTimers.spec.ts | 311 ++++++- .../eslintPluginSentry/preferFakeTimers.ts | 71 +- .../eslintPluginSentry/preferNuqs.spec.ts | 5 + .../preferReactComponent.spec.ts | 83 ++ .../preferReactComponent.ts | 50 +- .../preferRelativeChildImports.spec.ts | 10 +- .../preferRelativeChildImports.ts | 8 - .../requireFakeTimerCleanup.spec.ts | 37 + .../requireFakeTimerCleanup.ts | 24 +- .../requireFakeTimerCleanupFinally.spec.ts | 40 + .../sortInterfaceKeys.spec.ts | 164 +++- .../eslintPluginSentry/sortInterfaceKeys.ts | 5 +- .../oxlint/eslintPluginSentry/utils/styled.ts | 11 +- 136 files changed, 5811 insertions(+), 2465 deletions(-) delete mode 100644 static/oxlint/eslintPluginScraps/src/ast/extractor/cssProp.ts delete mode 100644 static/oxlint/eslintPluginScraps/src/ast/extractor/styleProp.ts delete mode 100644 static/oxlint/eslintPluginScraps/src/ast/extractor/styled.ts delete mode 100644 static/oxlint/eslintPluginScraps/src/ast/extractor/types.ts delete mode 100644 static/oxlint/eslintPluginScraps/src/ast/extractor/valueDecomposer.ts delete mode 100644 static/oxlint/eslintPluginScraps/src/ast/tracker/theme.ts create mode 100644 static/oxlint/eslintPluginScraps/src/ast/utils/removeJsxAttributes.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/cycleA.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/cycleB.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/defaultArrow.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/defaultBarrel.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/defaultClass.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/defaultFunction.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/defaultIdentifier.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/defaultOverloaded.ts create mode 100644 static/oxlint/eslintPluginSentry/fixtures/importedDefaultKinds.ts create mode 100644 static/oxlint/eslintPluginSentry/noStaticTranslations.spec.ts diff --git a/.agents/skills/lint-fix/references/fix-patterns.md b/.agents/skills/lint-fix/references/fix-patterns.md index d414cb4aa3db..1bbd778f0993 100644 --- a/.agents/skills/lint-fix/references/fix-patterns.md +++ b/.agents/skills/lint-fix/references/fix-patterns.md @@ -29,7 +29,7 @@ Load this reference when fixing violations of a specific `@sentry/scraps` rule. **Has autofix**: No. -**What the rule enforces**: Imports from `utils/theme/scraps` are only allowed in the designated theme directory. +**What the rule enforces**: Imports from `@sentry/scraps/tokens` (and its subpaths) are only allowed in the scraps theme and tokens directories. **Manual fix**: Move the token usage to a component that accesses tokens via `theme.tokens.*` through `useTheme()` or styled-component theme callback, rather than importing raw token values directly. diff --git a/oxlint.config.ts b/oxlint.config.ts index 3dc053c9dcd4..5f364ae90fb2 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -314,6 +314,7 @@ const config = defineConfig({ '**/dist/**/*', 'tests/**/fixtures/**/*', '!tests/js/**/*', + 'static/oxlint/**/fixtures/**/*', '**/vendor/**/*', 'build-utils/**/*', 'config/chartcuterie/config.js', diff --git a/static/app/components/arithmeticBuilder/token/deleteButton.tsx b/static/app/components/arithmeticBuilder/token/deleteButton.tsx index e1b177e0d733..5c36135a80d4 100644 --- a/static/app/components/arithmeticBuilder/token/deleteButton.tsx +++ b/static/app/components/arithmeticBuilder/token/deleteButton.tsx @@ -4,7 +4,7 @@ import {IconClose} from '@sentry/icons/close'; import {useArithmeticBuilder} from 'sentry/components/arithmeticBuilder/context'; import type {Token} from 'sentry/components/arithmeticBuilder/token'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {defined} from 'sentry/utils/defined'; interface DeleteButtonProps { diff --git a/static/app/components/events/interfaces/frame/deprecatedLine.tsx b/static/app/components/events/interfaces/frame/deprecatedLine.tsx index 81be87015f56..5530d6bc1b45 100644 --- a/static/app/components/events/interfaces/frame/deprecatedLine.tsx +++ b/static/app/components/events/interfaces/frame/deprecatedLine.tsx @@ -17,7 +17,7 @@ import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfac import {SourceMapsDebuggerModal} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal'; import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; import {getThreadById} from 'sentry/components/events/interfaces/utils'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {StrictClick} from 'sentry/components/strictClick'; import {t, tn} from 'sentry/locale'; import type {Event, Frame} from 'sentry/types/event'; diff --git a/static/app/components/events/interfaces/nativeFrame.tsx b/static/app/components/events/interfaces/nativeFrame.tsx index 0197d25c3593..f2e7e32a25ef 100644 --- a/static/app/components/events/interfaces/nativeFrame.tsx +++ b/static/app/components/events/interfaces/nativeFrame.tsx @@ -16,7 +16,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {StrictClick} from 'sentry/components/strictClick'; import {SLOW_TOOLTIP_DELAY} from 'sentry/constants'; import {t, tn} from 'sentry/locale'; diff --git a/static/app/components/feedback/feedbackItem/feedbackItemSection.tsx b/static/app/components/feedback/feedbackItem/feedbackItemSection.tsx index 46a9be92719f..99d452e58d9e 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItemSection.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItemSection.tsx @@ -4,7 +4,7 @@ import {IconChevron} from '@sentry/icons/chevron'; import {Flex, Stack} from '@sentry/scraps/layout'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {t} from 'sentry/locale'; import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState'; diff --git a/static/app/components/feedback/list/feedbackListItem.tsx b/static/app/components/feedback/list/feedbackListItem.tsx index a8a0ba6631ea..acd51181c0dc 100644 --- a/static/app/components/feedback/list/feedbackListItem.tsx +++ b/static/app/components/feedback/list/feedbackListItem.tsx @@ -10,7 +10,7 @@ import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {TextOverflow} from 'sentry/components/textOverflow'; import {TimeSince} from 'sentry/components/timeSince'; import {UnreadIndicator} from 'sentry/components/unreadIndicator'; diff --git a/static/app/components/group/externalIssuesList/linkedIssueRows.tsx b/static/app/components/group/externalIssuesList/linkedIssueRows.tsx index dbcac82e0523..2587b4c40ae5 100644 --- a/static/app/components/group/externalIssuesList/linkedIssueRows.tsx +++ b/static/app/components/group/externalIssuesList/linkedIssueRows.tsx @@ -7,7 +7,7 @@ import {ExternalLink} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {t} from 'sentry/locale'; import type {GroupIntegrationIssueResult} from './hooks/types'; diff --git a/static/app/components/interactionStateLayer/index.tsx b/static/app/components/interactionStateLayer/index.tsx index 416cb53a48cd..c3fea9abdf0a 100644 --- a/static/app/components/interactionStateLayer/index.tsx +++ b/static/app/components/interactionStateLayer/index.tsx @@ -1 +1 @@ -export {InteractionStateLayer as default} from './interactionStateLayer'; +export {InteractionStateLayer} from './interactionStateLayer'; diff --git a/static/app/components/onboardingWizard/content.tsx b/static/app/components/onboardingWizard/content.tsx index 375f424abed3..6064aaa3d236 100644 --- a/static/app/components/onboardingWizard/content.tsx +++ b/static/app/components/onboardingWizard/content.tsx @@ -13,7 +13,7 @@ import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {navigateTo} from 'sentry/actionCreators/navigation'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {useMutateOnboardingTasks} from 'sentry/components/onboarding/useMutateOnboardingTasks'; import {ProgressRing} from 'sentry/components/progressRing'; import {t, tct} from 'sentry/locale'; diff --git a/static/app/components/preprod/preprodBuildsSnapshotTable.tsx b/static/app/components/preprod/preprodBuildsSnapshotTable.tsx index 7d9b8aa3358a..1f52a5caba16 100644 --- a/static/app/components/preprod/preprodBuildsSnapshotTable.tsx +++ b/static/app/components/preprod/preprodBuildsSnapshotTable.tsx @@ -5,7 +5,7 @@ import {Flex, Stack} from '@sentry/scraps/layout'; import type {TableColumnConfig} from '@sentry/scraps/table'; import {Text} from '@sentry/scraps/text'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {TimeSince} from 'sentry/components/timeSince'; import {t} from 'sentry/locale'; diff --git a/static/app/components/preprod/preprodBuildsTableCommon.tsx b/static/app/components/preprod/preprodBuildsTableCommon.tsx index a137a8e71c02..e2e03a8742ed 100644 --- a/static/app/components/preprod/preprodBuildsTableCommon.tsx +++ b/static/app/components/preprod/preprodBuildsTableCommon.tsx @@ -11,7 +11,7 @@ import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {TimeSince} from 'sentry/components/timeSince'; import {t, tn} from 'sentry/locale'; diff --git a/static/app/components/replays/list/__stories__/replayListItem.tsx b/static/app/components/replays/list/__stories__/replayListItem.tsx index 3999c379c951..16a5c968b642 100644 --- a/static/app/components/replays/list/__stories__/replayListItem.tsx +++ b/static/app/components/replays/list/__stories__/replayListItem.tsx @@ -7,7 +7,7 @@ import {ProjectAvatar, UserAvatar} from '@sentry/scraps/avatar'; import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {TimeSince} from 'sentry/components/timeSince'; import {t} from 'sentry/locale'; import {getShortEventId} from 'sentry/utils/events'; diff --git a/static/app/components/replays/table/replayTable.tsx b/static/app/components/replays/table/replayTable.tsx index a85bfbeecb30..502249fda516 100644 --- a/static/app/components/replays/table/replayTable.tsx +++ b/static/app/components/replays/table/replayTable.tsx @@ -5,7 +5,7 @@ import type {Query} from 'history'; import {Alert} from '@sentry/scraps/alert'; import {Pagination} from '@sentry/scraps/pagination'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t} from 'sentry/locale'; import type {Sort} from 'sentry/utils/discover/fields'; diff --git a/static/app/components/searchQueryBuilder/tokens/boolean.tsx b/static/app/components/searchQueryBuilder/tokens/boolean.tsx index aeeefbb1ca1c..c8b6d96f9deb 100644 --- a/static/app/components/searchQueryBuilder/tokens/boolean.tsx +++ b/static/app/components/searchQueryBuilder/tokens/boolean.tsx @@ -9,7 +9,7 @@ import {IconClose} from '@sentry/icons/close'; import {CompactSelect} from '@sentry/scraps/compactSelect'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import { useSearchQueryBuilderConfig, useSearchQueryBuilderState, diff --git a/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx b/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx index bf079f0879d2..7ccca58f998c 100644 --- a/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx +++ b/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx @@ -6,7 +6,7 @@ import type {ListState} from '@react-stately/list'; import type {Node} from '@react-types/shared'; import {IconClose} from '@sentry/icons/close'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {useSearchQueryBuilderState} from 'sentry/components/searchQueryBuilder/context'; import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderGridItem'; import type { diff --git a/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx b/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx index 8a9d3e227a63..f182be1f591a 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx @@ -5,7 +5,7 @@ import {mergeProps} from '@react-aria/utils'; import type {ListState} from '@react-stately/list'; import type {Node} from '@react-types/shared'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import { useSearchQueryBuilderConfig, useSearchQueryBuilderState, diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx index db0080287bdb..1b20bedc2bef 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx @@ -6,7 +6,7 @@ import type {ListState} from '@react-stately/list'; import type {Node} from '@react-types/shared'; import {IconClose} from '@sentry/icons/close'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import { useSearchQueryBuilderConfig, useSearchQueryBuilderLayout, diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx index 7fdb7b866ff1..9ef7c3dd388b 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx @@ -5,7 +5,7 @@ import {mergeProps} from '@react-aria/utils'; import type {ListState} from '@react-stately/list'; import type {Node} from '@react-types/shared'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context'; import { Token, diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx index ce2206955cbf..f379c667abdf 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx @@ -8,7 +8,7 @@ import type {Node} from '@react-types/shared'; import {CompactSelect} from '@sentry/scraps/compactSelect'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import { useSearchQueryBuilderConfig, useSearchQueryBuilderState, diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx index 0b358d732923..6e966ff13612 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx @@ -13,7 +13,7 @@ import {Container, Flex} from '@sentry/scraps/layout'; import {Overlay} from '@sentry/scraps/overlay'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {AskSeerSetupLoading} from 'sentry/components/searchQueryBuilder/askSeer/askSeerSetupLoading'; import {OpenAskSeerButton} from 'sentry/components/searchQueryBuilder/askSeer/openAskSeerButton'; import { diff --git a/static/app/components/stream/group.tsx b/static/app/components/stream/group.tsx index f362460fde25..12373f3fc285 100644 --- a/static/app/components/stream/group.tsx +++ b/static/app/components/stream/group.tsx @@ -17,7 +17,7 @@ import {AssigneeSelector} from 'sentry/components/group/assigneeSelector'; import {getBadgeProperties} from 'sentry/components/group/inboxBadges/statusBadge'; import {GroupHeaderRow} from 'sentry/components/groupHeaderRow'; import {GroupMetaRow} from 'sentry/components/groupMetaRow'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import type {GroupListColumn} from 'sentry/components/issues/groupList'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {Placeholder} from 'sentry/components/placeholder'; diff --git a/static/app/components/tables/dataGrid/index.tsx b/static/app/components/tables/dataGrid/index.tsx index 584591b5635b..2a2a0d748d21 100644 --- a/static/app/components/tables/dataGrid/index.tsx +++ b/static/app/components/tables/dataGrid/index.tsx @@ -10,7 +10,7 @@ import { type TableColumnConfig, } from '@sentry/scraps/table'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t} from 'sentry/locale'; import {onRenderCallback, Profiler} from 'sentry/utils/performanceForSentry'; diff --git a/static/app/components/tables/simpleTable/index.stories.tsx b/static/app/components/tables/simpleTable/index.stories.tsx index 8eb94b84a46d..82230c3b92fa 100644 --- a/static/app/components/tables/simpleTable/index.stories.tsx +++ b/static/app/components/tables/simpleTable/index.stories.tsx @@ -5,7 +5,7 @@ import moment from 'moment-timezone'; import {Link} from '@sentry/scraps/link'; import type {TableColumnConfig} from '@sentry/scraps/table'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {TimeAgoCell} from 'sentry/components/workflowEngine/gridCell/timeAgoCell'; import {t} from 'sentry/locale'; diff --git a/static/app/components/tables/simpleTable/index.tsx b/static/app/components/tables/simpleTable/index.tsx index 1b563ad7a446..68ee62c56676 100644 --- a/static/app/components/tables/simpleTable/index.tsx +++ b/static/app/components/tables/simpleTable/index.tsx @@ -17,7 +17,7 @@ import {Flex, type FlexProps} from '@sentry/scraps/layout'; import {PanelProvider} from '@sentry/scraps/panelProvider'; import {fullWidthCellStyle, Table, type TableColumnConfig} from '@sentry/scraps/table'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import { type ColumnAlign, HeaderCellContent, diff --git a/static/app/components/tokenizedInput/token/deletableToken.tsx b/static/app/components/tokenizedInput/token/deletableToken.tsx index 4d6ff26b28da..c02feac54b82 100644 --- a/static/app/components/tokenizedInput/token/deletableToken.tsx +++ b/static/app/components/tokenizedInput/token/deletableToken.tsx @@ -5,7 +5,7 @@ import type {ListState} from '@react-stately/list'; import type {Node} from '@react-types/shared'; import {IconClose} from '@sentry/icons/close'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {useGridListItem} from 'sentry/components/tokenizedInput/grid/useGridListItem'; import {focusTarget} from 'sentry/components/tokenizedInput/grid/utils'; import {t} from 'sentry/locale'; diff --git a/static/app/oxlint-suppressions.json b/static/app/oxlint-suppressions.json index 1d93103494c3..606c74422b64 100644 --- a/static/app/oxlint-suppressions.json +++ b/static/app/oxlint-suppressions.json @@ -208,6 +208,11 @@ "count": 3 } }, + "components/core/form/field/rangeField.spec.tsx": { + "@sentry/prefer-fake-timers": { + "count": 1 + } + }, "components/core/relativeTime/relativeTime.spec.tsx": { "@sentry/require-fake-timer-cleanup": { "count": 1 @@ -2679,6 +2684,16 @@ "count": 1 } }, + "views/detectors/detectorNewSettings.spec.tsx": { + "@sentry/prefer-fake-timers": { + "count": 1 + } + }, + "views/detectors/hooks/useDetectorStatsPeriods.spec.tsx": { + "@sentry/prefer-fake-timers": { + "count": 5 + } + }, "views/discover/eventDetails.tsx": { "@sentry/scraps/prefer-primitives": { "count": 1 @@ -2887,6 +2902,9 @@ } }, "views/explore/logs/tables/logsInfiniteTable.spec.tsx": { + "@sentry/prefer-fake-timers": { + "count": 1 + }, "@sentry/require-fake-timer-cleanup": { "count": 1 } diff --git a/static/app/views/automations/components/actions/slack.tsx b/static/app/views/automations/components/actions/slack.tsx index 504f69265bf3..843eb32a33c6 100644 --- a/static/app/views/automations/components/actions/slack.tsx +++ b/static/app/views/automations/components/actions/slack.tsx @@ -33,12 +33,12 @@ export function SlackDetails({ logo: ActionMetadata[ActionType.SLACK]?.icon, workspace: integrationName, channel: action.config.targetDisplay || action.config.targetIdentifier, - tagsAndNotes: SlackTagsAndNotes(action), + tagsAndNotes: , } ); } -function SlackTagsAndNotes(action: Action) { +function SlackTagsAndNotes({action}: {action: Action}) { const notes = action.data.notes; const tags = action.data.tags; diff --git a/static/app/views/automations/components/automationListTable/connectedDetectors.tsx b/static/app/views/automations/components/automationListTable/connectedDetectors.tsx index c0de86932833..1255eb22c55f 100644 --- a/static/app/views/automations/components/automationListTable/connectedDetectors.tsx +++ b/static/app/views/automations/components/automationListTable/connectedDetectors.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import {Stack} from '@sentry/scraps/layout'; import {Hovercard} from 'sentry/components/hovercard'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {LoadingError} from 'sentry/components/loadingError'; import {Placeholder} from 'sentry/components/placeholder'; import {EmptyCell} from 'sentry/components/workflowEngine/gridCell/emptyCell'; diff --git a/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx b/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx index b15621f8bc9f..4dc24eea72b4 100644 --- a/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx @@ -378,37 +378,37 @@ export function SelectRow({ return columnOptionsWithSearched; }, [columnValue, columnOptionsWithSearched]); - const applySpansConditionalFilter = useCallback( - (nextField: QueryFieldValue, nextAggregateName: string): QueryFieldValue => { - if (!isSpansDataset || nextField.kind !== FieldValueKind.FUNCTION) { - return nextField; - } - const existingFilter = conditionalAggregate?.filter; - const filter = supportsConditionalAggregateFilter(nextAggregateName) - ? (existingFilter ?? '') - : ''; - if (!filter && existingFilter === undefined) { - return nextField; - } - // Rebuild from the base aggregate so we do not keep a leftover `_if` name - // or a search-filter argument in the column slot. - const baseField: QueryFieldValue = { - kind: FieldValueKind.FUNCTION, - function: [ - nextAggregateName as AggregationKeyWithAlias, - nextField.function[1] ?? '', - nextField.function[2], - nextField.function[3], - ], - alias: nextField.alias, - }; - return explodeFieldString( - applyConditionalFilter(generateFieldAsString(baseField), filter), - nextField.alias - ); - }, - [conditionalAggregate?.filter, isSpansDataset] - ); + const applySpansConditionalFilter = ( + nextField: QueryFieldValue, + nextAggregateName: string + ): QueryFieldValue => { + if (!isSpansDataset || nextField.kind !== FieldValueKind.FUNCTION) { + return nextField; + } + const existingFilter = conditionalAggregate?.filter; + const filter = supportsConditionalAggregateFilter(nextAggregateName) + ? (existingFilter ?? '') + : ''; + if (!filter && existingFilter === undefined) { + return nextField; + } + // Rebuild from the base aggregate so we do not keep a leftover `_if` name + // or a search-filter argument in the column slot. + const baseField: QueryFieldValue = { + kind: FieldValueKind.FUNCTION, + function: [ + nextAggregateName as AggregationKeyWithAlias, + nextField.function[1] ?? '', + nextField.function[2], + nextField.function[3], + ], + alias: nextField.alias, + }; + return explodeFieldString( + applyConditionalFilter(generateFieldAsString(baseField), filter), + nextField.alias + ); + }; const handleFilterSearch = useCallback( (filter: string) => { diff --git a/static/app/views/detectors/components/detectorListConnectedAutomations.tsx b/static/app/views/detectors/components/detectorListConnectedAutomations.tsx index b6fc0d9227b8..0430b30b4570 100644 --- a/static/app/views/detectors/components/detectorListConnectedAutomations.tsx +++ b/static/app/views/detectors/components/detectorListConnectedAutomations.tsx @@ -7,7 +7,7 @@ import {Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {Hovercard} from 'sentry/components/hovercard'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {LoadingError} from 'sentry/components/loadingError'; import {Placeholder} from 'sentry/components/placeholder'; import {EmptyCell} from 'sentry/components/workflowEngine/gridCell/emptyCell'; diff --git a/static/app/views/explore/replays/selectors/exampleReplaysList.tsx b/static/app/views/explore/replays/selectors/exampleReplaysList.tsx index 5193294eb29f..331f1c5021ff 100644 --- a/static/app/views/explore/replays/selectors/exampleReplaysList.tsx +++ b/static/app/views/explore/replays/selectors/exampleReplaysList.tsx @@ -8,7 +8,7 @@ import {Link} from '@sentry/scraps/link'; import {AnalyticsArea} from 'sentry/components/analyticsArea'; import {EmptyStateWarning} from 'sentry/components/emptyStateWarning'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {ReplayBadge} from 'sentry/components/replays/replayBadge'; import {t} from 'sentry/locale'; diff --git a/static/app/views/inbox/inboxItemList.tsx b/static/app/views/inbox/inboxItemList.tsx index 3a474e2cec70..9411037022d5 100644 --- a/static/app/views/inbox/inboxItemList.tsx +++ b/static/app/views/inbox/inboxItemList.tsx @@ -14,7 +14,7 @@ import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import {Text} from '@sentry/scraps/text'; import {toast} from '@sentry/scraps/toast'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {TimeSince} from 'sentry/components/timeSince'; import {UnreadIndicator} from 'sentry/components/unreadIndicator'; import {t} from 'sentry/locale'; diff --git a/static/app/views/issueList/newViewEmptyState.tsx b/static/app/views/issueList/newViewEmptyState.tsx index def49c8480f0..9b6c5a8c4aab 100644 --- a/static/app/views/issueList/newViewEmptyState.tsx +++ b/static/app/views/issueList/newViewEmptyState.tsx @@ -3,7 +3,7 @@ import {useQueryState} from 'nuqs'; import {Stack} from '@sentry/scraps/layout'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {Panel} from 'sentry/components/panels/panel'; import {ProvidedFormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery'; import {t} from 'sentry/locale'; diff --git a/static/app/views/issueList/pages/inbox/index.tsx b/static/app/views/issueList/pages/inbox/index.tsx index 4d06791857f8..903c23da3535 100644 --- a/static/app/views/issueList/pages/inbox/index.tsx +++ b/static/app/views/issueList/pages/inbox/index.tsx @@ -40,7 +40,7 @@ import { useLinkedPullRequests, } from 'sentry/components/group/externalIssuesList/linkedPullRequests'; import {getPullRequestStatusLabel} from 'sentry/components/group/externalIssuesList/pullRequestStatusBadge'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {LoadingError} from 'sentry/components/loadingError'; import {Placeholder} from 'sentry/components/placeholder'; import {QueryCount} from 'sentry/components/queryCount'; diff --git a/static/app/views/seerExplorer/components/fileDiffViewer.tsx b/static/app/views/seerExplorer/components/fileDiffViewer.tsx index 5f360d8c44a3..a8c44816bb85 100644 --- a/static/app/views/seerExplorer/components/fileDiffViewer.tsx +++ b/static/app/views/seerExplorer/components/fileDiffViewer.tsx @@ -13,7 +13,7 @@ import { type DiffLine, type FilePatch, } from 'sentry/components/events/autofix/types'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {DIFF_COLORS} from 'sentry/components/splitDiff'; import {t} from 'sentry/locale'; import {usePrismTokens} from 'sentry/utils/usePrismTokens'; diff --git a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx index 141b7a2e31df..d0b8af94f5c2 100644 --- a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx @@ -14,7 +14,7 @@ import {Heading, Text} from '@sentry/scraps/text'; import {sentryAppWebhookRequestsApiOptions} from 'sentry/actionCreators/sentryApps'; import {DateTime} from 'sentry/components/dateTime'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {LoadingError} from 'sentry/components/loadingError'; import {PerformanceDuration} from 'sentry/components/performanceDuration'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; diff --git a/static/app/views/settings/organizationTeams/otherTeamsTable.tsx b/static/app/views/settings/organizationTeams/otherTeamsTable.tsx index d9583d40a2c5..acead5941080 100644 --- a/static/app/views/settings/organizationTeams/otherTeamsTable.tsx +++ b/static/app/views/settings/organizationTeams/otherTeamsTable.tsx @@ -6,7 +6,7 @@ import {Flex, useResponsivePropValue} from '@sentry/scraps/layout'; import {openCreateTeamModal} from 'sentry/actionCreators/modal'; import {IdBadge} from 'sentry/components/idBadge'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t, tn} from 'sentry/locale'; import type {Organization, Team} from 'sentry/types/organization'; diff --git a/static/app/views/settings/organizationTeams/yourTeamsTable.tsx b/static/app/views/settings/organizationTeams/yourTeamsTable.tsx index 9ae62fc92ff0..5279b6474298 100644 --- a/static/app/views/settings/organizationTeams/yourTeamsTable.tsx +++ b/static/app/views/settings/organizationTeams/yourTeamsTable.tsx @@ -4,7 +4,7 @@ import {Button} from '@sentry/scraps/button'; import {openCreateTeamModal} from 'sentry/actionCreators/modal'; import {IdBadge} from 'sentry/components/idBadge'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {Placeholder} from 'sentry/components/placeholder'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t, tct, tn} from 'sentry/locale'; diff --git a/static/gsApp/views/seerAutomation/trial.tsx b/static/gsApp/views/seerAutomation/trial.tsx index f34e680a3102..523f1800335e 100644 --- a/static/gsApp/views/seerAutomation/trial.tsx +++ b/static/gsApp/views/seerAutomation/trial.tsx @@ -15,7 +15,7 @@ import {ExternalLink} from '@sentry/scraps/link'; import {Heading, Text} from '@sentry/scraps/text'; import {AnalyticsArea, useAnalyticsArea} from 'sentry/components/analyticsArea'; -import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import {InteractionStateLayer} from 'sentry/components/interactionStateLayer'; import {t, tct} from 'sentry/locale'; import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; import {orgNeedsSeerTrial} from 'sentry/utils/seer/orgNeedsSeerTrial'; diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/css.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/css.ts index a3705f7ddc77..e141ea51c98b 100644 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/css.ts +++ b/static/oxlint/eslintPluginScraps/src/ast/extractor/css.ts @@ -12,7 +12,12 @@ type CssNode = { nodes?: CssNode[]; params?: string; prop?: string; - raws?: {before?: string; between?: string}; + raws?: { + afterName?: string; + before?: string; + between?: string; + params?: {raw?: string}; + }; selector?: string; source?: { end?: {line?: number; offset?: number}; @@ -41,12 +46,12 @@ export interface ParsedCssDeclaration { root: boolean; sourceRange: [number, number] | null; value: string; - valueRange: [number, number] | null; } export interface ParsedCssAtRule { name: string; params: string; + sourceRange: [number, number]; } export interface ParsedCssSelector { @@ -60,17 +65,11 @@ export interface ParsedCssSelector { const styledSyntax: CssParser = createRequire(import.meta.url)('postcss-styled-syntax'); const PREFIX = 'styled.div'; -function valueStart(node: CssNode, source: string): number | undefined { +function valueStart(node: CssNode): number | undefined { const start = node.source?.start?.offset; - if (start === undefined || node.prop === undefined) { - return undefined; - } - const propertyEnd = start + node.prop.length; - if (node.raws?.between) { - return propertyEnd + node.raws.between.length; - } - const colon = source.indexOf(':', propertyEnd); - return colon === -1 ? undefined : colon + 1; + return start === undefined || node.prop === undefined + ? undefined + : start + node.prop.length + (node.raws?.between?.length ?? 0); } function parseCss(source: string): CssDocument | null { @@ -114,6 +113,30 @@ export function cookCssTemplateText(raw: string): string { ); } +export function isDisabledByCssDirective( + {directives, line, sourceRange}: ParsedCssDeclaration | ParsedCssSelector, + ruleId: string +): boolean { + let disabled = false; + for (const directive of directives) { + if (directive.rules.length > 0 && !directive.rules.includes(ruleId)) { + continue; + } + if (directive.action === 'disable-next-line') { + if (directive.endLine !== undefined && line === directive.endLine + 1) { + return true; + } + } else if (directive.action === 'disable-line') { + if (line === directive.line) { + return true; + } + } else if (sourceRange && directive.offset < sourceRange[0]) { + disabled = directive.action === 'disable'; + } + } + return disabled; +} + /** Parse a complete styled template and map each interpolation to its CSS value declaration. */ export function parseCssTemplate( template: ESTree.TemplateLiteral | null, @@ -122,8 +145,7 @@ export function parseCssTemplate( selectors?: ParsedCssSelector[] ): ParsedCssDeclaration[] | null { const templateStart = template?.range[0] ?? 0; - const parsedSource = `${PREFIX}${source}`; - const root = parseCss(parsedSource)?.nodes[0]; + const root = parseCss(`${PREFIX}${source}`)?.nodes[0]; if (!root) { return null; } @@ -166,8 +188,25 @@ export function parseCssTemplate( }), }); } - if (node.type === 'atrule' && node.name && node.params !== undefined) { - atRules?.push({name: node.name, params: node.params}); + if ( + node.type === 'atrule' && + node.name !== undefined && + node.params !== undefined && + node.source?.start?.offset !== undefined + ) { + const start = node.source.start.offset - PREFIX.length; + atRules?.push({ + name: node.name, + params: node.params, + sourceRange: [ + start, + start + + 1 + + node.name.length + + (node.raws?.afterName?.length ?? 0) + + (node.raws?.params?.raw ?? node.params).length, + ], + }); } if (node.type === 'comment' && node.source?.start?.offset !== undefined) { const match = node.text @@ -189,15 +228,11 @@ export function parseCssTemplate( if (node.type === 'decl' && node.prop !== undefined && node.value !== undefined) { const sourceStart = node.source?.start?.offset; const sourceEnd = node.source?.end?.offset; - const start = valueStart(node, parsedSource); + const start = valueStart(node); const sourceRange: [number, number] | null = sourceStart === undefined || sourceEnd === undefined ? null : [sourceStart - PREFIX.length, sourceEnd - PREFIX.length]; - const valueRange: [number, number] | null = - start === undefined || sourceEnd === undefined - ? null - : [start - PREFIX.length, sourceEnd - PREFIX.length]; const interpolations = (template?.expressions ?? []).flatMap( (expression, index) => { if ( @@ -224,7 +259,6 @@ export function parseCssTemplate( root: isRoot, sourceRange, value: node.value, - valueRange, }); } else if (node.nodes) { walk( diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/cssProp.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/cssProp.ts deleted file mode 100644 index 3a6ed4aca795..000000000000 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/cssProp.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type {ESTree} from '@oxlint/plugins'; -/** - * @file Extracts style declarations from JSX css prop patterns. - * - * Handles: - * -
- * -
- * -
- * -
{ ... }} /> - */ - -import {normalizePropertyName} from '../utils/normalizePropertyName.ts'; - -import type {ExtractorContext, StyleDeclaration} from './types.ts'; -import {decomposeValue} from './valueDecomposer.ts'; - -/** - * Creates the css prop extractor with ESLint visitors. - */ -export function createCssPropExtractor({ - collector, - themeTracker, - ruleContext, -}: ExtractorContext) { - /** - * Process an object expression from css={{ ... }} - */ - function processObjectExpression( - objNode: ESTree.ObjectExpression, - sourceNode: ESTree.Node - ) { - for (const prop of objNode.properties) { - if (prop.type !== 'Property') { - continue; - } - - const propertyName = - prop.key.type === 'Identifier' - ? prop.key.name - : prop.key.type === 'Literal' - ? String(prop.key.value) - : null; - - if (!propertyName) { - continue; - } - - const values = decomposeValue(prop.value, themeTracker); - - const declaration: StyleDeclaration = { - kind: 'css-prop', - property: { - name: normalizePropertyName(propertyName), - node: prop.key, - }, - values, - context: { - file: ruleContext.filename, - scopeId: themeTracker.getCurrentScopeId(), - themeBinding: themeTracker.getActiveBinding(), - }, - raw: { - containerNode: objNode, - sourceNode, - }, - }; - - collector.add(declaration); - } - } - - /** - * Process an array of styles css={[...]} - */ - function processArrayExpression( - arrNode: ESTree.ArrayExpression, - sourceNode: ESTree.Node - ) { - for (const element of arrNode.elements) { - if (!element) { - continue; - } - - if (element.type === 'ObjectExpression') { - processObjectExpression(element, sourceNode); - } - // TaggedTemplateExpressions (css`...`) are handled by the styled extractor - } - } - - /** - * Process an arrow function css={(theme) => ...} - */ - function processArrowFunction( - arrowNode: ESTree.ArrowFunctionExpression, - sourceNode: ESTree.Node - ) { - // Register theme parameter binding - const themeParam = arrowNode.params[0]; - if (themeParam?.type === 'Identifier') { - themeTracker.registerCallbackBinding(themeParam.name, arrowNode); - } - - const body = arrowNode.body; - - if (body.type === 'ObjectExpression') { - processObjectExpression(body, sourceNode); - } - // BlockStatement bodies would need return statement analysis - } - - return { - JSXAttribute(node: ESTree.JSXAttribute) { - if (node.name?.name !== 'css') { - return; - } - - const value = node.value; - if (value?.type !== 'JSXExpressionContainer') { - return; - } - - const expr = value.expression; - - if (expr.type === 'ObjectExpression') { - processObjectExpression(expr, node); - } else if (expr.type === 'ArrayExpression') { - processArrayExpression(expr, node); - } else if (expr.type === 'ArrowFunctionExpression') { - processArrowFunction(expr, node); - } - // TaggedTemplateExpressions (css`...`) are handled by styled extractor - }, - }; -} diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/index.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/index.ts index dee6cd819723..678fdec0f039 100644 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/index.ts +++ b/static/oxlint/eslintPluginScraps/src/ast/extractor/index.ts @@ -1,109 +1,185 @@ -import type {ESTree, Context, Visitor} from '@oxlint/plugins'; +import type {Context, ESTree, Visitor} from '@oxlint/plugins'; import {createImportTracker} from '../tracker/imports.ts'; -import {createThemeTracker} from '../tracker/theme.ts'; +import {normalizePropertyName} from '../utils/normalizePropertyName.ts'; +import {getStyledCallInfo} from '../utils/styled.ts'; -import {createCssPropExtractor} from './cssProp.ts'; -import {createStyledExtractor} from './styled.ts'; -import {createStylePropExtractor} from './styleProp.ts'; -import type {ExtractorContext, StyleCollector, StyleDeclaration} from './types.ts'; +import {isDisabledByCssDirective, parseCssTemplate} from './css.ts'; -/** - * @file Aggregates all style extractors and provides the collector factory. - */ +export interface TokenReference { + node: ESTree.MemberExpression; + tokenPath: string; +} + +const THEME_NAMES = new Set(['p', 't']); -/** - * Lightweight regex pre-scan to detect if file needs style analysis. - * Returns false if file has no emotion/styled patterns. - * False positives are acceptable; we just want to skip clearly unrelated files. - */ export function shouldAnalyze(context: Readonly) { const text = context.sourceCode.getText(); + return ( + text.includes('@emotion/styled') || + text.includes('@emotion/react') || + /\b(?:css|style)\s*=/.test(text) + ); +} - // Check for emotion imports OR usage patterns - // We check both because: - // - Files with imports but no usage are rare but possible (re-exports) - // - Files with usage but no direct import may get styled/css from elsewhere - const hasEmotionImport = - text.includes('@emotion/styled') || text.includes('@emotion/react'); - - const hasUsagePattern = - /\buseTheme\b/.test(text) || // useTheme hook - /\bstyled[.(`]/.test(text) || // styled.div, styled(, styled` - /\bcss[`({]/.test(text) || // css`, css(, css{ - /\b(?:css|style)\s*=/.test(text); // css= or style= JSX attrs +export function createStyleVisitors( + context: Context, + check: (property: string, token: TokenReference) => void +): Visitor { + const importTracker = createImportTracker(context); + const themeCallbacks = new Set(); + + function isThemeBinding(node: ESTree.Node): boolean { + const definition = importTracker.resolveVariable(node)?.defs[0]; + const declaration = definition?.node; + if (declaration?.type === 'ArrowFunctionExpression') { + return ( + themeCallbacks.has(declaration) && declaration.params[0] === definition?.name + ); + } + return ( + declaration?.type === 'VariableDeclarator' && + declaration.init?.type === 'CallExpression' && + importTracker.is(declaration.init.callee, '@emotion/react', 'useTheme') + ); + } - return hasEmotionImport || hasUsagePattern; -} + function findTokenReference(node: ESTree.MemberExpression): TokenReference | null { + const path: string[] = []; + let current: ESTree.Node = node; + while ( + current.type === 'MemberExpression' && + current.property.type === 'Identifier' + ) { + path.unshift(current.property.name); + current = current.object; + } + if (current.type === 'Identifier') { + path.unshift(current.name); + } + const tokensIndex = path.indexOf('tokens'); + if ( + tokensIndex === -1 || + tokensIndex === path.length - 1 || + !( + THEME_NAMES.has(path[0] ?? '') || + path.includes('theme') || + (current.type === 'Identifier' && isThemeBinding(current)) + ) + ) { + return null; + } + return {node, tokenPath: path.slice(tokensIndex + 1).join('.')}; + } -/** - * Merge multiple visitor objects, combining handlers for same node types. - */ -function mergeVisitors(...visitorObjects: Visitor[]) { - const merged: Visitor = {}; + function checkValue(property: string, node: ESTree.Node): void { + switch (node.type) { + case 'ConditionalExpression': + checkValue(property, node.consequent); + checkValue(property, node.alternate); + break; + case 'LogicalExpression': + checkValue(property, node.left); + checkValue(property, node.right); + break; + case 'TemplateLiteral': + for (const expression of node.expressions) { + checkValue(property, expression); + } + break; + case 'ArrowFunctionExpression': + if (node.expression) { + themeCallbacks.add(node); + checkValue(property, node.body); + } + break; + case 'MemberExpression': { + const token = findTokenReference(node); + if (token) { + check(property, token); + } + break; + } + default: + break; + } + } - for (const visitors of visitorObjects) { - for (const [nodeType, handler] of Object.entries(visitors)) { - if (!handler) { + function checkObject(node: ESTree.ObjectExpression) { + for (const property of node.properties) { + if ( + property.type !== 'Property' || + (property.computed && property.key.type !== 'Literal') + ) { continue; } - const existing = merged[nodeType] as ((node: ESTree.Node) => void) | undefined; - if (existing) { - merged[nodeType] = (node: ESTree.Node) => { - existing(node); - (handler as (node: ESTree.Node) => void)(node); - }; - } else { - merged[nodeType] = handler; + const name = + property.key.type === 'Identifier' + ? property.key.name + : property.key.type === 'Literal' + ? String(property.key.value) + : ''; + if (name) { + checkValue(normalizePropertyName(name), property.value); } } } - return merged; -} - -/** - * Creates a style collector that aggregates declarations from all extractors. - */ -export function createStyleCollector(context: Context) { - const declarations: StyleDeclaration[] = []; - - const collector: StyleCollector = { - add(decl) { - declarations.push(decl); - }, - getAll() { - return declarations; - }, - clear() { - declarations.length = 0; + return { + TaggedTemplateExpression(node) { + if (!getStyledCallInfo(node, importTracker)) { + return; + } + const declarations = + parseCssTemplate(node.quasi, context.sourceCode.getText(node.quasi)) ?? []; + for (const declaration of declarations) { + if (isDisabledByCssDirective(declaration, context.id)) { + continue; + } + for (const {expression} of declaration.interpolations) { + checkValue(declaration.name, expression); + } + } }, - }; - // Create theme tracker first (extractors depend on it) - const themeTracker = createThemeTracker(); - const importTracker = createImportTracker(context); + CallExpression(node) { + const [argument] = node.arguments; + if ( + argument?.type === 'ObjectExpression' && + getStyledCallInfo(node, importTracker) + ) { + checkObject(argument); + } + }, - // Create extractors with access to collector and theme tracker - const extractorContext: ExtractorContext = { - collector, - themeTracker, - ruleContext: context, - importTracker, + JSXAttribute(node) { + if (node.value?.type !== 'JSXExpressionContainer') { + return; + } + const {expression} = node.value; + if (node.name.name === 'style') { + if (expression.type === 'ObjectExpression') { + checkObject(expression); + } + return; + } + if (node.name.name !== 'css') { + return; + } + if (expression.type === 'ObjectExpression') { + checkObject(expression); + } else if (expression.type === 'ArrayExpression') { + for (const element of expression.elements) { + if (element?.type === 'ObjectExpression') { + checkObject(element); + } + } + } else if (expression.type === 'ArrowFunctionExpression') { + themeCallbacks.add(expression); + if (expression.body.type === 'ObjectExpression') { + checkObject(expression.body); + } + } + }, }; - - const styledVisitors = createStyledExtractor(extractorContext); - const cssPropVisitors = createCssPropExtractor(extractorContext); - const stylePropVisitors = createStylePropExtractor(extractorContext); - - // Merge all visitors - const visitors = mergeVisitors( - importTracker.visitors, - themeTracker.visitors, - styledVisitors, - cssPropVisitors, - stylePropVisitors - ); - - return {collector, visitors, themeTracker}; } diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/styleProp.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/styleProp.ts deleted file mode 100644 index 69c9335e9b90..000000000000 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/styleProp.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type {ESTree, Visitor} from '@oxlint/plugins'; -/** - * @file Extracts style declarations from JSX style prop patterns. - * - * Handles: - * -
- * -
- * -
- */ - -import {normalizePropertyName} from '../utils/normalizePropertyName.ts'; - -import type {ExtractorContext, StyleDeclaration} from './types.ts'; -import {decomposeValue} from './valueDecomposer.ts'; - -/** - * Creates the style prop extractor with ESLint visitors. - */ -export function createStylePropExtractor({ - collector, - themeTracker, - ruleContext, -}: ExtractorContext): Visitor { - /** - * Process an object expression from style={{ ... }} - */ - function processObjectExpression( - objNode: ESTree.ObjectExpression, - sourceNode: ESTree.Node - ) { - for (const prop of objNode.properties) { - if (prop.type !== 'Property') { - continue; - } - - const propertyName = - prop.key.type === 'Identifier' - ? prop.key.name - : prop.key.type === 'Literal' - ? String(prop.key.value) - : null; - - if (!propertyName) { - continue; - } - - const values = decomposeValue(prop.value, themeTracker); - - const declaration: StyleDeclaration = { - kind: 'style-prop', - property: { - name: normalizePropertyName(propertyName), - node: prop.key, - }, - values, - context: { - file: ruleContext.filename, - scopeId: themeTracker.getCurrentScopeId(), - themeBinding: themeTracker.getActiveBinding(), - }, - raw: { - containerNode: objNode, - sourceNode, - }, - }; - - collector.add(declaration); - } - } - - return { - JSXAttribute(node: ESTree.JSXAttribute) { - if (node.name?.name !== 'style') { - return; - } - - const value = node.value; - if (value?.type !== 'JSXExpressionContainer') { - return; - } - - const expr = value.expression; - - if (expr.type === 'ObjectExpression') { - processObjectExpression(expr, node); - } - // Variables and function calls would need flow analysis - }, - }; -} diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/styled.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/styled.ts deleted file mode 100644 index 3c6bb0fd944d..000000000000 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/styled.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type {ESTree, Visitor} from '@oxlint/plugins'; -/** - * @file Extracts style declarations from styled-components/emotion patterns. - * - * Handles: - * - styled.div`...` - * - styled.div({ ... }) - * - styled('div')`...` - * - styled('div')({ ... }) - * - styled(Component)`...` - * - css`...` - */ - -import {normalizePropertyName} from '../utils/normalizePropertyName.ts'; -import {getStyledCallInfo} from '../utils/styled.ts'; - -import {parseCssTemplate} from './css.ts'; -import type {ExtractorContext, StyleDeclaration} from './types.ts'; -import {decomposeValue} from './valueDecomposer.ts'; - -/** - * Creates the styled/css extractor with ESLint visitors. - */ -export function createStyledExtractor({ - collector, - themeTracker, - ruleContext, - importTracker, -}: ExtractorContext): Visitor { - /** - * Check if we're in a lookup table pattern that should be excluded. - * e.g., ({ none: theme.tokens.content.primary })[status] - */ - function isLookupTablePattern(node: ESTree.Node) { - let current = node; - while (current?.parent) { - current = current.parent; - if ( - current.type === 'MemberExpression' && - current.computed && - current.object?.type === 'ObjectExpression' - ) { - return true; - } - } - return false; - } - - /** - * Process a template literal and extract style declarations. - */ - function processTemplateLiteral( - templateNode: ESTree.TemplateLiteral, - sourceNode: ESTree.Node - ) { - const parsed = parseCssTemplate( - templateNode, - ruleContext.sourceCode.getText(templateNode) - ); - if (!parsed) { - return; - } - - for (const declaration of parsed) { - let disabled = false; - let disabledLine = false; - for (const directive of declaration.directives) { - if (directive.rules.length > 0 && !directive.rules.includes(ruleContext.id)) { - continue; - } - if (directive.action === 'disable-next-line') { - disabledLine ||= declaration.line === (directive.endLine ?? -2) + 1; - } else if (directive.action === 'disable-line') { - disabledLine ||= declaration.line === directive.line; - } else if ( - declaration.sourceRange && - directive.offset < declaration.sourceRange[0] - ) { - disabled = directive.action === 'disable'; - } - } - if (disabled || disabledLine) { - continue; - } - for (const {expression, index} of declaration.interpolations) { - const precedingQuasi = templateNode.quasis[index]; - if (!precedingQuasi) { - continue; - } - collector.add({ - kind: 'styled', - property: {name: declaration.name, node: precedingQuasi}, - values: decomposeValue(expression, themeTracker), - context: { - file: ruleContext.filename, - scopeId: themeTracker.getCurrentScopeId(), - themeBinding: themeTracker.getActiveBinding(), - }, - raw: {containerNode: templateNode, sourceNode}, - }); - } - } - } - - /** - * Process an object expression from styled.div({ ... }) syntax. - */ - function processObjectExpression( - objNode: ESTree.ObjectExpression, - sourceNode: ESTree.Node - ) { - // Skip lookup table patterns - if (isLookupTablePattern(objNode)) { - return; - } - - for (const prop of objNode.properties) { - if (prop.type !== 'Property') { - continue; - } - - const propertyName = - prop.key.type === 'Identifier' - ? prop.key.name - : prop.key.type === 'Literal' - ? String(prop.key.value) - : null; - - if (!propertyName) { - continue; - } - - const values = decomposeValue(prop.value, themeTracker); - - const declaration: StyleDeclaration = { - kind: 'styled', - property: { - name: normalizePropertyName(propertyName), - node: prop.key, - }, - values, - context: { - file: ruleContext.filename, - scopeId: themeTracker.getCurrentScopeId(), - themeBinding: themeTracker.getActiveBinding(), - }, - raw: { - containerNode: objNode, - sourceNode, - }, - }; - - collector.add(declaration); - } - } - - return { - TaggedTemplateExpression(node: ESTree.TaggedTemplateExpression) { - if (!getStyledCallInfo(node, importTracker)) { - return; - } - processTemplateLiteral(node.quasi, node); - }, - - // Handle styled.div({ ... }) object syntax - CallExpression(node: ESTree.CallExpression) { - if (!getStyledCallInfo(node, importTracker)) { - return; - } - - // Process object argument - const objectArg = node.arguments[0]; - if (objectArg?.type === 'ObjectExpression') { - processObjectExpression(objectArg, node); - } - }, - }; -} diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/types.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/types.ts deleted file mode 100644 index 89d29a8ad2ca..000000000000 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/types.ts +++ /dev/null @@ -1,234 +0,0 @@ -import type {ESTree, Context, Visitor} from '@oxlint/plugins'; - -import type {createImportTracker} from '../tracker/imports.ts'; -/** - * @file Type definitions for the Style Declaration IR - * - * This module defines the intermediate representation (IR) used by all - * style extractors and consumed by lint rules. - */ - -/** - * Represents a single CSS property declaration found in the source code. - */ -export interface StyleDeclaration { - /** - * Contextual information about where the declaration was found. - */ - context: StyleContext; - - /** - * The type of style context this declaration was found in. - */ - kind: 'styled' | 'css-prop' | 'style-prop' | 'theme'; - - /** - * Information about the CSS property being set. - */ - property: StyleProperty; - - /** - * References to the original AST nodes for error reporting. - */ - raw: StyleRawNodes; - - /** - * All possible values this property could resolve to (handles ternaries, etc.). - */ - values: StyleValue[]; -} - -/** - * Information about a CSS property in a style declaration. - */ -interface StyleProperty { - /** - * Canonical CSS property name in kebab-case (e.g., 'background-color'). - */ - name: string; - - /** - * The AST node representing the property (for error location). - */ - node: ESTree.Node; -} - -/** - * A possible value for a CSS property. - */ -export interface StyleValue { - /** - * Whether this value can be statically analyzed with confidence. - */ - confident: boolean; - - /** - * The type of expression this value comes from. - */ - kind: - | 'literal' - | 'template-quasi' - | 'member' - | 'call' - | 'conditional' - | 'logical' - | 'unknown'; - - /** - * The AST node representing this value. - */ - node: ESTree.Node; - - /** - * If this value is a theme token reference, contains token path info. - */ - tokenInfo: TokenInfo | null; -} - -/** - * Information about a theme token reference. - */ -export interface TokenInfo { - /** - * The member expression node for precise error highlighting. - */ - node: ESTree.Node; - - /** - * The final token name (e.g., 'primary'). - */ - tokenName: string; - - /** - * The full token path (e.g., 'content.primary'). - */ - tokenPath: string; -} - -/** - * Context information for a style declaration. - */ -interface StyleContext { - /** - * The filename where this declaration was found. - */ - file: string; - - /** - * Unique identifier for the scope (for theme binding resolution). - */ - scopeId: number; - - /** - * If a theme binding is active in scope, contains binding info. - */ - themeBinding: ThemeBinding | null; -} - -/** - * Information about a theme variable binding. - */ -export interface ThemeBinding { - /** - * The node where the binding was declared. - */ - declarationNode: ESTree.Node; - - /** - * The local variable name (e.g., 'theme', 't', 'p'). - */ - localName: string; - - /** - * How the theme binding was created. - */ - source: 'useTheme' | 'styled-callback' | 'css-callback'; -} - -/** - * Raw AST node references for a style declaration. - */ -interface StyleRawNodes { - /** - * The containing node (template literal, object expression, etc.). - */ - containerNode: ESTree.Node; - - /** - * The root styled/css/style node. - */ - sourceNode: ESTree.Node; -} - -/** - * The style collector accumulates declarations from all extractors. - */ -export interface StyleCollector { - /** - * Add a style declaration to the collection. - */ - add: (decl: StyleDeclaration) => void; - - /** - * Clear all declarations (call at end of file). - */ - clear: () => void; - - /** - * Get all collected declarations. - */ - getAll: () => StyleDeclaration[]; -} - -/** - * Context passed to extractor factory functions. - */ -export interface ExtractorContext { - /** - * The collector to add declarations to. - */ - collector: StyleCollector; - - /** Resolves Emotion APIs against their lexical imports. */ - importTracker: ReturnType; - - /** - * The ESLint rule context. - */ - ruleContext: Context; - - /** - * The theme binding tracker. - */ - themeTracker: ThemeTracker; -} - -/** - * Interface for the theme tracker. - */ -export interface ThemeTracker { - /** - * Get the currently active theme binding. - */ - getActiveBinding: () => ThemeBinding | null; - - /** - * Get the current scope ID. - */ - getCurrentScopeId: () => number; - - /** - * Check if a name is a known theme binding. - */ - isThemeBinding: (name: string) => boolean; - - /** - * Register a callback parameter as a theme binding. - */ - registerCallbackBinding: (name: string, node: ESTree.Node) => void; - - /** - * ESLint visitor functions for tracking theme bindings. - */ - visitors: Visitor; -} diff --git a/static/oxlint/eslintPluginScraps/src/ast/extractor/valueDecomposer.ts b/static/oxlint/eslintPluginScraps/src/ast/extractor/valueDecomposer.ts deleted file mode 100644 index 2c4f64bce409..000000000000 --- a/static/oxlint/eslintPluginScraps/src/ast/extractor/valueDecomposer.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type {ESTree} from '@oxlint/plugins'; -/** - * @file Value decomposition for style expressions. - * - * Extracts all possible values from complex expressions like ternaries, - * logical expressions, and nested member expressions. - */ - -import type {StyleValue, ThemeTracker, TokenInfo} from './types.ts'; - -/** - * Decompose an expression into all possible StyleValue entries. - */ -export function decomposeValue(node: ESTree.Node, themeTracker: ThemeTracker) { - const values: StyleValue[] = []; - - collectValues(node, values, themeTracker); - - return values; -} - -/** - * Recursively collect values from an expression. - */ -function collectValues( - node: ESTree.Node, - values: StyleValue[], - themeTracker: ThemeTracker -) { - if (!node || typeof node !== 'object') { - return; - } - - switch (node.type) { - case 'ConditionalExpression': - // Ternary: condition ? consequent : alternate - // Both branches are possible values - collectValues(node.consequent, values, themeTracker); - collectValues(node.alternate, values, themeTracker); - break; - - case 'LogicalExpression': - // foo || bar, foo && bar - // Both operands are potential values - collectValues(node.left, values, themeTracker); - collectValues(node.right, values, themeTracker); - break; - - case 'MemberExpression': - // Check if this is a theme token reference - values.push(createMemberValue(node, themeTracker)); - break; - - case 'Literal': - values.push({ - node, - kind: 'literal', - confident: true, - tokenInfo: null, - }); - break; - - case 'TemplateLiteral': - // Template literals might contain interpolations - if (node.expressions.length === 0) { - values.push({ - node, - kind: 'template-quasi', - confident: true, - tokenInfo: null, - }); - } else { - // Has interpolations - check each expression - for (const expr of node.expressions) { - collectValues(expr, values, themeTracker); - } - } - break; - - case 'CallExpression': - values.push({ - node, - kind: 'call', - confident: false, - tokenInfo: null, - }); - break; - - case 'ArrowFunctionExpression': - // Arrow functions like `p => p.theme.tokens.content.primary` - // Need to analyze the body/return value - if (node.expression && node.body) { - // Implicit return: (p) => p.theme.tokens... - // Register the parameter as a potential theme binding - const param = node.params[0]; - if (param?.type === 'Identifier') { - themeTracker.registerCallbackBinding(param.name, node); - } - collectValues(node.body, values, themeTracker); - } - break; - - case 'Identifier': - // Could be a variable reference - would need scope analysis - values.push({ - node, - kind: 'unknown', - confident: false, - tokenInfo: null, - }); - break; - - default: - values.push({ - node, - kind: 'unknown', - confident: false, - tokenInfo: null, - }); - } -} - -/** - * Create a StyleValue for a MemberExpression, extracting token info if present. - */ -function createMemberValue( - node: ESTree.MemberExpression, - themeTracker: ThemeTracker -): StyleValue { - const tokenInfo = extractTokenInfo(node, themeTracker); - return { - node, - kind: 'member', - confident: tokenInfo !== null, - tokenInfo, - }; -} - -/** - * Extract token information from a MemberExpression if it's a theme token. - */ -function extractTokenInfo( - node: ESTree.MemberExpression, - themeTracker: ThemeTracker -): TokenInfo | null { - const pathParts: string[] = []; - let current: ESTree.Node = node; - - // Walk up the member expression chain - while (current.type === 'MemberExpression' && current.property?.type === 'Identifier') { - pathParts.unshift(current.property.name); - current = current.object; - } - - // Check the base identifier - if (current.type === 'Identifier') { - pathParts.unshift(current.name); - } - - const tokensIndex = pathParts.indexOf('tokens'); - if (tokensIndex === -1 || tokensIndex >= pathParts.length - 1) { - return null; - } - - // Verify the base is a known theme binding or common convention - const base = pathParts[0]; - if (!base) { - return null; - } - const hasThemeInPath = pathParts.includes('theme'); - const isKnownBinding = themeTracker.isThemeBinding(base); - const isCommonConvention = ['theme', 'p', 't'].includes(base); - - if (!isKnownBinding && !isCommonConvention && !hasThemeInPath) { - return null; - } - - const tokenPath = pathParts.slice(tokensIndex + 1).join('.'); - const tokenName = pathParts[pathParts.length - 1]; - if (!tokenName) { - return null; - } - - return { - tokenPath, - tokenName, - node, - }; -} diff --git a/static/oxlint/eslintPluginScraps/src/ast/tracker/imports.spec.ts b/static/oxlint/eslintPluginScraps/src/ast/tracker/imports.spec.ts index 3f00de44477d..3343bd4255c5 100644 --- a/static/oxlint/eslintPluginScraps/src/ast/tracker/imports.spec.ts +++ b/static/oxlint/eslintPluginScraps/src/ast/tracker/imports.spec.ts @@ -21,8 +21,6 @@ const testRule = defineRule({ const tracker = createImportTracker(context); return { - ...tracker.visitors, - // Test resolve() by checking JSX element names JSXOpeningElement(node: ESTree.JSXOpeningElement) { if (node.name.type === 'JSXIdentifier') { @@ -43,7 +41,6 @@ const testRule = defineRule({ } }, - // Test findLocalNames() via a special comment trigger 'Program:exit'() { const localNames = tracker.findLocalNames('@sentry/scraps/layout', 'Flex'); if (localNames.length > 0) { @@ -106,34 +103,108 @@ ruleTester.run('createImportTracker', testRule, { errors: [{messageId: 'unresolved', data: {name: 'Unknown'}}], }, { - // findLocalNames works — Program:exit fires after JSXOpeningElement code: ` - import {Flex} from '@sentry/scraps/layout'; - const x = ; + import {Flex as F, Flex} from '@sentry/scraps/layout'; + import type {Flex as T} from '@sentry/scraps/layout'; + import {type Flex as U} from '@sentry/scraps/layout'; + import {Flex as O} from '@sentry/scraps/other'; + `, + filename: '/project/src/file.tsx', + errors: [{messageId: 'found', data: {localNames: 'F,Flex'}}], + }, + { + code: ` + const x =