Ctrl K
ComunidadGuías…

eslint.config.mjs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { defineConfig, globalIgnores } from "eslint/config";
import { createRequire } from "node:module";
import js from "@eslint/js";
import globals from "globals";
import stylistic from "@stylistic/eslint-plugin";
import jsdoc from "eslint-plugin-jsdoc";
import security from "eslint-plugin-security";
import noUnsanitized from "eslint-plugin-no-unsanitized";

const require = createRequire(import.meta.url);

// In-repo rules from bin/eslint-rules, loaded as a local plugin (flat config has no --rulesdir).
const countly = {
    meta: { name: "countly-local-rules" },
    rules: {
        "no-prototype-pollution-sink": require("./bin/eslint-rules/no-prototype-pollution-sink.js")
    }
};

// ESLint 10 no longer ships require-jsdoc and valid-jsdoc. These are their
// eslint-plugin-jsdoc counterparts, configured to the options the old rules ran with:
// require-jsdoc's `require` map, and valid-jsdoc with requireReturn: false (a function
// documents @returns only when it returns a value).
const jsdocRules = {
    "jsdoc/require-jsdoc": ["error", {
        require: {
            FunctionDeclaration: true,
            MethodDefinition: true,
            ClassDeclaration: true,
            // The core rule only required JSDoc on an arrow function bound to a
            // variable (`const f = () => ...`), never on a callback or property value.
            ArrowFunctionExpression: false,
            FunctionExpression: false
        },
        contexts: ["VariableDeclarator > ArrowFunctionExpression"]
    }],
    "jsdoc/require-param": ["error", { checkDestructured: false }],
    "jsdoc/check-param-names": ["error", { checkDestructured: false }],
    "jsdoc/require-param-type": "error",
    "jsdoc/require-param-description": "error",
    "jsdoc/require-returns": ["error", { forceReturnsWithAsync: false }],
    "jsdoc/require-returns-check": "error",
    "jsdoc/require-returns-type": "error",
    "jsdoc/require-returns-description": "error",
    "jsdoc/valid-types": "error"
};

// Rules shared by the server-side and browser-side application code blocks.
const appCodeRules = {
    ...jsdocRules,
    "for-direction": "off",
    "no-control-regex": "off",
    "dot-notation": "error",
    "eqeqeq": ["error", "always"],
    "no-alert": "error",
    "no-caller": "error",
    "no-eval": "error",
    "no-extend-native": "error",
    "no-iterator": "error",
    "no-shadow": "error"
};

const nodeGlobals = { ...globals.node, ...globals.es2021 };

// Bluebird-only APIs, flagged as warnings until bluebird is gone.
const bluebirdStatics = [
    ["map", "use native arrays with Promise.all or a library like 'p-map'."],
    ["reduce", "use Array.reduce plus async/await or a concurrency library."],
    ["filter", "use Array.filter plus async/await or a concurrency library."],
    ["each", "use a for-loop/forEach with async/await."],
    ["props", "use Promise.all with Object.entries or a custom approach."],
    ["join", "use Promise.all([...]) and destructuring in .then."],
    ["try", "use a try/catch block or an async function."],
    ["attempt", "use a try/catch block or an async function."],
    ["method", "define an async function or return a native Promise."],
    ["promisify", "use native 'util.promisify' or wrap in a new Promise."],
    ["promisifyAll", "consider 'util.promisify' for each function or a similar library."],
    ["fromCallback", "use new Promise(...) or 'util.promisify'."],
    ["coroutine", "use native async/await."],
    ["spawn", "use native async/await."],
    ["using", "use try/finally or a resource-management library."],
    ["disposer", "use try/finally or a resource-management library."],
    ["settle", "use native 'Promise.allSettled'."]
];
const bluebirdInstanceMethods = [
    ["tap", "use '.then(value => { ...; return value; })'."],
    ["tapCatch", "use '.catch(error => { ...; throw error; })'."],
    ["spread", "use '.then(([a, b]) => ... )' with array destructuring."],
    ["delay", "use setTimeout() or a library (e.g., p-delay)."],
    ["timeout", "use p-timeout or similar library."],
    ["return", "use '.then(() => someValue)' or rewrite chain."],
    ["throw", "use '.then(() => { throw error; })'."],
    ["asCallback", "use 'util.callbackify' or rewrite manually."],
    ["nodeify", "use 'util.callbackify' or rewrite manually."],
    ["reflect", "use 'Promise.allSettled' or custom handling."],
    ["caught", "use '.catch()' with condition or separate logic."],
    ["catchReturn", "use '.catch(err => fallbackValue)' or similar."],
    ["catchThrow", "use '.catch(err => { throw newError; })'."]
];

