From 9b3c7f5732cdc99da9919876ecf66c9bf52f8a04 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 31 Mar 2026 09:23:30 -0600 Subject: [PATCH 1/6] Prevent calling messenger actions in controller/service constructors Some controllers or services call actions from other controllers or services in the constructor (e.g. to populate local or persisted state). But this practice is not recommended, as it forces clients to use a specific order when instantiating controllers and services. This commit updates the controller guidelines to advise against this practice and adds a lint rule to prevent it from appearing again. --- docs/code-guidelines/controller-guidelines.md | 45 +++ eslint-suppressions.json | 356 ++++++++++++++++-- eslint.config.mjs | 20 + 3 files changed, 395 insertions(+), 26 deletions(-) diff --git a/docs/code-guidelines/controller-guidelines.md b/docs/code-guidelines/controller-guidelines.md index 372fb67fe31..0fc1c13872d 100644 --- a/docs/code-guidelines/controller-guidelines.md +++ b/docs/code-guidelines/controller-guidelines.md @@ -954,6 +954,51 @@ A messenger that allows no actions or events (whether internal or external) look export type FooServiceMessenger = Messenger<'FooService', never, never>; ``` +## Do not call messenger actions in the constructor + +One of the responsibilities of the messenger is to act as a liaison between controllers and services. A controller should not require direct access to another controller or service in order to communicate with it. This decouples controllers and services from each other and allows clients to initialize controllers and services in any order. + +Calling `this.messenger.call(...)` inside a controller's constructor, however, prevents this goal from being achieved. The action's handler must already be registered by the time the constructor runs, which means the controller that owns that handler must have been instantiated first. + +Instead of accessing actions in controller constructors, move any calls to an `init()` method (which clients are free to call after instantiating all controllers and services). + +🚫 **A messenger action is called during construction** + +```typescript +class TokensController extends BaseController { + #selectedAccountId: string; + + constructor({ messenger }: { messenger: TokensControllerMessenger }) { + super({ messenger /* ... */ }); + + // This requires AccountsController to have already been initialized + const { id } = this.messenger.call('AccountsController:getSelectedAccount'); + this.#selectedAccountId = id; + + // ... + } +} +``` + +✅ **The call is deferred to an `init()` method** + +```typescript +class TokensController extends BaseController { + #selectedAccountId: string | null = null; + + constructor({ messenger }: { messenger: TokensControllerMessenger }) { + super({ messenger /* ... */ }); + + // ... + } + + init() { + const { id } = this.messenger.call('AccountsController:getSelectedAccount'); + this.#selectedAccountId = id; + } +} +``` + ## Define and export a type for the controller's state The name of this type should be `${ControllerName}State`. It should be exported. diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 3f38d05e9ff..ae08774891b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -95,11 +95,6 @@ "count": 1 } }, - "packages/accounts-controller/src/index.ts": { - "no-restricted-syntax": { - "count": 2 - } - }, "packages/address-book-controller/src/AddressBookController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -116,11 +111,6 @@ "count": 2 } }, - "packages/analytics-controller/src/index.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/approval-controller/src/ApprovalController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 22 @@ -137,14 +127,44 @@ "count": 6 } }, + "packages/assets-controller/src/AssetsController.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, "packages/assets-controller/src/__fixtures__/MockAssetControllerMessenger.ts": { "import-x/no-relative-packages": { "count": 1 } }, - "packages/assets-controller/src/index.ts": { + "packages/assets-controller/src/data-sources/AccountsApiDataSource.ts": { "no-restricted-syntax": { - "count": 9 + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/PriceDataSource.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/RpcDataSource.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controller/src/data-sources/SnapDataSource.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/selectors/balance.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/utils/formatStateForTransactionPay.ts": { + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/jest.environment.js": { @@ -163,6 +183,11 @@ "count": 3 } }, + "packages/assets-controllers/src/AccountTrackerController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/assets-controllers/src/AssetsContractController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 @@ -174,6 +199,9 @@ "packages/assets-controllers/src/AssetsContractController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/CurrencyRateController.test.ts": { @@ -226,6 +254,9 @@ }, "@typescript-eslint/naming-convention": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts": { @@ -242,6 +273,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 1 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/NftController.test.ts": { @@ -279,11 +313,17 @@ }, "no-param-reassign": { "count": 2 + }, + "no-restricted-syntax": { + "count": 6 } }, "packages/assets-controllers/src/NftDetectionController.ts": { "import-x/no-relative-packages": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/src/RatesController/RatesController.test.ts": { @@ -341,6 +381,16 @@ "count": 2 } }, + "packages/assets-controllers/src/TokenBalancesController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controllers/src/TokenDetectionController.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, "packages/assets-controllers/src/TokenRatesController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 4 @@ -449,6 +499,11 @@ "count": 4 } }, + "packages/assets-controllers/src/multi-chain-accounts-service/api-balance-fetcher.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 @@ -470,6 +525,9 @@ "packages/assets-controllers/src/multicall.ts": { "id-length": { "count": 2 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/rpc-service/rpc-balance-fetcher.test.ts": { @@ -498,6 +556,11 @@ "count": 1 } }, + "packages/assets-controllers/src/token-prices-service/codefi-v2.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/assets-controllers/src/utils/formatters.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 9 @@ -577,6 +640,11 @@ "count": 2 } }, + "packages/bridge-controller/src/utils/quote.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/bridge-controller/src/utils/slippage.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -595,6 +663,9 @@ "packages/bridge-controller/src/utils/trade-utils.ts": { "no-restricted-globals": { "count": 1 + }, + "no-restricted-syntax": { + "count": 5 } }, "packages/bridge-controller/src/utils/validators.ts": { @@ -610,11 +681,21 @@ "count": 2 } }, + "packages/bridge-status-controller/src/utils/snaps.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, "packages/bridge-status-controller/src/utils/swap-received-amount.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 } }, + "packages/bridge-status-controller/src/utils/transaction.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, "packages/chain-agnostic-permission/src/caip25Permission.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 11 @@ -679,14 +760,39 @@ "count": 2 } }, - "packages/config-registry-controller/src/index.ts": { + "packages/claims-controller/src/utils.ts": { "no-restricted-syntax": { - "count": 2 + "count": 1 } }, - "packages/core-backend/src/index.ts": { + "packages/composable-controller/src/ComposableController.ts": { "no-restricted-syntax": { - "count": 2 + "count": 3 + } + }, + "packages/controller-utils/src/create-service-policy.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/controller-utils/src/util.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/core-backend/src/AccountActivityService.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/core-backend/src/BackendWebSocketService.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/core-backend/src/api/shared-types.ts": { + "no-restricted-syntax": { + "count": 1 } }, "packages/delegation-controller/src/DelegationController.test.ts": { @@ -828,6 +934,21 @@ "count": 6 } }, + "packages/eth-json-rpc-middleware/src/fetch.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/eth-json-rpc-middleware/src/inflight-cache.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/eth-json-rpc-provider/src/internal-provider.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/foundryup/src/cli.ts": { "no-restricted-globals": { "count": 1 @@ -884,6 +1005,9 @@ }, "id-denylist": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/foundryup/types/unzipper.d.ts": { @@ -913,7 +1037,7 @@ "count": 1 }, "no-restricted-syntax": { - "count": 14 + "count": 16 } }, "packages/gas-fee-controller/src/gas-util.ts": { @@ -924,6 +1048,31 @@ "count": 1 } }, + "packages/json-rpc-engine/src/JsonRpcEngine.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/v2/MiddlewareContext.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/v2/utils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/keyring-controller/src/KeyringController.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, "packages/logging-controller/src/LoggingController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -935,6 +1084,9 @@ "packages/logging-controller/src/LoggingController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/message-manager/src/AbstractMessageManager.test.ts": { @@ -1099,6 +1251,9 @@ }, "no-negated-condition": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/multichain-api-middleware/src/middlewares/MultichainMiddlewareManager.ts": { @@ -1127,6 +1282,9 @@ }, "id-length": { "count": 4 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts": { @@ -1140,6 +1298,9 @@ }, "@typescript-eslint/no-misused-promises": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/name-controller/src/NameController.ts": { @@ -1221,9 +1382,87 @@ "count": 1 } }, + "packages/network-controller/src/NetworkController.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "packages/network-controller/src/create-auto-managed-network-client.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/network-controller/src/create-network-client.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/network-controller/src/rpc-service/rpc-service.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/network-enablement-controller/src/NetworkEnablementController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/network-enablement-controller/src/selectors.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 + }, + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/perps-controller/src/PerpsController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/providers/HyperLiquidProvider.ts": { + "no-restricted-syntax": { + "count": 12 + } + }, + "packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/perps-controller/src/services/TradingService.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/types/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/perps-controller/src/types/transactionTypes.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/perps-controller/src/utils/errorUtils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/utils/hyperLiquidAdapter.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/perps-controller/src/utils/hyperLiquidValidation.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/utils/marketDataTransform.ts": { + "no-restricted-syntax": { + "count": 1 } }, "packages/perps-controller/src/utils/myxAdapter.ts": { @@ -1268,6 +1507,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 6 + }, + "no-restricted-syntax": { + "count": 9 } }, "packages/phishing-controller/src/PhishingDetector.test.ts": { @@ -1299,6 +1541,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 1 + }, + "no-restricted-syntax": { + "count": 4 } }, "packages/polling-controller/src/AbstractPollingController.ts": { @@ -1317,11 +1562,6 @@ "count": 1 } }, - "packages/profile-metrics-controller/src/index.ts": { - "no-restricted-syntax": { - "count": 2 - } - }, "packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 @@ -1365,6 +1605,9 @@ }, "id-length": { "count": 1 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockMessenger.ts": { @@ -1498,6 +1741,11 @@ "count": 1 } }, + "packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, "packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/types.ts": { "@typescript-eslint/naming-convention": { "count": 5 @@ -1528,6 +1776,9 @@ "packages/profile-sync-controller/src/sdk/errors.ts": { "id-length": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/profile-sync-controller/src/sdk/mocks/userstorage.ts": { @@ -1626,19 +1877,42 @@ "count": 1 } }, - "packages/ramps-controller/src/index.ts": { + "packages/ramps-controller/src/RampsController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts": { "no-restricted-syntax": { "count": 1 } }, - "packages/remote-feature-flag-controller/src/index.ts": { + "packages/remote-feature-flag-controller/src/utils/version.ts": { "no-restricted-syntax": { "count": 1 } }, + "packages/seedless-onboarding-controller/src/SecretMetadata.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/seedless-onboarding-controller/src/assertions.ts": { + "no-restricted-syntax": { + "count": 10 + } + }, + "packages/seedless-onboarding-controller/src/errors.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, "packages/selected-network-controller/src/SelectedNetworkController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 4 + }, + "no-restricted-syntax": { + "count": 3 } }, "packages/selected-network-controller/src/SelectedNetworkMiddleware.ts": { @@ -1677,6 +1951,11 @@ "count": 1 } }, + "packages/signature-controller/src/utils/delegations.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/signature-controller/src/utils/normalize.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 @@ -1710,11 +1989,36 @@ "count": 2 } }, - "packages/subscription-controller/src/index.ts": { + "packages/subscription-controller/src/errors.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-controller/src/TransactionController.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/transaction-controller/src/utils/retry.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/transaction-controller/src/utils/utils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-pay-controller/src/strategy/across/across-actions.ts": { "no-restricted-syntax": { "count": 2 } }, + "packages/transaction-pay-controller/src/strategy/relay/hyperliquid-withdraw.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/transaction-pay-controller/src/utils/source-amounts.ts": { "import-x/no-relative-packages": { "count": 1 @@ -1824,4 +2128,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index def3343abc8..2f60ad1f3ca 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -211,6 +211,26 @@ const config = createConfig([ 'import-x/no-relative-packages': 'error', }, }, + // Prevent calling messenger actions in controller/service constructors + { + files: ['packages/*/src/**/*.ts'], + ignores: ['**/*.test.ts', '**/tests/**/*.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + ...collectExistingRuleOptions('no-restricted-syntax', [ + base, + typescript, + ]), + { + selector: + 'MethodDefinition[kind="constructor"] CallExpression[callee.type="MemberExpression"][callee.property.name="call"][callee.object.type="MemberExpression"][callee.object.object.type="ThisExpression"][callee.object.property.name="messenger"]', + message: + 'Do not call messenger actions in the constructor, as this forces clients to instantiate controllers or services in a specific order. Move this call to an init() method instead. Read the controller guidelines for more: ...', + }, + ], + }, + }, { files: ['packages/foundryup/**/*.{js,ts}'], rules: { From 0e265bf0a686e0d19ee5871c7501725a0db957d6 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 31 Mar 2026 10:38:07 -0600 Subject: [PATCH 2/6] Fix lint violation --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index ae08774891b..6ddb0a3b2b6 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2128,4 +2128,4 @@ "count": 1 } } -} \ No newline at end of file +} From a33bc311887a936263963b8d7b78fe3b40ca35b9 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 31 Mar 2026 10:54:34 -0600 Subject: [PATCH 3/6] Update lint rule to link to new section in guidelines --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 2f60ad1f3ca..f47a6d021db 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -226,7 +226,7 @@ const config = createConfig([ selector: 'MethodDefinition[kind="constructor"] CallExpression[callee.type="MemberExpression"][callee.property.name="call"][callee.object.type="MemberExpression"][callee.object.object.type="ThisExpression"][callee.object.property.name="messenger"]', message: - 'Do not call messenger actions in the constructor, as this forces clients to instantiate controllers or services in a specific order. Move this call to an init() method instead. Read the controller guidelines for more: ...', + 'Do not call messenger actions in the constructor, as this forces clients to instantiate controllers or services in a specific order. Move this call to an init() method instead. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#do-not-call-messenger-actions-in-the-constructor', }, ], }, From f0b3fb1409c8a0924ae18a372441c00483b4a413 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 31 Mar 2026 12:07:14 -0600 Subject: [PATCH 4/6] Don't overwrite previous no-restricted-syntax rule --- eslint-suppressions.json | 47 +++++++++++++++++++++++- eslint.config.mjs | 78 ++++++++++++++++++++++------------------ 2 files changed, 89 insertions(+), 36 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 6ddb0a3b2b6..a5bb3ef74e1 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -95,6 +95,11 @@ "count": 1 } }, + "packages/accounts-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/address-book-controller/src/AddressBookController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -111,6 +116,11 @@ "count": 2 } }, + "packages/analytics-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/approval-controller/src/ApprovalController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 22 @@ -157,6 +167,11 @@ "count": 1 } }, + "packages/assets-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 9 + } + }, "packages/assets-controller/src/selectors/balance.ts": { "no-restricted-syntax": { "count": 1 @@ -770,6 +785,11 @@ "count": 3 } }, + "packages/config-registry-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/controller-utils/src/create-service-policy.ts": { "no-restricted-syntax": { "count": 1 @@ -795,6 +815,11 @@ "count": 1 } }, + "packages/core-backend/src/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/delegation-controller/src/DelegationController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 @@ -1562,6 +1587,11 @@ "count": 1 } }, + "packages/profile-metrics-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 @@ -1882,6 +1912,16 @@ "count": 1 } }, + "packages/ramps-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/remote-feature-flag-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts": { "no-restricted-syntax": { "count": 1 @@ -1994,6 +2034,11 @@ "count": 1 } }, + "packages/subscription-controller/src/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/transaction-controller/src/TransactionController.ts": { "no-restricted-syntax": { "count": 3 @@ -2128,4 +2173,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index f47a6d021db..e4cbc478c0e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,6 +5,19 @@ import typescript from '@metamask/eslint-config-typescript'; const NODE_LTS_VERSION = 22; +/** + * Arguments to the `no-restricted-syntax` rule that prevents messsenger actions + * from being called in constructors. + */ +const NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_RULES = [ + { + selector: + 'MethodDefinition[kind="constructor"] CallExpression[callee.type="MemberExpression"][callee.property.name="call"][callee.object.type="MemberExpression"][callee.object.object.type="ThisExpression"][callee.object.property.name="messenger"]', + message: + 'Do not call messenger actions in the constructor, as this forces clients to instantiate controllers or services in a specific order. Move this call to an init() method instead. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#do-not-call-messenger-actions-in-the-constructor', + }, +]; + /** * Collects all options for a given array-valued rule across one or more flat * config arrays, excluding the leading severity element. @@ -109,39 +122,6 @@ const config = createConfig([ 'jsdoc/require-jsdoc': 'off', }, }, - { - // Prohibit exporting certain types from package index files. - // Extends the upstream no-restricted-syntax selectors from base and - // typescript configs with additional selectors specific to this monorepo. - files: ['packages/*/src/index.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - ...collectExistingRuleOptions('no-restricted-syntax', [ - base, - typescript, - ]), - { - selector: - 'ExportNamedDeclaration > ExportSpecifier[local.name=/AllowedActions$/]', - message: - 'Do not export AllowedActions types from package index files. These types describe external messenger dependencies and are obtainable from the packages that define them directly. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#define-but-do-not-export-a-type-union-for-external-action-types', - }, - { - selector: - 'ExportNamedDeclaration > ExportSpecifier[local.name=/AllowedEvents$/]', - message: - 'Do not export AllowedEvents types from package index files. These types describe external messenger dependencies and are obtainable from the packages that define them directly. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#define-but-do-not-export-a-type-union-for-external-event-types', - }, - { - selector: - 'ExportNamedDeclaration > ExportSpecifier[local.name=/MethodActions$/]', - message: - 'Do not export *MethodActions types from package index files. Internal messenger actions are already available via the *Actions type. Export the individual action types (along with *Actions) instead. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#expose-controller-methods-through-messenger-in-bulk', - }, - ], - }, - }, { files: ['**/*.test.{js,ts}', '**/tests/**/*.{js,ts}'], extends: [jest], @@ -222,11 +202,39 @@ const config = createConfig([ base, typescript, ]), + ...NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_RULES, + ], + }, + }, + { + // Prohibit exporting *AllowedActions, *AllowedEvents, and *MethodActions + // from package index files + files: ['packages/*/src/index.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + ...collectExistingRuleOptions('no-restricted-syntax', [ + base, + typescript, + ]), + ...NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_RULES, + { + selector: + 'ExportNamedDeclaration > ExportSpecifier[local.name=/AllowedActions$/]', + message: + 'Do not export AllowedActions types from package index files. These types describe external messenger dependencies and are obtainable from the packages that define them directly. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#define-but-do-not-export-a-type-union-for-external-action-types', + }, { selector: - 'MethodDefinition[kind="constructor"] CallExpression[callee.type="MemberExpression"][callee.property.name="call"][callee.object.type="MemberExpression"][callee.object.object.type="ThisExpression"][callee.object.property.name="messenger"]', + 'ExportNamedDeclaration > ExportSpecifier[local.name=/AllowedEvents$/]', message: - 'Do not call messenger actions in the constructor, as this forces clients to instantiate controllers or services in a specific order. Move this call to an init() method instead. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#do-not-call-messenger-actions-in-the-constructor', + 'Do not export AllowedEvents types from package index files. These types describe external messenger dependencies and are obtainable from the packages that define them directly. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#define-but-do-not-export-a-type-union-for-external-event-types', + }, + { + selector: + 'ExportNamedDeclaration > ExportSpecifier[local.name=/MethodActions$/]', + message: + 'Do not export *MethodActions types from package index files. Internal messenger actions are already available via the *Actions type. Export the individual action types (along with *Actions) instead. Read the controller guidelines for more: https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md#expose-controller-methods-through-messenger-in-bulk', }, ], }, From 11f7f2a820978bbb39cf0667d9e837a6e978a60e Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 31 Mar 2026 12:07:49 -0600 Subject: [PATCH 5/6] Fix lint violation --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index a5bb3ef74e1..8b565d2abf3 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2173,4 +2173,4 @@ "count": 1 } } -} \ No newline at end of file +} From 640126cb1db76e36b3a77d11aa6c510a9805e116 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 31 Mar 2026 13:46:40 -0600 Subject: [PATCH 6/6] Fix formatting again --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 75a67830626..dd1102c6f21 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2167,4 +2167,4 @@ "count": 1 } } -} \ No newline at end of file +}