// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {defineConfig, globalIgnores} from 'eslint/config'; import tseslint from "@typescript-eslint/eslint-plugin"; import * as tsparser from "@typescript-eslint/parser"; import importPlugin from "eslint-plugin-import"; import reactHooks from "eslint-plugin-react-hooks"; import stylistic from "@stylistic/eslint-plugin"; // Load custom rulesets import eslintMattermost, {jestConfig} from "./eslint/eslint-mattermost.mjs"; import eslintReact from "./eslint/eslint-react.mjs"; export default defineConfig([ globalIgnores([ "node_modules/", "dist/", "build/", "coverage/", "android/", "ios/", ".github/", "detox/artifacts/", ]), eslintMattermost, jestConfig, eslintReact, { files: ["**/*.{js,jsx,ts,tsx}"], languageOptions: { parser: tsparser, parserOptions: { ecmaVersion: "latest", ecmaFeatures: { jsx: true } }, globals: { __DEV__: true }, sourceType: "module" }, plugins: { "@stylistic": stylistic, "@typescript-eslint": tseslint, "import": importPlugin, "react-hooks": reactHooks }, settings: { react: { pragma: "React", version: "detect" } }, rules: { "eol-last": ["error", "always"], "no-restricted-syntax": [ "error", { "selector": "CallExpression[callee.name=/^(logError|logWarning|logInfo|logDebug)$/] MemberExpression[property.name=/^(email|username|nickname|displayName|display_name|firstName|first_name|lastName|last_name|fullName|full_name|phoneNumber|phone_number)$/]", "message": "Do not log PII (names/emails/usernames). Logs are user-exportable and sent to Sentry. Log stable identifiers (IDs, channel type) instead. See CLAUDE.md > Error Handling & Logging." }, { "selector": "CallExpression[callee.name=/^(logError|logWarning|logInfo|logDebug)$/] Identifier[name=/([Tt]oken|[Pp]assword|[Ss]ecret)$/]", "message": "Do not log credentials (tokens/passwords/secrets). Logs are user-exportable and sent to Sentry. See CLAUDE.md > Error Handling & Logging." } ], "global-require": "off", "no-undefined": "off", "no-shadow": "off", "new-cap": [ "error", { "capIsNewExceptions": [ "Gesture.Pan", "Gesture.Tap", "Gesture.LongPress", "Gesture.Pinch", "Gesture.Rotate", "Gesture.ForceTouch", "Gesture.Exclusive", "Gesture.Simultaneous", "Gesture.Race", "Gesture.Native", ] } ], "react/display-name": [2, { "ignoreTranspilerName": false }], "react/jsx-filename-extension": "off", "react-hooks/exhaustive-deps": "error", "camelcase": ["off", { "properties": "never" }], "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unused-vars": [ 2, { vars: "all", args: "after-used", caughtErrors: "none", ignoreRestSiblings: true, } ], "@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-explicit-any": "warn", "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "no-underscore-dangle": "off", "indent": [2, 4, { "SwitchCase": 1 }], "key-spacing": [ 2, { "singleLine": { "beforeColon": false, "afterColon": true } } ], "@stylistic/member-delimiter-style": 2, "@typescript-eslint/no-unsafe-declaration-merging": "off", "import/order": [ 2, { "groups": ["builtin", "external", "parent", "sibling", "index", "type"], "newlines-between": "always", "pathGroups": [ { "pattern": "{@(@actions|@agents|@app|@assets|@calls|@client|@components|@constants|@context|@database|@helpers|@hooks|@init|@keyboard|@managers|@playbooks|@queries|@screens|@selectors|@share|@store|@telemetry|@typings|@test|@utils)/**,@(@agents|@constants|@i18n|@keyboard|@store|@websocket)}", "group": "external", "position": "after" }, { "pattern": "app/**", "group": "parent", "position": "before" } ], "alphabetize": { "order": "asc", "caseInsensitive": true }, "pathGroupsExcludedImportTypes": ["type"] } ] } }, { files: ["detox/e2e/**"], languageOptions: { globals: { by: true, detox: true, device: true, element: true, waitFor: true } }, rules: { "func-names": "off", "import/no-unresolved": "off", "max-nested-callbacks": "off", "no-process-env": "off", "no-unused-expressions": "off", // SEC-10992: Detox's bare element.longPress() (no duration) resolves to a single // tap on Android (detoxsingletap), not a long press — it fires onPress instead of // onLongPress (e.g. opening a link bookmark's URL in the browser instead of the // options sheet, which hung MM-T5725_1). Always pass an explicit duration, e.g. // .longPress(timeouts.TWO_SEC). The selector matches only zero-arg calls, so // .longPress(timeouts.X) / .longPress(2000) are unaffected, and Gesture.LongPress // (react-native-gesture-handler) is excluded by the lower-case property name. "no-restricted-syntax": [ "error", { "selector": "CallExpression[callee.type=\"MemberExpression\"][callee.property.name=\"longPress\"][arguments.length=0]", "message": "Pass an explicit duration to longPress() (e.g. .longPress(timeouts.TWO_SEC)). Bare .longPress() with no args resolves to a single tap on Android, firing onPress instead of onLongPress. See SEC-10992." } ] } } ]);