From cd11afa297ad1fa227b75e311e5cb35050bc7837 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 6 Mar 2026 11:46:19 +0100 Subject: [PATCH 1/6] add eslint rule enforcing getReportName being read-only --- eslint.config.mjs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 361e2b63b90ae..ba2e0f0fbea33 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -592,6 +592,42 @@ const config = defineConfig([ }, }, + { + files: ['src/libs/ReportNameUtils.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + // Global selectors (must be repeated since file-scoped rules override the global ones) + { + selector: 'TSEnumDeclaration', + message: "Please don't declare enums, use union types instead.", + }, + { + selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', + message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', + }, + { + selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', + message: + 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', + }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', + }, + // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. + { + selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', + message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', + }, + ], + }, + }, + { files: ['src/**/*'], ignores: ['src/languages/**', 'src/CONST/index.ts', 'src/NAICS.ts'], From 86d90b79652838d8979521035a38223b5e7f8f68 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 6 Mar 2026 12:13:04 +0100 Subject: [PATCH 2/6] add eslint rule enforcing getReportName being read-only --- eslint.changed.config.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 79114b59a65eb..eedbdc9ab556a 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -106,6 +106,19 @@ const config = defineConfig([ ], }, }, + + { + files: ['src/libs/ReportNameUtils.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', + message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', + }, + ], + }, + }, ]); export default config; From ecbed280068bcfa182086fa02d33e9e85d3f7f8d Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 9 Mar 2026 11:14:00 +0100 Subject: [PATCH 3/6] remove part from lint-changed --- eslint.changed.config.mjs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index eedbdc9ab556a..79114b59a65eb 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -106,19 +106,6 @@ const config = defineConfig([ ], }, }, - - { - files: ['src/libs/ReportNameUtils.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - { - selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', - message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', - }, - ], - }, - }, ]); export default config; From 56840bf9203d7bdc64ada8847b430efbabc8b19a Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 9 Mar 2026 11:23:57 +0100 Subject: [PATCH 4/6] The repetition pattern for ReportNameUtils --- eslint.changed.config.mjs | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 79114b59a65eb..8ca8864f1e775 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -106,6 +106,66 @@ const config = defineConfig([ ], }, }, + + { + files: ['src/libs/ReportNameUtils.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + // All selectors from parent blocks must be repeated since file-scoped rules override them + // From mainConfig's global block + { + selector: 'TSEnumDeclaration', + message: "Please don't declare enums, use union types instead.", + }, + { + selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', + message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', + }, + { + selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', + message: + 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', + }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', + }, + // From the **/*.ts, **/*.tsx block above + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^@libs/]', + message: 'Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"', + }, + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^@userActions/]', + message: 'Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"', + }, + { + selector: + 'JSXElement[openingElement.name.name=/^Pressable(WithoutFeedback|WithFeedback|WithDelayToggle|WithoutFocus)$/]:not(:has(JSXAttribute[name.name="sentryLabel"]))', + message: 'All Pressable components must include sentryLabel prop for Sentry tracking. Example: ', + }, + // From the **/libs/**/*.{ts,tsx} block above + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\.\\./]', + message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "../libs/module"', + }, + { + selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\./]', + message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "./libs/module"', + }, + // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. + { + selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', + message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', + }, + ], + }, + }, ]); export default config; From cb6813b3ac36f1be54fb2489c110fe14300d40d3 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 11 Mar 2026 10:34:54 +0100 Subject: [PATCH 5/6] eslint custom rule --- eslint-plugin-report-name-utils/index.mjs | 30 ++++++++++++ eslint.changed.config.mjs | 59 ++--------------------- eslint.config.mjs | 35 ++------------ 3 files changed, 36 insertions(+), 88 deletions(-) create mode 100644 eslint-plugin-report-name-utils/index.mjs diff --git a/eslint-plugin-report-name-utils/index.mjs b/eslint-plugin-report-name-utils/index.mjs new file mode 100644 index 0000000000000..0c4d8cfc2860f --- /dev/null +++ b/eslint-plugin-report-name-utils/index.mjs @@ -0,0 +1,30 @@ +/** + * ESLint plugin that enforces architectural constraints on ReportNameUtils.ts. + * + * `getReportName` must remain a pure read-only function — it reads from + * pre-computed `reportAttributesDerivedValue` and must never call other + * functions. All computation belongs in `computeReportName`. + */ + +const noFunctionCallInGetReportName = { + meta: { + type: 'problem', + docs: {description: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.'}, + messages: {noFunctionCall: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.'}, + schema: [], + }, + create(context) { + return { + 'FunctionDeclaration[id.name="getReportName"] CallExpression'(node) { + context.report({node, messageId: 'noFunctionCall'}); + }, + }; + }, +}; + +export default { + meta: {name: 'eslint-plugin-report-name-utils'}, + rules: { + 'no-function-call-in-get-report-name': noFunctionCallInGetReportName, + }, +}; diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 8ca8864f1e775..87417ae72a2ab 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -1,4 +1,5 @@ import {defineConfig} from 'eslint/config'; +import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs'; import mainConfig from './eslint.config.mjs'; const restrictedIconImportPaths = [ @@ -109,62 +110,8 @@ const config = defineConfig([ { files: ['src/libs/ReportNameUtils.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - // All selectors from parent blocks must be repeated since file-scoped rules override them - // From mainConfig's global block - { - selector: 'TSEnumDeclaration', - message: "Please don't declare enums, use union types instead.", - }, - { - selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', - message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', - }, - { - selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', - message: - 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', - }, - { - selector: 'LabeledStatement', - message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', - }, - { - selector: 'WithStatement', - message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', - }, - // From the **/*.ts, **/*.tsx block above - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^@libs/]', - message: 'Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"', - }, - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^@userActions/]', - message: 'Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"', - }, - { - selector: - 'JSXElement[openingElement.name.name=/^Pressable(WithoutFeedback|WithFeedback|WithDelayToggle|WithoutFocus)$/]:not(:has(JSXAttribute[name.name="sentryLabel"]))', - message: 'All Pressable components must include sentryLabel prop for Sentry tracking. Example: ', - }, - // From the **/libs/**/*.{ts,tsx} block above - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\.\\./]', - message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "../libs/module"', - }, - { - selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\./]', - message: 'Namespace imports are not allowed. Use named imports instead. Example: import { method } from "./libs/module"', - }, - // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. - { - selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', - message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', - }, - ], - }, + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, }, ]); diff --git a/eslint.config.mjs b/eslint.config.mjs index ba2e0f0fbea33..6051af243bcde 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import typescriptEslint from 'typescript-eslint'; import reactCompilerCompat from './eslint-plugin-react-compiler-compat/index.mjs'; +import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs'; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -594,38 +595,8 @@ const config = defineConfig([ { files: ['src/libs/ReportNameUtils.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - // Global selectors (must be repeated since file-scoped rules override the global ones) - { - selector: 'TSEnumDeclaration', - message: "Please don't declare enums, use union types instead.", - }, - { - selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', - message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', - }, - { - selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', - message: - 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', - }, - { - selector: 'LabeledStatement', - message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', - }, - { - selector: 'WithStatement', - message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.', - }, - // File-specific: getReportName must only read from reportAttributesDerivedValue — no function calls allowed. - { - selector: 'FunctionDeclaration[id.name="getReportName"] CallExpression', - message: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.', - }, - ], - }, + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, }, { From 3322d39b908b7111e098fa1680848d403a8309dd Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 16 Mar 2026 11:38:08 +0100 Subject: [PATCH 6/6] fix eslint error --- eslint-plugin-report-name-utils/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-plugin-report-name-utils/index.mjs b/eslint-plugin-report-name-utils/index.mjs index 0c4d8cfc2860f..78bbc70504e0f 100644 --- a/eslint-plugin-report-name-utils/index.mjs +++ b/eslint-plugin-report-name-utils/index.mjs @@ -15,7 +15,7 @@ const noFunctionCallInGetReportName = { }, create(context) { return { - 'FunctionDeclaration[id.name="getReportName"] CallExpression'(node) { + 'FunctionDeclaration[id.name="getReportName"] CallExpression': function (node) { context.report({node, messageId: 'noFunctionCall'}); }, };