This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch chore/lint-cleanup-tech-debt in repository https://gitbox.apache.org/repos/asf/superset.git
commit e8ffc90c0b9889bb4a7d96ca86387a2a84c087ae Author: Evan Rusackas <[email protected]> AuthorDate: Wed Feb 11 00:03:02 2026 -0800 chore(lint): enforce stricter eslint/oxlint rules Upgrade several lint rules from warn to error: - constructor-super - no-alert - react/no-danger - react/forbid-foreign-prop-types - unicorn/no-invalid-remove-event-listener Add new rules: - react-you-might-not-need-an-effect/no-empty-effect - react-you-might-not-need-an-effect/no-pass-live-state-to-parent - react-you-might-not-need-an-effect/no-initialize-state Fix violations: - ExtensibleFunction.ts: Add eslint-disable for intentional super() skip - SqlEditorTabHeader: Add eslint-disable with TODO for native prompt - App/index.tsx: Fix removeEventListener by storing bound function ref - html.tsx, TextCellRenderer.tsx, TableChart.tsx: Add eslint-disable comments for safe dangerouslySetInnerHTML usages (sanitized HTML) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- superset-frontend/.eslintrc.js | 6 ++++++ superset-frontend/oxlint.json | 7 +++++-- .../packages/superset-ui-core/src/models/ExtensibleFunction.ts | 3 ++- .../packages/superset-ui-core/src/utils/html.test.tsx | 1 + superset-frontend/packages/superset-ui-core/src/utils/html.tsx | 2 ++ .../plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx | 2 ++ superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx | 4 +++- superset-frontend/src/SqlLab/components/App/index.tsx | 8 ++++++-- .../src/SqlLab/components/SqlEditorTabHeader/index.tsx | 2 ++ 9 files changed, 29 insertions(+), 6 deletions(-) diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index 5f4b50ff9d2..5cbc53a4403 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -135,6 +135,7 @@ module.exports = { 'icons', 'i18n-strings', 'react-prefer-function-component', + 'react-you-might-not-need-an-effect', 'prettier', ], rules: { @@ -235,6 +236,11 @@ module.exports = { 'jsx-a11y/mouse-events-have-key-events': 0, 'jsx-a11y/no-static-element-interactions': 0, + // React effect best practices + 'react-you-might-not-need-an-effect/no-empty-effect': 'error', + 'react-you-might-not-need-an-effect/no-pass-live-state-to-parent': 'error', + 'react-you-might-not-need-an-effect/no-initialize-state': 'error', + // Lodash 'lodash/import-scope': [2, 'member'], diff --git a/superset-frontend/oxlint.json b/superset-frontend/oxlint.json index 81b7523dce5..6e5076075d0 100644 --- a/superset-frontend/oxlint.json +++ b/superset-frontend/oxlint.json @@ -28,7 +28,8 @@ // === Core ESLint rules === // Error prevention "no-console": "warn", - "no-alert": "warn", + "no-alert": "error", + "constructor-super": "error", "no-debugger": "error", "no-unused-vars": "off", "no-undef": "error", @@ -148,7 +149,8 @@ ], "react/no-array-index-key": "off", "react/no-children-prop": "error", - "react/no-danger": "warn", + "react/no-danger": "error", + "react/forbid-foreign-prop-types": "error", "react/no-danger-with-children": "error", "react/no-deprecated": "error", "react/no-did-update-set-state": "error", @@ -250,6 +252,7 @@ ], // === Unicorn rules (bonus coverage) === + "unicorn/no-invalid-remove-event-listener": "error", "unicorn/filename-case": "off", "unicorn/prevent-abbreviations": "off", "unicorn/no-null": "off", diff --git a/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts b/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts index 66f82af0499..566b898091f 100644 --- a/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts +++ b/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts @@ -22,7 +22,8 @@ */ export default class ExtensibleFunction extends Function { - // @ts-expect-error + // @ts-expect-error - intentionally not calling super(), using setPrototypeOf pattern instead + // eslint-disable-next-line constructor-super constructor(fn: Function) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, no-constructor-return return Object.setPrototypeOf(fn, new.target.prototype); diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx index 648b2b68ce7..ea24e0c8c50 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx @@ -106,6 +106,7 @@ describe('safeHtmlSpan', () => { expect(safeSpan).toEqual( <span className="safe-html-wrapper" + // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: htmlString }} />, ); diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx index de7267c8231..940fc232b6a 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx +++ b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx @@ -167,6 +167,8 @@ export function safeHtmlSpan(possiblyHtmlString: string) { return ( <span className="safe-html-wrapper" + // Safe: HTML is sanitized before rendering + // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: sanitizeHtml(possiblyHtmlString) }} /> ); diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx index 88e7bc23693..eb52f206a88 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx @@ -64,6 +64,8 @@ export const TextCellRenderer = (params: CellRendererProps) => { ); } if (allowRenderHtml && isProbablyHTML(value)) { + // Safe: HTML is sanitized before rendering + // eslint-disable-next-line react/no-danger return <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(value) }} />; } } diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 08f808bcf23..9aae0231f51 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -1060,17 +1060,19 @@ export default function TableChart<D extends DataRecord = DataRecord>( }; if (html) { if (truncateLongCells) { - // eslint-disable-next-line react/no-danger return ( <StyledCell {...cellProps}> <div className="dt-truncate-cell" style={columnWidth ? { width: columnWidth } : undefined} + // Safe: HTML is sanitized via formatColumnValue + // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={html} /> </StyledCell> ); } + // Safe: HTML is sanitized via formatColumnValue // eslint-disable-next-line react/no-danger return <StyledCell {...cellProps} dangerouslySetInnerHTML={html} />; } diff --git a/superset-frontend/src/SqlLab/components/App/index.tsx b/superset-frontend/src/SqlLab/components/App/index.tsx index efe9597c9f7..83200616524 100644 --- a/superset-frontend/src/SqlLab/components/App/index.tsx +++ b/superset-frontend/src/SqlLab/components/App/index.tsx @@ -117,12 +117,16 @@ interface AppState { class App extends PureComponent<AppProps, AppState> { hasLoggedLocalStorageUsage: boolean; + private boundOnHashChanged: () => void; + constructor(props: AppProps) { super(props); this.state = { hash: window.location.hash, }; + this.boundOnHashChanged = this.onHashChanged.bind(this); + this.showLocalStorageUsageWarning = throttle( this.showLocalStorageUsageWarning, LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS, @@ -131,7 +135,7 @@ class App extends PureComponent<AppProps, AppState> { } componentDidMount() { - window.addEventListener('hashchange', this.onHashChanged.bind(this)); + window.addEventListener('hashchange', this.boundOnHashChanged); // Horrible hack to disable side swipe navigation when in SQL Lab. Even though the // docs say setting this style on any div will prevent it, turns out it only works @@ -165,7 +169,7 @@ class App extends PureComponent<AppProps, AppState> { } componentWillUnmount() { - window.removeEventListener('hashchange', this.onHashChanged.bind(this)); + window.removeEventListener('hashchange', this.boundOnHashChanged); // And now we need to reset the overscroll behavior back to the default. document.body.style.overscrollBehaviorX = 'auto'; diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx index e63da7ad287..40c11f8a7d7 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx @@ -102,6 +102,8 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => { ); function renameTab() { + // TODO: Replace native prompt with a proper modal dialog + // eslint-disable-next-line no-alert const newTitle = prompt(t('Enter a new title for the tab')); if (newTitle) { actions.queryEditorSetTitle(qe, newTitle, qe.id);
