This is an automated email from the ASF dual-hosted git repository. 100pah pushed a commit to branch release-dev in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 3698a52bed5fd923ecc2a46db5425b22a667d48e Author: 100pah <[email protected]> AuthorDate: Fri May 8 23:54:02 2026 +0800 lint: Enhance eslint -- add rules. --- .eslintrc-common-production.yaml | 137 +++++++++++++++ .gitignore | 1 + build/eslint/eslint-plugin-ec/index.cjs | 263 ++++++++++++++++++++++++++++ build/eslint/eslint-plugin-ec/package.json | 17 ++ extension-src/{ => bmap}/.eslintrc.yaml | 14 +- extension-src/{ => dataTool}/.eslintrc.yaml | 18 +- package.json | 4 +- src/.eslintrc.yaml | 19 +- ssr/client/src/.eslintrc.yaml | 19 +- 9 files changed, 426 insertions(+), 66 deletions(-) diff --git a/.eslintrc-common-production.yaml b/.eslintrc-common-production.yaml new file mode 100644 index 000000000..5195e5803 --- /dev/null +++ b/.eslintrc-common-production.yaml @@ -0,0 +1,137 @@ + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +extends: './.eslintrc-common.yaml' +parser: "@typescript-eslint/parser" +parserOptions: + ecmaVersion: 6 + sourceType: module + ecmaFeatures: + modules: true + project: "tsconfig.json" +plugins: + - "@typescript-eslint" + - '@echarts-x/ec' +env: + # Do not set `browser: true, node: true`, which introduces some global variables + # (e.g., "require", "global"), which bypasses checks and may lead to errors. + es6: false +globals: + console: false + setTimeout: true + clearTimeout: true + navigator: false + __DEV__: true +rules: + "no-restricted-syntax": + - 2 + - + "selector": "SpreadElement" + "message": "Spread syntax is unnecessary; it introduces verbose code after compilation." + - + "selector": "AssignmentPattern" + "message": "Default parameters only apply to undefined, but echarts treats undefined and null the same." + - + "selector": "FunctionDeclaration[async=true]" + "message": "No need to use async/await yet; it introduces verbose code after compilation." + - + "selector": "ArrowFunctionExpression[async=true]" + "message": "No need to use async/await yet; it introduces verbose code after compilation." + - + "selector": "AwaitExpression" + "message": "No need to use async/await yet; it introduces verbose code after compilation." + "@echarts-x/ec/no-props-polyfill-uncertain": + # Currently, echarts does not officially discard legacy platforms. The following methods are + # not polyfilled, and alternatives are widely used in this codebase by convention, which also + # benefits compression. + - 2 + - {"receiver": "Array.prototype", "method": "map", "message": "Use `map` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "forEach", "message": "Use `each` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "reduce", "message": "Use `reduce` or `each` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "reduceRight", "message": "Use `reduce` or `each` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "filter", "message": "Use `filter` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "indexOf", "message": "Use `indexOf` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "find", "message": "Use `find` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "findIndex", "message": "Use `find` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "findLast", "message": "Use `find` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "findLastIndex", "message": "Use `find` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "includes", "message": "Use `indexOf` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "some", "message": "Use `find` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Array.prototype", "method": "flat"} + - {"receiver": "Array.prototype", "method": "flatMap"} + - {"receiver": "Array.prototype", "method": "at"} + - {"receiver": "Array.prototype", "method": "every"} + - {"receiver": "Array.prototype", "method": "fill"} + - {"receiver": "String.prototype", "method": "startsWith", "message": "Use `String.prototype.indexOf` instead."} + - {"receiver": "String.prototype", "method": "endsWith", "message": "Use `String.prototype.indexOf` instead."} + - {"receiver": "String.prototype", "method": "includes", "message": "Use `String.prototype.indexOf` instead."} + - {"receiver": "String.prototype", "method": "repeat"} + - {"receiver": "String.prototype", "method": "padStart"} + - {"receiver": "String.prototype", "method": "padEnd"} + - {"receiver": "String.prototype", "method": "matchAll"} + - {"receiver": "String.prototype", "method": "replaceAll"} + - {"receiver": "Array", "method": "isArray", "message": "Use `isArray` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Function", "method": "bind", "message": "Use `bind` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Function.prototype", "method": "bind", "message": "Use `bind` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Date", "method": "now", "message": "Use `+(new Date())` instead."} + - {"receiver": "Date.prototype", "method": "toJSON"} + - {"receiver": "Object", "method": "assign", "message": "Use `extend` or `defaults` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Object", "method": "keys", "message": "Use `keys` in `zrender/src/core/util.ts` instead."} + - {"receiver": "Object", "method": "seal", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "isSealed", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "freeze", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "isFrozen", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "isExtensible", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "preventExtensions", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "create", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "defineProperty", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "defineProperties", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "getOwnPropertyDescriptor", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "getOwnPropertyNames", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "is", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "entries", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "values", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "fromEntries", "message": "Typically not necessary in production code."} + - {"receiver": "Object", "method": "hasOwn", "message": "Typically not necessary in production code."} + "no-restricted-globals": + - 2 + # Avoid dangerous usage of globals for production code. + - "event" + - "name" + - "length" + - "orientation" + - "top" + - "parent" + - "location" + - "closed" + - "jQuery" + - "$" + # No need to use them yet. + - "Promise" + # Currently, echarts does not officially discard legacy platforms. The following methods are + # not polyfilled, and alternatives are widely used in this codebase by convention, which also + # benefits compression. + - {name: "Map", message: "Use `HashMap` in `zrender/src/core/util.ts` instead."} + - {name: "Set", message: "Use `HashMap` in `zrender/src/core/util.ts` instead."} + - {name: "WeakMap", message: "No polyfill for it. Typically it is not necessary. Use `makeInner` or `src/util/cycleCache` instead."} + - {name: "WeakSet", message: "No polyfill for it. Typically it is not necessary. Use `makeInner` or `src/util/cycleCache` instead."} + - {name: "Symbol", message: "No polyfill for it. Typically it is not necessary."} + - {name: "Proxy", message: "No polyfill for it. Typically it is not necessary."} + - {name: "Reflect", message: "No polyfill for it. Typically it is not necessary."} + - {name: "Intl", message: "No polyfill for it. Typically it is not necessary."} + - {name: "Atomics", message: "No polyfill for it. Typically it is not necessary."} diff --git a/.gitignore b/.gitignore index 575d1a17d..959f3d150 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,4 @@ todo /features.d.ts *.tgz /test/ZEXAMPLE_* +/build/eslint/eslint-plugin-ec/package-lock.json diff --git a/build/eslint/eslint-plugin-ec/index.cjs b/build/eslint/eslint-plugin-ec/index.cjs new file mode 100644 index 000000000..c594a92cd --- /dev/null +++ b/build/eslint/eslint-plugin-ec/index.cjs @@ -0,0 +1,263 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +const { ESLintUtils } = require('@typescript-eslint/utils'); +const ts = require('typescript'); + +/** + * [ECHARTS_SPECIFIC_ESLINT_RULES] + * + * [MEMO]: + * Currently, we still use `node_modules` to import local rules implementation (publish to NPM separately), + * due to following factors: + * - Importing local rules implementation without `node_modules` requires "flat config" (i.e., using `esling.config.js` rather than `.eslintrc`), which requires eslint `v8+`. + * - eslint `v9+` requires Node.js `v20+`. + * - Reading `esling.config.js` may not be a default behavior of VSCode ESLint Extension for eslint `v8`, but requires the explicit setting `"eslint.experimental.useFlatConfig": true`. + */ + +// console.log('___eslint_plugin_ec_debug_index_loaded'); + +const _rules = {}; + +function createRule(ruleName, ruleConfig) { + const ruleConfig1 = Object.assign({}, ruleConfig); + ruleConfig1.name = ruleName; + _rules[ruleName] = ESLintUtils.RuleCreator( + () => 'https://github.com/apache/echarts/tree/master/build/eslint/eslint-plugin-ec/index.cjs' + )(ruleConfig1); +} + +const _callExpressionReceiverDetectors = { + 'Array.prototype': function (checker, node, type) { + // Detect patterns like `[...].forEach(...)`. + return containsArrayType(checker, type); + }, + 'String.prototype': function (checker, node, type) { + return containsStringType(checker, type); + }, + 'Function.prototype': function (checker, node, type) { + return containsFunctionType(checker, type); + }, + 'Date.prototype': function (checker, node, type) { + return containsDateType(checker, type); + }, + 'Object': function (checker, node, type) { + return isBuiltInCtor(node, type, 'Object'); + }, + 'Function': function (checker, node, type) { + return isBuiltInCtor(node, type, 'Function'); + }, + 'Array': function (checker, node, type) { + return isBuiltInCtor(node, type, 'Array'); + }, + 'Date': function (type, node) { + return isBuiltInCtor(node, type, 'Date'); + }, +}; + +/** + * Configuartion is like: + * ```js + * rules: { + * "@echarts/ec/no-props-polyfill-uncertain": [ + * 2, + * { + * "receiver": "Array.prototype", + * "method": "map", + * "message": "xxx", + * }, + * { + * "receiver": "Object", + * "method": "assign", + * "message": "xxx" + * } + * ] + * } + * ``` + * It's like the pattern of rule "no-restricted-properties". + */ +createRule('no-props-polyfill-uncertain', { + meta: { + type: 'problem', + docs: { + description: 'Methods with uncertain polyfills is not allowed.', + }, + schema: { + type: 'array', + items: { + type: 'object', + properties: { + receiver: { + type: 'string', + enum: Object.keys(_callExpressionReceiverDetectors) + }, + method: {type: 'string'}, + message: {type: 'string'} + }, + required: ['receiver', 'method'], + additionalProperties: false + } + } + }, + + defaultOptions: [], + + create(context) { + + const restrictionList = context.options.slice(); + const services = ESLintUtils.getParserServices(context); + const typeChecker = services.program.getTypeChecker(); + + // console.log('___create', context); + + return { + CallExpression(node) { + + // console.log('___CallExpression'); + + restrictionList.forEach(function (restrictionItem) { + if ( + node.callee.type !== 'MemberExpression' + || node.callee.property.type !== 'Identifier' + || node.callee.property.name !== restrictionItem.method + ) { + return; + } + + const tsNode = services.esTreeNodeToTSNodeMap.get(node.callee.object); + const type = typeChecker.getTypeAtLocation(tsNode); + const isAny = (type.flags & ts.TypeFlags.Any) !== 0; // Considered union type. + const anyMsg = isAny ? ' (TS `any` detected. Use a concrete type to avoid this report.)' : ''; + + if (isAny || _callExpressionReceiverDetectors[restrictionItem.receiver](typeChecker, node, type)) { + const message = 'Direct use of `' + restrictionItem.receiver + '.' + restrictionItem.method + '`' + + ' is not allowed. ' + + (restrictionItem.message || 'No polyfill for it.') + anyMsg; + context.report({node, message}); + } + }); + + }, // End of `CallExpression` + }; + }, +}); // End of `createRule` + +/** + * Detect patterns like `Object.assign` + * @usage + * isBuiltInCtor(node, type, 'Object') + */ +function isBuiltInCtor( + node, + type, // {ts.Type} + name + // @return {ts.Type[]} +) { + return node.callee.object.type === 'Identifier' + && node.callee.object.name === name; +} + +/** + * NOTE: `(some as SomeType)` is not covered. + */ +function collectTypes( + checker, // {ts.TypeChecker} + type // {ts.Type} + // @return {ts.Type[]} +) { + // handle conditional types explicitly + if (type.flags & ts.TypeFlags.Conditional) { + const apparent = checker.getApparentType(type); + return collectTypes(checker, apparent); + } + + if (type.flags & ts.TypeFlags.Union) { // NOTE: `type.isUnion` does not always exist. + return type.types.flatMap(t => collectTypes(checker, t)); + } + if (type.flags & ts.TypeFlags.Intersection) { // NOTE: `type.isIntersection` does not always exist. + return type.types.flatMap(t => collectTypes(checker, t)); + } + if (checker.isArrayType(type) || checker.isTupleType(type)) { + return [type]; + } + return [type]; +} + +function containsArrayType( + checker, // {ts.TypeChecker} + type // {ts.Type} + // @return {ts.Type[]} +) { + return collectTypes(checker, type).some(function (t) { + return checker.isArrayType(t) || checker.isTupleType(t); + }); +} + +function containsStringType( + checker, // {ts.TypeChecker} + type // {ts.Type} + // @return {ts.Type[]} +) { + return collectTypes(checker, type).some(function (t) { + // primitive string + string literals + return (t.flags & ts.TypeFlags.StringLike) !== 0; + }); +} + +function containsFunctionType( + checker, // {ts.TypeChecker} + type // {ts.Type} + // @return {ts.Type[]} +) { + return collectTypes(checker, type).some(function (t) { + // unwrap apparent type (important for generics/conditional types) + const apparent = checker.getApparentType(t); + // function detection via call signatures + return checker.getSignaturesOfType( + apparent, + ts.SignatureKind.Call + ).length > 0; + }); +} + +function containsDateType( + checker, // {ts.TypeChecker} + type // {ts.Type} + // @return {ts.Type[]} +) { + return collectTypes(checker, type).some(function (t) { + const apparent = checker.getApparentType(t); + // Get symbol (the named declaration this type correspond to) + const symbol = apparent.getSymbol(); + if (!symbol) { + return false; + } + if (symbol.getName() === 'Date') { + return true; + } + // fallback: sometimes Date is a global constructor type + const typeStr = checker.typeToString(apparent); + // Cover patterns like `globalThis.Date`, `lib.es5.Date`, `SomeNamespace.Date` + return typeStr === 'Date' || typeStr.endsWith('.Date'); + }); +} + +module.exports = { + rules: _rules, +}; diff --git a/build/eslint/eslint-plugin-ec/package.json b/build/eslint/eslint-plugin-ec/package.json new file mode 100644 index 000000000..a6b554a56 --- /dev/null +++ b/build/eslint/eslint-plugin-ec/package.json @@ -0,0 +1,17 @@ +{ + "name": "@echarts-x/eslint-plugin-ec", + "version": "1.0.0", + "main": "index.cjs", + "homepage": "https://github.com/apache/echarts/blob/master/build/eslint/eslint-plugin-ec", + "repository": { + "type": "git", + "url": "https://github.com/apache/echarts.git" + }, + "dependencies": { + "typescript": "4.4.3", + "@typescript-eslint/utils": "^5.62.0" + }, + "peerDependencies": { + "eslint": "^7.15.0" + } +} diff --git a/extension-src/.eslintrc.yaml b/extension-src/bmap/.eslintrc.yaml similarity index 85% copy from extension-src/.eslintrc.yaml copy to extension-src/bmap/.eslintrc.yaml index 87f2639f5..79e4722d3 100644 --- a/extension-src/.eslintrc.yaml +++ b/extension-src/bmap/.eslintrc.yaml @@ -28,20 +28,10 @@ # ``` # Note that it should be "workingDirectories" rather than "WorkingDirectories". -parser: "@typescript-eslint/parser" +extends: '../../.eslintrc-common-production.yaml' parserOptions: ecmaVersion: 6 sourceType: module ecmaFeatures: modules: true - project: "tsconfig.json" -plugins: ["@typescript-eslint"] -env: - browser: true - node: true - es6: false -globals: - jQuery: false - Promise: false - __DEV__: true -extends: '../.eslintrc-common.yaml' + project: "extension-src/bmap/tsconfig.json" diff --git a/extension-src/.eslintrc.yaml b/extension-src/dataTool/.eslintrc.yaml similarity index 79% rename from extension-src/.eslintrc.yaml rename to extension-src/dataTool/.eslintrc.yaml index 87f2639f5..f3c856dbd 100644 --- a/extension-src/.eslintrc.yaml +++ b/extension-src/dataTool/.eslintrc.yaml @@ -28,20 +28,4 @@ # ``` # Note that it should be "workingDirectories" rather than "WorkingDirectories". -parser: "@typescript-eslint/parser" -parserOptions: - ecmaVersion: 6 - sourceType: module - ecmaFeatures: - modules: true - project: "tsconfig.json" -plugins: ["@typescript-eslint"] -env: - browser: true - node: true - es6: false -globals: - jQuery: false - Promise: false - __DEV__: true -extends: '../.eslintrc-common.yaml' +extends: '../../.eslintrc-common-production.yaml' diff --git a/package.json b/package.json index 5018d8f55..6bb064326 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,10 @@ "mktest3": "node test/build/mktest.js --with-inputs-all", "mktest:canvas:debug": "node test/build/mktest.js --with-canvas-debug", "mktest:help": "node test/build/mktest.js -h", - "checktype": "tsc --noEmit", + "checktype": "npx tsc --noEmit && npx tsc -p ./extension-src/bmap/tsconfig.json --noEmit", "checkheader": "node build/checkHeader.js", "lint": "npx eslint --cache --cache-location node_modules/.cache/eslint src/**/*.ts ssr/client/src/**/*.ts extension-src/**/*.ts", + "lint:nocache": "npx eslint src/**/*.ts ssr/client/src/**/*.ts extension-src/**/*.ts", "lint:fix": "npx eslint --fix src/**/*.ts extension-src/**/*.ts", "lint:dist": "echo 'It might take a while. Please wait ...' && npx jshint --config .jshintrc-dist dist/echarts.js" }, @@ -80,6 +81,7 @@ "@babel/types": "7.10.5", "@definitelytyped/typescript-versions": "^0.1.9", "@definitelytyped/utils": "0.0.188", + "@echarts-x/eslint-plugin-ec": "^1.0.0", "@lang/rollup-plugin-dts": "2.0.3", "@microsoft/api-extractor": "7.31.2", "@rollup/plugin-commonjs": "^17.0.0", diff --git a/src/.eslintrc.yaml b/src/.eslintrc.yaml index e36222640..2464bcf10 100644 --- a/src/.eslintrc.yaml +++ b/src/.eslintrc.yaml @@ -28,21 +28,4 @@ # ``` # Note that it should be "workingDirectories" rather than "WorkingDirectories". -parser: "@typescript-eslint/parser" -parserOptions: - ecmaVersion: 6 - sourceType: module - ecmaFeatures: - modules: true - project: "tsconfig.json" -plugins: ["@typescript-eslint"] -env: - es6: false -globals: - jQuery: false - Promise: false - console: true - setTimeout: true - clearTimeout: true - __DEV__: true -extends: '../.eslintrc-common.yaml' +extends: '../.eslintrc-common-production.yaml' diff --git a/ssr/client/src/.eslintrc.yaml b/ssr/client/src/.eslintrc.yaml index 4f97f861f..bb5821d48 100644 --- a/ssr/client/src/.eslintrc.yaml +++ b/ssr/client/src/.eslintrc.yaml @@ -28,21 +28,4 @@ # ``` # Note that it should be "workingDirectories" rather than "WorkingDirectories". -parser: "@typescript-eslint/parser" -parserOptions: - ecmaVersion: 6 - sourceType: module - ecmaFeatures: - modules: true - project: "tsconfig.json" -plugins: ["@typescript-eslint"] -env: - es6: false -globals: - jQuery: false - Promise: false - console: true - setTimeout: true - clearTimeout: true - __DEV__: true -extends: '../../../.eslintrc-common.yaml' +extends: '../../../.eslintrc-common-production.yaml' --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