export default defineConfig([
    globalIgnores([
        // ESLint 8 skipped dot-directories by default; flat config does not.
        ".*/**",
        "**/node_modules/**",
        "bin/upgrade/**",
        "frontend/express/public/javascripts/dom/**",
        "frontend/express/public/javascripts/min/**",
        "frontend/express/public/javascripts/utils/**",
        "frontend/express/public/javascripts/visualization/**",
        "frontend/express/public/themes/**",
        "frontend/express/public/sdk/**",
        "plugins/*/frontend/public/*",
        "plugins/*/frontend/public/javascripts/*",
        "bin/scripts/nghttp2/*",
        "plugins/push/api/parts/apn/*",
        "!plugins/*/frontend/public/javascripts",
        "!plugins/*/frontend/public/javascripts/countly.*.js"
    ]),
    {
        // .eslintrc.json had no top-level parser options, so files outside every
        // block below parse as ES5 scripts with no environment globals, as before.
        languageOptions: {
            ecmaVersion: 5,
            sourceType: "script"
        },
        plugins: {
            "@stylistic": stylistic,
            jsdoc,
            security,
            "no-unsanitized": noUnsanitized,
            countly
        },
        settings: {
            jsdoc: { mode: "typescript" }
        }
    },
    js.configs.recommended,
    {
        rules: {
            "require-atomic-updates": "off",

            /* ---------- Security ----------
               Counts below were measured against this tree before each rule was turned on.
               A rule that costs nothing today is worth having as a forward gate; a rule
               whose cost is a long cleanup is carried at "warn" with that cleanup named.

               Deliberately NOT enabled:

               guard-for-in — 454 reports here, and 6.2% of them (28) sit on a loop that
                 actually contains a prototype-reaching write. Those 28 are found by the same
                 two predicates no-prototype-pollution-sink already uses, so the rule adds no
                 coverage over it while adding 454 reports. It also misses the sinks that
                 motivated that rule, because a lone if/else loop body satisfies it.

               security/detect-object-injection — fires on nearly every obj[key] read.

               security/detect-non-literal-fs-filename — 145 reports, dominated by legitimate
                 path building in Gruntfile, countlyFs and the upload handlers.

               security/detect-non-literal-require — 19 reports, all of them the plugin
                 loader doing what a plugin loader does. */

            // 0 reports: free forward gates.
            "no-proto": "error",
            "no-implied-eval": "error",
            "no-new-func": "error",
            "no-script-url": "error",
            // 0 reports. Exempts the destructured `const {spawn} = require('child_process')`
            // that 24 files use, and flags a non-literal first argument to exec() — which is
            // exactly the anti-pattern CLAUDE.md calls out.
            "security/detect-child-process": "error",
            "security/detect-eval-with-expression": "error",
            "security/detect-pseudoRandomBytes": "error",

            // 26 reports across 16 files, mostly in api/utils/common.js. Each needs a
            // read to tell a real ReDoS from a bounded pattern, so it stays at "warn"
            // until those are triaged rather than being disabled at 26 sites.
            "security/detect-unsafe-regex": "warn",
            // 14 innerHTML assignments across 6 files, all in frontend code, none of them
            // literal. Same reason: real findings, but fixing them is a code change, so
            // they are surfaced rather than either blocking the build or being silenced.
            "no-unsanitized/method": "warn",
            "no-unsanitized/property": "warn",

            "@stylistic/block-spacing": ["error", "always"],
            "@stylistic/brace-style": ["error", "stroustrup"],
            "@stylistic/comma-spacing": ["error", { before: false, after: true }],
            "@stylistic/comma-style": ["error", "last"],
            "@stylistic/computed-property-spacing": ["error", "never"],
            "curly": ["error", "all"],
            "@stylistic/function-call-spacing": ["error", "never"],
            "@stylistic/indent": ["error", 4, { SwitchCase: 0 }],
            "@stylistic/key-spacing": ["error", { beforeColon: false, afterColon: true }],
            "@stylistic/keyword-spacing": ["error", { before: true, after: true }],
            "@stylistic/lines-between-class-members": ["error", "always"],
            "@stylistic/no-multi-spaces": "error",
            "@stylistic/no-trailing-spaces": ["error", { ignoreComments: true }],
            "no-useless-escape": "off",
            "@stylistic/no-whitespace-before-property": "error",
            "@stylistic/object-curly-newline": ["error", { multiline: true, consistent: true }],
            "@stylistic/object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
            "@stylistic/semi": ["error", "always"],
            "@stylistic/semi-style": ["error", "last"],
            "@stylistic/space-before-blocks": ["error", "always"],
            "@stylistic/space-before-function-paren": ["error", { anonymous: "never", named: "never", asyncArrow: "never", catch: "ignore" }],
            "@stylistic/space-in-parens": ["error", "never"],
            "@stylistic/space-infix-ops": "error",
            "@stylistic/space-unary-ops": ["error", { words: true, nonwords: false }],
            "@stylistic/switch-colon-spacing": "error",
            "unicode-bom": ["error", "never"],

            "no-restricted-properties": ["warn",
                ...bluebirdStatics.map(([property, suggestion]) => ({
                    object: "Promise",
                    property,
                    message: "Bluebird-specific method 'Promise." + property + "' detected. Suggestion: " + suggestion
                })),
                ...bluebirdStatics.map(([property, suggestion]) => ({
                    object: "Bluebird",
                    property,
                    message: "Bluebird-specific method 'Bluebird." + property + "' detected. Suggestion: " + suggestion
                }))
            ],
            "no-restricted-syntax": ["warn",
                ...bluebirdInstanceMethods.map(([name, suggestion]) => ({
                    selector: "CallExpression[callee.property.name='" + name + "']",
                    message: "Bluebird-specific instance method '." + name + "()' detected. Suggestion: " + suggestion
                })),
                {
                    selector: "CallExpression[callee.type='MemberExpression'][callee.property.name='bind'][callee.object.name=/^(Promise|Bluebird|BPromise)$/]",
                    message: "Bluebird-specific '.bind()' detected on a Bluebird promise. Suggestion: manually bind 'this' or use arrow functions."
                }
            ]
        }
    },
    {
        files: ["api/**/*.js", "plugins/*/api/**/*.js", "frontend/express/public/javascripts/countly/**/*.js", "plugins/*/frontend/**/*.js"],
        ignores: ["**/tests.js", "**/tests/**"],
        rules: {
            "countly/no-prototype-pollution-sink": "error"
        }
    },
    {
        files: [
            "plugins/content/frontend/vite.config.js",
            "plugins/journey_engine/frontend/vite.config.js",
            "plugins/journey_engine/frontend/vite-manifest.js"
        ],
        languageOptions: {
            ecmaVersion: 2023,
            sourceType: "module",
            globals: nodeGlobals
        },
        rules: {
            "no-console": "off"
        }
    },
    {
        files: [
            "plugins/content/frontend/content-blocks/**/*.js",
            "plugins/journey_engine/frontend/builder/**/*.js"
        ],
        languageOptions: {
            ecmaVersion: 2023,
            sourceType: "module",
            globals: nodeGlobals
        },
        rules: {
            "no-console": "error",
            "no-unused-vars": ["error", {
                argsIgnorePattern: "^_", // unused function args
                varsIgnorePattern: "^_" // unused variables
            }]
        }
    },
    {
        files: [
            "frontend/express/public/javascripts/countly/*.js",
            "frontend/express/public/javascripts/countly/vue/*.js",
            "frontend/express/public/javascripts/countly/vue/components/*.js",
            "frontend/express/public/javascripts/countly/vue/data/*.js",
            "frontend/express/public/core/*/javascripts/countly.*.js",
            "plugins/*/frontend/public/javascripts/countly.*.js"
        ],
        languageOptions: {
            ecmaVersion: 2020,
            sourceType: "script",
            globals: { ...globals.browser, ...globals.es2020 }
        },
        rules: {
            ...appCodeRules,
            "no-console": "error",
            "no-loop-func": "error"
        }
    },
    {
        files: ["plugins/content/api/positioning/**/*.js"],
        languageOptions: {
            ecmaVersion: 2023,
            sourceType: "module",
            globals: nodeGlobals
        }
    },
    {
        files: ["plugins/content/api/positioning/**/*.cjs"],
        languageOptions: {
            ecmaVersion: 2023,
            sourceType: "commonjs",
            globals: nodeGlobals
        }
    },
    {
        files: [
            "api/**/*.js",
            "frontend/express/*.js",
            "frontend/express/libs/*.js",
            "plugins/pluginManager.js",
            "plugins/pluginDependencies.js",
            "plugins/*/api/**/*.js",
            "plugins/*/frontend/**/*.js",
            "plugins/*/extend/**/*.js"
        ],
        ignores: [
            "plugins/*/frontend/public/**/*.js",
            "plugins/content/frontend/content-blocks/**/*.js",
            "plugins/journey_engine/frontend/builder/**/*.js"
        ],
        languageOptions: {
            ecmaVersion: 2023,
            // env: node in .eslintrc.json; it never changed sourceType
            parserOptions: { ecmaFeatures: { globalReturn: true } },
            globals: nodeGlobals
        },
        rules: {
            ...appCodeRules,
            "no-console": "off",
            "no-await-in-loop": "off",
            "no-loop-func": "off"
        }
    },
    {
        files: [
            "Gruntfile.js",
            "bin/**/*.js",
            "scripts/**/*.js",
            "extend/**/*.js",
            "plugins/*/*.js",
            "plugins/*/scripts/*.js",
            "*.js"
        ],
        ignores: ["plugins/*/tests.js"],
        languageOptions: {
            ecmaVersion: 2023,
            // env: node in .eslintrc.json; it never changed sourceType
            parserOptions: { ecmaFeatures: { globalReturn: true } },
            globals: nodeGlobals
        },
        rules: {
            "no-console": "off"
        }
    },
    {
        files: [
            "test/**/*.js",
            "plugins/*/tests/**/*.js",
            "plugins/*/tests.js"
        ],
        languageOptions: {
            ecmaVersion: 2023,
            // env: node in .eslintrc.json; it never changed sourceType
            parserOptions: { ecmaFeatures: { globalReturn: true } },
            globals: { ...nodeGlobals, ...globals.mocha }
        },
        rules: {
            "no-console": "off",
            "no-unused-vars": "off",
            "no-redeclare": "off"
        }
    },
    {
        files: ["ui-tests/**/*.js"],
        languageOptions: {
            ecmaVersion: 2023,
            sourceType: "module",
            globals: {
                ...nodeGlobals,
                ...globals.mocha,
                Cypress: "readonly",
                cy: "readonly"
            }
        },
        rules: {
            "no-console": "off",
            "no-unused-vars": "off",
            "no-redeclare": "off"
        }
    },
    {
        // This file itself is an ES module run by Node.
        files: ["eslint.config.mjs"],
        languageOptions: {
            ecmaVersion: 2023,
            sourceType: "module",
            globals: nodeGlobals
        }
    },
    {
        // ES module files that a Node block above also matches. A module has no global
        // return, and handing espree both makes it clear globalReturn on the shared
        // options object, which then turns off for every other file in that block.
        files: [
            "plugins/content/frontend/vite.config.js",
            "plugins/journey_engine/frontend/vite.config.js",
            "plugins/journey_engine/frontend/vite-manifest.js",
            "plugins/content/frontend/content-blocks/**/*.js",
            "plugins/journey_engine/frontend/builder/**/*.js",
            "plugins/content/api/positioning/**/*.js",
            "ui-tests/**/*.js"
        ],
        languageOptions: {
            sourceType: "module",
            parserOptions: { ecmaFeatures: { globalReturn: false } }
        }
    }
]);