diff --git a/.eslintrc.js b/.eslintrc.js index 9dbc3cf0342..d902880a45f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,6 @@ +const rulesDirPlugin = require('eslint-plugin-rulesdir'); +rulesDirPlugin.RULES_DIR = 'tools/eslint/lib'; + module.exports = { root: true, env: { @@ -136,10 +139,12 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/stylistic', ], - plugins: ['eslint-plugin-tsdoc', 'local'], + plugins: ['eslint-plugin-tsdoc', 'rulesdir'], rules: { // Keeps comments formatted. - 'local/prettier-comments': 'error', + 'rulesdir/prettier-comments': 'error', + // Enforces clean up of used resources. + 'rulesdir/use-using': 'off', // Brackets keep code readable. curly: ['error', 'all'], // Brackets keep code readable and `return` intentions clear. diff --git a/package-lock.json b/package-lock.json index c7e14865f12..0252735bbc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "workspaces": [ "packages/*", "test", - "test/installation" + "test/installation", + "tools/eslint" ], "devDependencies": { "@actions/core": "1.10.0", @@ -45,9 +46,9 @@ "eslint-config-prettier": "9.0.0", "eslint-formatter-codeframe": "7.32.1", "eslint-plugin-import": "2.28.1", - "eslint-plugin-local": "1.0.0", "eslint-plugin-mocha": "10.1.0", "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-rulesdir": "0.2.2", "eslint-plugin-tsdoc": "0.2.17", "eslint-plugin-unused-imports": "3.0.0", "esprima": "4.0.1", @@ -80,6 +81,12 @@ "zod": "3.22.2" } }, + "eslint": { + "name": "@puppeteer/eslint", + "version": "0.1.0", + "extraneous": true, + "license": "Apache-2.0" + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -1873,6 +1880,10 @@ "resolved": "packages/browsers", "link": true }, + "node_modules/@puppeteer/eslint": { + "resolved": "tools/eslint", + "link": true + }, "node_modules/@puppeteer/ng-schematics": { "resolved": "packages/ng-schematics", "link": true @@ -4523,11 +4534,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-local": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/eslint-plugin-mocha": { "version": "10.1.0", "dev": true, @@ -4622,6 +4628,15 @@ } } }, + "node_modules/eslint-plugin-rulesdir": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-plugin-tsdoc": { "version": "0.2.17", "dev": true, @@ -11191,6 +11206,12 @@ "ws": "8.13.0" } }, + "packages/ts-plugin": { + "name": "@puppeteer/ts-plugin", + "version": "0.1.0", + "extraneous": true, + "license": "Apache-2.0" + }, "test": { "name": "@puppeteer-test/test", "version": "latest", @@ -11205,6 +11226,11 @@ "glob": "10.3.3", "mocha": "10.2.0" } + }, + "tools/eslint": { + "name": "@puppeteer/eslint", + "version": "0.1.0", + "license": "Apache-2.0" } }, "dependencies": { @@ -12367,6 +12393,9 @@ } } }, + "@puppeteer/eslint": { + "version": "file:tools/eslint" + }, "@puppeteer/ng-schematics": { "version": "file:packages/ng-schematics", "requires": { @@ -14304,10 +14333,6 @@ } } }, - "eslint-plugin-local": { - "version": "1.0.0", - "dev": true - }, "eslint-plugin-mocha": { "version": "10.1.0", "dev": true, @@ -14357,6 +14382,12 @@ "synckit": "^0.8.5" } }, + "eslint-plugin-rulesdir": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", + "dev": true + }, "eslint-plugin-tsdoc": { "version": "0.2.17", "dev": true, diff --git a/package.json b/package.json index 3271c66ff53..ab60fd73949 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "lint:prettier": "prettier --check .", "lint": "run-s lint:prettier lint:eslint", "postinstall": "npm run postinstall --workspaces --if-present", + "prepare": "npm run prepare --workspaces --if-present", "test-install": "npm run test --workspace @puppeteer-test/installation", "test-types": "tsd -t packages/puppeteer", "test:chrome:headful": "wireit", @@ -137,7 +138,7 @@ "eslint-config-prettier": "9.0.0", "eslint-formatter-codeframe": "7.32.1", "eslint-plugin-import": "2.28.1", - "eslint-plugin-local": "1.0.0", + "eslint-plugin-rulesdir": "0.2.2", "eslint-plugin-mocha": "10.1.0", "eslint-plugin-prettier": "5.0.0", "eslint-plugin-tsdoc": "0.2.17", @@ -174,6 +175,7 @@ "workspaces": [ "packages/*", "test", - "test/installation" + "test/installation", + "tools/eslint" ] } diff --git a/tools/eslint/package.json b/tools/eslint/package.json new file mode 100644 index 00000000000..89b7bfae856 --- /dev/null +++ b/tools/eslint/package.json @@ -0,0 +1,34 @@ +{ + "name": "@puppeteer/eslint", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/tools/eslint" + }, + "scripts": { + "build": "wireit", + "prepare": "wireit" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "files": [ + "src/**" + ], + "output": [ + "lib/**", + "tsconfig.tsbuildinfo" + ] + }, + "prepare": { + "dependencies": [ + "build" + ] + } + }, + "author": "The Chromium Authors", + "license": "Apache-2.0" +} diff --git a/.eslintplugin.js b/tools/eslint/src/prettier-comments.js similarity index 70% rename from .eslintplugin.js rename to tools/eslint/src/prettier-comments.js index fccbf6b3524..b024d714118 100644 --- a/.eslintplugin.js +++ b/tools/eslint/src/prettier-comments.js @@ -1,5 +1,26 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed 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. + */ + +// @ts-nocheck +// TODO: We should convert this to types. + const prettier = require('@prettier/sync'); -const prettierConfig = require('./.prettierrc.cjs'); + +const prettierConfigPath = '../../../.prettierrc.cjs'; +const prettierConfig = require(prettierConfigPath); const cleanupBlockComment = value => { return value @@ -46,7 +67,7 @@ const buildBlockComment = (value, offset) => { /** * @type import("eslint").Rule.RuleModule */ -const rule = { +const prettierCommentsRule = { meta: { type: 'suggestion', docs: { @@ -85,8 +106,4 @@ const rule = { }, }; -module.exports = { - rules: { - 'prettier-comments': rule, - }, -}; +module.exports = prettierCommentsRule; diff --git a/tools/eslint/src/use-using.ts b/tools/eslint/src/use-using.ts new file mode 100644 index 00000000000..113b3296eb0 --- /dev/null +++ b/tools/eslint/src/use-using.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed 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. + */ + +import {ESLintUtils, TSESTree} from '@typescript-eslint/utils'; +import type { + RuleListener, + RuleModule, +} from '@typescript-eslint/utils/ts-eslint'; + +const usingSymbols = ['ElementHandle', 'JSHandle']; + +const createRule = ESLintUtils.RuleCreator(name => { + return `https://github.com/puppeteer/puppeteer/tree/main/tools/eslint/${name}.js`; +}); + +const useUsingRule: RuleModule<'useUsing', [], RuleListener> = createRule< + [], + 'useUsing' +>({ + name: 'use-using', + meta: { + docs: { + description: "Requires 'using' for element/JS handles.", + requiresTypeChecking: true, + }, + messages: { + useUsing: "Use 'using'.", + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create(context) { + const services = ESLintUtils.getParserServices(context); + const checker = services.program.getTypeChecker(); + + return { + VariableDeclaration(node): void { + if (['using', 'await using'].includes(node.kind)) { + return; + } + for (const declaration of node.declarations) { + if (declaration.id.type === TSESTree.AST_NODE_TYPES.Identifier) { + const tsNode = services.esTreeNodeToTSNodeMap.get(declaration.id); + const type = checker.getTypeAtLocation(tsNode); + let isElementHandleReference = false; + if (type.isUnionOrIntersection()) { + for (const member of type.types) { + if ( + member.symbol !== undefined && + usingSymbols.includes(member.symbol.escapedName as string) + ) { + isElementHandleReference = true; + break; + } + } + } else { + isElementHandleReference = + type.symbol !== undefined + ? usingSymbols.includes(type.symbol.escapedName as string) + : false; + } + if (isElementHandleReference) { + context.report({ + node: declaration.id, + messageId: 'useUsing', + }); + } + } + } + }, + }; + }, +}); + +export = useUsingRule; diff --git a/tools/eslint/tsconfig.json b/tools/eslint/tsconfig.json new file mode 100644 index 00000000000..f7525316e52 --- /dev/null +++ b/tools/eslint/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "./src", + "outDir": "./lib", + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "composite": false, + "removeComments": true + } +}