From 61078d26178c613ff55a68fd310389fb6d09868c Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 3 Jul 2026 14:33:12 -0600 Subject: [PATCH] chore(studio): add jsx-a11y ESLint ratchet rules for statically-detectable a11y issues (#47582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Follow-up to the axe-core accessibility audit (FE-3781), which found 1,733 failing elements across 126 Studio surfaces deduplicating to 12 root-cause families. A subset of those (missing accessible names/labels, invalid/redundant ARIA, empty headings/anchors) is statically detectable — this adds ESLint coverage for it instead of relying solely on the runtime axe-core CI gate. - Adds 13 `jsx-a11y` rules to `apps/studio/eslint.config.cjs` at `'warn'`: `aria-props`, `aria-proptypes`, `role-supports-aria-props`, `anchor-has-content`, `control-has-associated-label` (`controlComponents: ['Button', 'Switch']`), `label-has-associated-control` (`labelComponents: ['Label']`, `controlComponents: ['Input', 'Switch']`), `aria-role`, `no-redundant-roles`, `no-aria-hidden-on-focusable`, `tabindex-no-positive`, `anchor-is-valid`, `heading-has-content`, `no-distracting-elements`. - Wires all 13 into the existing `lint:ratchet` script and initializes their baselines in `apps/studio/.github/eslint-rule-baselines.json`, so any *new* violation fails `studio-lint-ratchet.yml` while the pre-existing ones (mostly `control-has-associated-label`: 274, `label-has-associated-control`: 37) are tracked and shrink over time via the weekly baseline-decrease cron. Resolves [FE-3795](https://linear.app/supabase/issue/FE-3795/add-jsx-a11y-eslint-ratchet-rules-for-statically-detectable-a11y). ## Test plan - [x] `pnpm --filter studio run lint:ratchet` passes (exit 0, no regressions) - [x] Spot-checked several flagged instances against source to confirm true positives (e.g. an unlabeled save/cancel icon-button pair in `AIAssistantChatSelector.tsx`, an empty `

` in `PITRForm.tsx`) - [x] CI (`studio-lint-ratchet.yml`, typecheck.yml lint step) green on this PR ## Summary by CodeRabbit * **Bug Fixes** * Expanded Studio’s accessibility linting to cover additional ARIA prop validation, label/control relationships, anchor/heading validity, role/ARIA correctness, and focus/tab behavior (including distracting markup). * Updated accessibility lint baselines so tracked violations remain accurate as rules expand. * **New Features** * Enhanced the Studio lint “ratchet” workflow to load ratchet rule IDs from an external `rules-file` instead of a long inline command. * **Tests** * Added an integration test to verify rule IDs are read from the `rules-file`. --- .../studio/.github/eslint-rule-baselines.json | 281 +++++++++++++++++- apps/studio/eslint.config.cjs | 16 + apps/studio/package.json | 2 +- .../__tests__/ratchet-eslint-rules.test.ts | 22 ++ apps/studio/scripts/ratchet-eslint-rules.ts | 39 ++- apps/studio/scripts/ratchet-rules.json | 22 ++ 6 files changed, 377 insertions(+), 5 deletions(-) create mode 100644 apps/studio/scripts/ratchet-rules.json diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index e8b45f1f023..98476e55426 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -6,7 +6,20 @@ "@typescript-eslint/no-explicit-any": 901, "no-restricted-imports": 0, "no-restricted-exports": 221, - "react/no-unstable-nested-components": 39 + "react/no-unstable-nested-components": 39, + "jsx-a11y/aria-props": 0, + "jsx-a11y/aria-proptypes": 0, + "jsx-a11y/role-supports-aria-props": 0, + "jsx-a11y/anchor-has-content": 0, + "jsx-a11y/control-has-associated-label": 274, + "jsx-a11y/label-has-associated-control": 37, + "jsx-a11y/aria-role": 0, + "jsx-a11y/no-redundant-roles": 4, + "jsx-a11y/no-aria-hidden-on-focusable": 0, + "jsx-a11y/tabindex-no-positive": 0, + "jsx-a11y/anchor-is-valid": 7, + "jsx-a11y/heading-has-content": 3, + "jsx-a11y/no-distracting-elements": 0 }, "ruleFiles": { "react-hooks/exhaustive-deps": { @@ -840,6 +853,270 @@ "components/ui/ErrorBoundary/ErrorBoundary.tsx": 1, "components/ui/NoPermission.tsx": 1, "pages/project/[ref]/observability/database.tsx": 1 - } + }, + "jsx-a11y/aria-props": {}, + "jsx-a11y/aria-proptypes": {}, + "jsx-a11y/role-supports-aria-props": {}, + "jsx-a11y/anchor-has-content": {}, + "jsx-a11y/control-has-associated-label": { + "components/grid/components/editor/DateTimeEditor.tsx": 1, + "components/grid/components/editor/NumberEditor.tsx": 1, + "components/grid/components/editor/TimeEditor.tsx": 1, + "components/grid/components/footer/pagination/Pagination.tsx": 3, + "components/grid/components/grid/AddColumn.tsx": 1, + "components/grid/components/header/filter/FilterRow.tsx": 1, + "components/grid/components/header/sort/SortRow.tsx": 2, + "components/interfaces/APIKeys/APIKeyRow.tsx": 1, + "components/interfaces/Account/AccessTokens/AccessTokenList.tsx": 1, + "components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner.tsx": 1, + "components/interfaces/Account/AccessTokens/Classic/NewTokenButton.tsx": 1, + "components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx": 1, + "components/interfaces/Account/AccessTokens/Scoped/Form/ResourceAccess/ResourceOption.tsx": 1, + "components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList.tsx": 1, + "components/interfaces/Account/Preferences/AnalyticsSettings.tsx": 1, + "components/interfaces/Account/Preferences/DashboardToggle.tsx": 1, + "components/interfaces/Account/Preferences/HotkeyToggle.tsx": 1, + "components/interfaces/Advisors/CreateRuleSheet.tsx": 1, + "components/interfaces/Auth/AuditLogsForm.tsx": 1, + "components/interfaces/Auth/AuthProvidersForm/FormField.tsx": 1, + "components/interfaces/Auth/BasicAuthSettingsForm.tsx": 4, + "components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx": 1, + "components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx": 2, + "components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx": 1, + "components/interfaces/Auth/Hooks/CreateHookSheet.tsx": 1, + "components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx": 1, + "components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx": 2, + "components/interfaces/Auth/OAuthApps/NewOAuthAppBanner.tsx": 1, + "components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx": 2, + "components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx": 2, + "components/interfaces/Auth/Passkeys/PasskeysSettingsForm.tsx": 1, + "components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx": 1, + "components/interfaces/Auth/RateLimits/RateLimits.tsx": 1, + "components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx": 2, + "components/interfaces/Auth/SmtpForm/SmtpForm.tsx": 1, + "components/interfaces/Auth/Users/UserPanel.tsx": 1, + "components/interfaces/Auth/Users/UsersFooter.tsx": 1, + "components/interfaces/Auth/Users/UsersGridComponents.tsx": 1, + "components/interfaces/Auth/Users/UsersSearch.tsx": 1, + "components/interfaces/BranchManagement/CreateBranchModal.tsx": 1, + "components/interfaces/BranchManagement/Overview.tsx": 2, + "components/interfaces/BranchManagement/WorkflowLogsCard.tsx": 1, + "components/interfaces/Branching/MergeRequest.tsx": 1, + "components/interfaces/ConnectSheet/ConnectConfigSection.tsx": 1, + "components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx": 1, + "components/interfaces/Database/Extensions/ExtensionRow.tsx": 1, + "components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx": 1, + "components/interfaces/Database/Functions/CreateFunction/index.tsx": 2, + "components/interfaces/Database/Hooks/HooksList/HookList.tsx": 1, + "components/interfaces/Database/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx": 1, + "components/interfaces/Database/Policies/PolicyTableRow/PolicyRow.tsx": 1, + "components/interfaces/Database/Privileges/PrivilegesTable.tsx": 2, + "components/interfaces/Database/Publications/PublicationSkeleton.tsx": 1, + "components/interfaces/Database/Publications/PublicationsList.tsx": 1, + "components/interfaces/Database/Publications/PublicationsTableItem.tsx": 1, + "components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AnalyticsBucket/Fields.tsx": 2, + "components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DuckLake/Fields.tsx": 2, + "components/interfaces/Database/Replication/DestinationPanel/DestinationForm/Snowflake/Fields.tsx": 1, + "components/interfaces/Database/Replication/Destinations.tsx": 2, + "components/interfaces/Database/Replication/ReadReplicas/ReadReplicaRow.tsx": 1, + "components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx": 1, + "components/interfaces/Database/Replication/RowMenu.tsx": 1, + "components/interfaces/Database/Roles/CreateRolePanel.tsx": 2, + "components/interfaces/Database/Roles/RoleRow.tsx": 1, + "components/interfaces/Database/Tables/ColumnList.tsx": 1, + "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx": 1, + "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx": 1, + "components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx": 1, + "components/interfaces/Home/ProjectList/ProjectCard.tsx": 1, + "components/interfaces/HomePageActions.tsx": 1, + "components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CronJobScheduleSection.tsx": 1, + "components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx": 2, + "components/interfaces/Integrations/CronJobs/CronJobsTab.Header.tsx": 1, + "components/interfaces/Integrations/Queues/CreateQueueSheet/RlsSection.tsx": 1, + "components/interfaces/Integrations/Queues/QueuesSettings.tsx": 1, + "components/interfaces/Integrations/Queues/QueuesTab.tsx": 1, + "components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx": 1, + "components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx": 1, + "components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx": 2, + "components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1, + "components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx": 2, + "components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx": 2, + "components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx": 1, + "components/interfaces/Linter/LintDetail.tsx": 1, + "components/interfaces/Linter/LinterDataGrid.tsx": 1, + "components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx": 3, + "components/interfaces/LogDrains/LogDrainsCard.tsx": 1, + "components/interfaces/LogDrains/LogDrainsList.tsx": 1, + "components/interfaces/LogDrains/OrgAuditLogDrains.tsx": 1, + "components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx": 1, + "components/interfaces/Organization/BillingSettings/BillingBreakdown/ComputeMetric.tsx": 1, + "components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx": 1, + "components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx": 1, + "components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 1, + "components/interfaces/Organization/NewOrg/NewOrgForm.tsx": 1, + "components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx": 1, + "components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx": 1, + "components/interfaces/Organization/OAuthApps/OAuthApps.tsx": 1, + "components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx": 1, + "components/interfaces/Organization/PrivateApps/Apps/AppsList/AppsListTable.tsx": 1, + "components/interfaces/Organization/PrivateApps/Apps/CreateAppSheet/CreateAppSheet.tsx": 3, + "components/interfaces/Organization/PrivateApps/Apps/ViewAppSheet/ViewAppSheet.tsx": 1, + "components/interfaces/Organization/SSO/JoinOrganizationOnSignup.tsx": 1, + "components/interfaces/Organization/SSO/SSOConfig.tsx": 2, + "components/interfaces/Organization/SSO/SSODomains.tsx": 1, + "components/interfaces/Organization/SSO/SSOMetadata.tsx": 1, + "components/interfaces/Organization/SecuritySettings.tsx": 1, + "components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx": 1, + "components/interfaces/Organization/TeamSettings/MemberActions.tsx": 1, + "components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx": 1, + "components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointList.tsx": 1, + "components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointSheet.tsx": 1, + "components/interfaces/Platform/Webhooks/PlatformWebhooksPage.tsx": 1, + "components/interfaces/ProjectAPIDocs/SecondLevelNav.Layout.tsx": 1, + "components/interfaces/ProjectCreation/HighAvailabilityInput.tsx": 1, + "components/interfaces/QueryInsights/QueryInsightsTable/QueryInsightsTable.tsx": 1, + "components/interfaces/QueryInsights/hooks/useQueryInsightsTableColumns.tsx": 2, + "components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx": 1, + "components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx": 2, + "components/interfaces/QueryPerformance/components/FilterInput.tsx": 1, + "components/interfaces/Realtime/Inspector/ChooseChannelPopover.tsx": 1, + "components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx": 3, + "components/interfaces/Realtime/RealtimeSettings.tsx": 2, + "components/interfaces/Reports/ReportFilterBar.tsx": 1, + "components/interfaces/Reports/ReportWidget.tsx": 1, + "components/interfaces/RoleImpersonationSelector/RoleImpersonationRadio.tsx": 1, + "components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx": 1, + "components/interfaces/SQLEditor/OngoingQueriesPanel.tsx": 2, + "components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx": 1, + "components/interfaces/Settings/API/DataApiEnableSwitchForm.tsx": 1, + "components/interfaces/Settings/API/ExposedFunctionSelector.tsx": 1, + "components/interfaces/Settings/API/ExposedSchemaSelector.tsx": 1, + "components/interfaces/Settings/API/ExposedTableSelector.tsx": 1, + "components/interfaces/Settings/API/PostgrestConfig.tsx": 1, + "components/interfaces/Settings/Database/ConnectionLogging.tsx": 2, + "components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration.tsx": 1, + "components/interfaces/Settings/Database/SSLConfiguration.tsx": 1, + "components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx": 1, + "components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx": 1, + "components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 1, + "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx": 3, + "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx": 2, + "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 1, + "components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx": 1, + "components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx": 3, + "components/interfaces/Settings/Integrations/VercelIntegration/VercelIntegrationConnectionForm.tsx": 3, + "components/interfaces/Settings/Logs/MultiSelectActionBar.tsx": 1, + "components/interfaces/Settings/Logs/PreviewFilterPanel.tsx": 1, + "components/interfaces/SignIn/ResetPasswordForm.tsx": 2, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx": 1, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/DecryptedReadOnlyInput.tsx": 2, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 3, + "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx": 1, + "components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx": 4, + "components/interfaces/Storage/CreateBucketModal.tsx": 3, + "components/interfaces/Storage/EditBucketModal.tsx": 3, + "components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx": 2, + "components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx": 1, + "components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 2, + "components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx": 1, + "components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketsSection.tsx": 1, + "components/interfaces/Storage/StorageSettings/S3Connection.tsx": 1, + "components/interfaces/Storage/StorageSettings/StorageCredItem.tsx": 1, + "components/interfaces/Storage/StorageSettings/StorageSettings.tsx": 1, + "components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx": 1, + "components/interfaces/Storage/VectorBuckets/VectorBucketDetails/index.tsx": 1, + "components/interfaces/Support/AttachmentUpload.tsx": 1, + "components/interfaces/Support/DashboardLogsToggle.tsx": 1, + "components/interfaces/Support/SupportAccessToggle.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 4, + "components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/Pagination.tsx": 2, + "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx": 1, + "components/interfaces/UnifiedLogs/ServiceFlow/components/ServiceFlowPanelControls.tsx": 3, + "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/DetailRow.tsx": 1, + "components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 2, + "components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx": 2, + "components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1, + "components/layouts/ObservabilityLayout/ObservabilityMenuItem.tsx": 1, + "components/layouts/ProjectLayout/PauseFailedState.tsx": 1, + "components/layouts/ProjectLayout/RestoreFailedState.tsx": 1, + "components/layouts/TableEditorLayout/EntityListItem.tsx": 1, + "components/ui/AIAssistantPanel/AIAssistantChatSelector.tsx": 4, + "components/ui/AIAssistantPanel/CollapsibleCodeBlock.tsx": 1, + "components/ui/AdvisorPanel/AdvisorPanelBody.tsx": 1, + "components/ui/AdvisorPanel/AdvisorSignalDetail.tsx": 1, + "components/ui/AiAssistantDropdown.tsx": 1, + "components/ui/Charts/ReportSettings.tsx": 2, + "components/ui/EditorPanel/EditorPanel.tsx": 1, + "components/ui/Logs/LogsExplorerHeader.tsx": 1, + "components/ui/PasswordStrengthBar.tsx": 1, + "components/ui/Resource/ResourceItem.tsx": 1, + "pages/project/[ref]/auth/templates/[templateId].tsx": 1, + "pages/project/[ref]/branches/merge-requests.tsx": 1, + "pages/project/[ref]/database/policies.tsx": 1, + "pages/project/[ref]/functions/index.tsx": 1, + "pages/project/[ref]/observability/auth.tsx": 1, + "pages/project/[ref]/observability/database.tsx": 1, + "pages/project/[ref]/observability/edge-functions.tsx": 1, + "pages/project/[ref]/observability/postgrest.tsx": 1, + "pages/project/[ref]/observability/realtime.tsx": 1, + "pages/project/[ref]/observability/storage.tsx": 1, + "pages/project/[ref]/settings/log-drains.tsx": 1 + }, + "jsx-a11y/label-has-associated-control": { + "components/grid/components/header/sort/SortRow.tsx": 1, + "components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx": 1, + "components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx": 1, + "components/interfaces/BranchManagement/CreateBranchModal.tsx": 2, + "components/interfaces/BranchManagement/EditBranchModal.tsx": 1, + "components/interfaces/Docs/Param.tsx": 3, + "components/interfaces/Docs/ResourceContent.tsx": 1, + "components/interfaces/Docs/RpcContent.tsx": 1, + "components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx": 1, + "components/interfaces/Organization/PrivateApps/Apps/CreateAppSheet/CreateAppSheet.tsx": 1, + "components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointSheet.tsx": 1, + "components/interfaces/Platform/Webhooks/PlatformWebhooksPage.tsx": 1, + "components/interfaces/Realtime/Inspector/ChooseChannelPopover.tsx": 1, + "components/interfaces/Reports/v2/ReportsNumericFilter.tsx": 2, + "components/interfaces/SQLEditor/MoveQueryModal.tsx": 1, + "components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx": 2, + "components/interfaces/Settings/General/CustomDomainConfig/DNSTableHeaders.tsx": 2, + "components/interfaces/Settings/Logs/LogsFormatters.tsx": 2, + "components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerPreviewPane.tsx": 2, + "components/interfaces/Storage/StorageExplorer/PreviewPane.tsx": 2, + "components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatePreview.tsx": 1, + "components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 1, + "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx": 1, + "components/ui/Charts/ComposedChart.tsx": 1, + "components/ui/Charts/ComposedChart.utils.tsx": 2 + }, + "jsx-a11y/aria-role": {}, + "jsx-a11y/no-redundant-roles": { + "components/interfaces/Organization/BillingSettings/Subscription/EnterpriseCard.tsx": 1, + "components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx": 1, + "components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx": 2 + }, + "jsx-a11y/no-aria-hidden-on-focusable": {}, + "jsx-a11y/tabindex-no-positive": {}, + "jsx-a11y/anchor-is-valid": { + "components/interfaces/BranchManagement/Overview.tsx": 1, + "components/interfaces/Organization/ProjectClaim/confirm.tsx": 1, + "components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx": 2, + "components/interfaces/SignIn/SignInMfaForm.tsx": 1, + "components/interfaces/TableGridEditor/GridHeaderActions.tsx": 1, + "components/layouts/Navigation/LayoutHeader/BreadcrumbsView.tsx": 1 + }, + "jsx-a11y/heading-has-content": { + "components/interfaces/Database/Backups/PITR/PITRForm.tsx": 1, + "components/layouts/Scaffold.tsx": 2 + }, + "jsx-a11y/no-distracting-elements": {} } } diff --git a/apps/studio/eslint.config.cjs b/apps/studio/eslint.config.cjs index 83e90b2ae12..c61b3261ebb 100644 --- a/apps/studio/eslint.config.cjs +++ b/apps/studio/eslint.config.cjs @@ -62,6 +62,22 @@ module.exports = defineConfig([ 'barrel-files/avoid-re-export-all': 'error', 'jsx-a11y/alt-text': 'warn', 'jsx-a11y/role-has-required-aria-props': 'error', + 'jsx-a11y/aria-props': 'warn', + 'jsx-a11y/aria-proptypes': 'warn', + 'jsx-a11y/role-supports-aria-props': 'warn', + 'jsx-a11y/anchor-has-content': 'warn', + 'jsx-a11y/control-has-associated-label': ['warn', { controlComponents: ['Button', 'Switch'] }], + 'jsx-a11y/label-has-associated-control': [ + 'warn', + { labelComponents: ['Label'], controlComponents: ['Input', 'Switch'] }, + ], + 'jsx-a11y/aria-role': 'warn', + 'jsx-a11y/no-redundant-roles': 'warn', + 'jsx-a11y/no-aria-hidden-on-focusable': 'warn', + 'jsx-a11y/tabindex-no-positive': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + 'jsx-a11y/heading-has-content': 'warn', + 'jsx-a11y/no-distracting-elements': 'warn', }, }, // Analytics SQL wire boundary: every call to a SQL-bearing analytics diff --git a/apps/studio/package.json b/apps/studio/package.json index 6e33001d93c..7da743e96fe 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -18,7 +18,7 @@ "smoke:tanstack": "node scripts/smoke-server.mjs", "start:tanstack": "node scripts/serve.js", "lint": "eslint .", - "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @typescript-eslint/no-explicit-any --rule no-restricted-imports --rule no-restricted-exports --rule react/no-unstable-nested-components", + "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rules-file scripts/ratchet-rules.json", "clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo", "test": "vitest --run --coverage", "test:watch": "vitest watch", diff --git a/apps/studio/scripts/__tests__/ratchet-eslint-rules.test.ts b/apps/studio/scripts/__tests__/ratchet-eslint-rules.test.ts index 247dcd57446..18da11ee685 100644 --- a/apps/studio/scripts/__tests__/ratchet-eslint-rules.test.ts +++ b/apps/studio/scripts/__tests__/ratchet-eslint-rules.test.ts @@ -83,6 +83,28 @@ describe('ratchet-eslint-rules integration', () => { expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/b.ts')} (+1)`) }) + it('reads rule ids from --rules-file', () => { + const tmp = createTempDir() + const metadataPath = path.join(tmp, 'baseline.json') + const rulesFilePath = path.join(tmp, 'rules.json') + writeFileSync(rulesFilePath, JSON.stringify(['no-console', 'no-debugger'])) + + const eslintResults = buildEslintResults([ + { filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 1, 'no-debugger': 2 } }, + ]) + + const result = invokeRatchet( + ['--metadata', metadataPath, '--rules-file', rulesFilePath, '--init'], + eslintResults + ) + + expect(result).toBe(0) + + const metadata = JSON.parse(readFileSync(metadataPath, 'utf8')) + expect(metadata.rules['no-console']).toBe(1) + expect(metadata.rules['no-debugger']).toBe(2) + }) + it('falls back gracefully when baseline is missing per-file data', () => { const tmp = createTempDir() const metadataPath = path.join(tmp, 'baseline.json') diff --git a/apps/studio/scripts/ratchet-eslint-rules.ts b/apps/studio/scripts/ratchet-eslint-rules.ts index e516aa446ad..eed8c1b3498 100644 --- a/apps/studio/scripts/ratchet-eslint-rules.ts +++ b/apps/studio/scripts/ratchet-eslint-rules.ts @@ -21,7 +21,9 @@ * --init Write current counts for the provided --rule(s) into metadata and exit 0 * --eslint "" ESLint command to run (default "npx eslint"). Do not pass untrusted input. * --eslint-args "<...>" Extra args/paths for ESLint (e.g., "."). Do not pass untrusted input. - * --rule [,...] Rule id(s). Repeat flag or comma-separate. REQUIRED. + * --rule [,...] Rule id(s). Repeat flag or comma-separate. + * --rules-file Path to a JSON file containing an array of rule id strings. + * Combines with --rule if both are given. One of the two is REQUIRED. * --decrease-baselines When improvements occur, lower stored baselines to match the new counts. * * Notes: @@ -41,6 +43,7 @@ interface Args { eslintArgs: string decreaseBaselines: boolean rules: string[] + rulesFile?: string } interface ESLintMessage { @@ -67,6 +70,31 @@ interface RuleSnapshot { files: Record } +function readRulesFile(filePath: string): string[] { + let raw: string + try { + raw = readFileSync(filePath, 'utf8') + } catch (e) { + console.error(`Error: Could not read --rules-file ${filePath}: ${e}`) + process.exit(2) + } + + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch (e) { + console.error(`Error: Could not parse --rules-file ${filePath} as JSON: ${e}`) + process.exit(2) + } + + if (!Array.isArray(parsed) || !parsed.every((r) => typeof r === 'string')) { + console.error(`Error: --rules-file ${filePath} must contain a JSON array of rule id strings.`) + process.exit(2) + } + + return parsed +} + function parseArgs(argv: string[]): Args { const args: Args = { metadata: '.github/eslint-rule-baselines.json', @@ -97,6 +125,8 @@ function parseArgs(argv: string[]): Args { .filter(Boolean) ) } + } else if (a === '--rules-file') { + args.rulesFile = argv[++i] } else if (a === '--decrease-baselines') { args.decreaseBaselines = true } else { @@ -104,9 +134,14 @@ function parseArgs(argv: string[]): Args { } } + if (args.rulesFile) { + args.rules.push(...readRulesFile(args.rulesFile)) + } + if (args.rules.length === 0) { - console.error('Error: You must provide at least one --rule .') + console.error('Error: You must provide at least one --rule or a --rules-file.') console.error('Example: --rule exhaustive-deps --rule no-console') + console.error('Example: --rules-file scripts/ratchet-rules.json') process.exit(2) } diff --git a/apps/studio/scripts/ratchet-rules.json b/apps/studio/scripts/ratchet-rules.json new file mode 100644 index 00000000000..4f0135b422e --- /dev/null +++ b/apps/studio/scripts/ratchet-rules.json @@ -0,0 +1,22 @@ +[ + "react-hooks/exhaustive-deps", + "import/no-anonymous-default-export", + "@tanstack/query/exhaustive-deps", + "@typescript-eslint/no-explicit-any", + "no-restricted-imports", + "no-restricted-exports", + "react/no-unstable-nested-components", + "jsx-a11y/aria-props", + "jsx-a11y/aria-proptypes", + "jsx-a11y/role-supports-aria-props", + "jsx-a11y/anchor-has-content", + "jsx-a11y/control-has-associated-label", + "jsx-a11y/label-has-associated-control", + "jsx-a11y/aria-role", + "jsx-a11y/no-redundant-roles", + "jsx-a11y/no-aria-hidden-on-focusable", + "jsx-a11y/tabindex-no-positive", + "jsx-a11y/anchor-is-valid", + "jsx-a11y/heading-has-content", + "jsx-a11y/no-distracting-elements" +]