diff --git a/.github/workflows/eslint-rules.yml b/.github/workflows/eslint-rules.yml new file mode 100644 index 000000000..75b183a46 --- /dev/null +++ b/.github/workflows/eslint-rules.yml @@ -0,0 +1,28 @@ +name: Internal_ESLint_Rules_Test + +on: + push: + paths: + - "scripts/tools/eslint-plugin-prettier-internal-rules" + - ".github/workflows/eslint-rules.yml" + pull_request: + paths: + - "scripts/tools/eslint-plugin-prettier-internal-rules" + - ".github/workflows/eslint-rules.yml" + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Setup Node.js + uses: actions/setup-node@v2.1.4 + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: Test + run: cd scripts/tools/eslint-plugin-prettier-internal-rules && yarn test-coverage diff --git a/.gitignore b/.gitignore index 1a0eb682a..cdee460be 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ package-lock.json !.yarn/sdks !.yarn/versions .pnp.* +.nyc_output +/scripts/tools/eslint-plugin-prettier-internal-rules/node_modules diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js b/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js index 44032e57f..0346e4212 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js @@ -53,7 +53,7 @@ module.exports = { }, messages: { [MESSAGE_ID_PREFER_NAME_CHECK]: - "Prefer `name {{operator}} {{property}}` over `parent.{{property}} {{operator}} node`", + "Prefer `name {{operator}} {{propertyText}}` over `parent.{{property}} {{operator}} node`.", [MESSAGE_ID_NAME_CHECK_FIRST]: "`name` comparison should be on left side.", }, @@ -79,7 +79,11 @@ module.exports = { context.report({ node, messageId: MESSAGE_ID_PREFER_NAME_CHECK, - data: { property: propertyText, operator }, + data: { + property: sourceCode.getText(property), + propertyText, + operator, + }, fix: (fixer) => fixer.replaceText(node, `name ${operator} ${propertyText}`), }); diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/no-node-comments.js b/scripts/tools/eslint-plugin-prettier-internal-rules/no-node-comments.js index 761911b0e..493549b52 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/no-node-comments.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/no-node-comments.js @@ -50,8 +50,14 @@ module.exports = { ]; }) ); + // avoid report on `const {comments} = node` twice + const reported = new Set(); return { [selector](node) { + if (reported.has(node)) { + return; + } + if (ignored.has(fileName)) { const functionNames = ignored.get(fileName); if (functionNames === true) { @@ -75,6 +81,7 @@ module.exports = { return; } } + reported.add(node); context.report({ node, messageId, diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/package.json b/scripts/tools/eslint-plugin-prettier-internal-rules/package.json index 6815d7966..aaee2c7dc 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/package.json +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/package.json @@ -5,5 +5,9 @@ "private": true, "author": "fisker", "main": "./index.js", - "license": "MIT" + "license": "MIT", + "scripts": { + "test": "node test.js", + "test-coverage": "npx nyc node test.js" + } } diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/test.js b/scripts/tools/eslint-plugin-prettier-internal-rules/test.js new file mode 100644 index 000000000..b0e6ddd04 --- /dev/null +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/test.js @@ -0,0 +1,190 @@ +/* eslint-disable import/no-extraneous-dependencies */ +"use strict"; + +const path = require("path"); +const { outdent } = require("outdent"); +const { RuleTester } = require("eslint"); +const { rules } = require("."); + +const test = (ruleId, tests) => { + new RuleTester({ parserOptions: { ecmaVersion: 2021 } }).run( + ruleId, + rules[ruleId], + tests + ); +}; + +test("better-parent-property-check-in-needs-parens", { + valid: ["function needsParens() {return parent.test === node;}"], + invalid: [ + { + code: 'return parent.type === "MemberExpression" && name === "object";', + errors: [{ message: "`name` comparison should be on left side." }], + }, + { + code: "return parent.test === node;", + output: 'return name === "test";', + errors: [ + { message: 'Prefer `name === "test"` over `parent.test === node`.' }, + ], + }, + { + code: "return parent.test !== node;", + output: 'return name !== "test";', + errors: [ + { message: 'Prefer `name !== "test"` over `parent.test !== node`.' }, + ], + }, + { + code: 'return parent["property"] === node;', + output: 'return name === "property";', + errors: [ + { + message: + 'Prefer `name === "property"` over `parent."property" === node`.', + }, + ], + }, + ].map((testCase) => ({ + ...testCase, + code: `function needsParens() {${testCase.code}}`, + output: `function needsParens() {${testCase.output || testCase.code}}`, + filename: "needs-parens.js", + })), +}); + +test("directly-loc-start-end", { + valid: [], + invalid: [ + { + code: "options.locStart(node)", + output: "locStart(node)", + errors: [ + { message: "Please import `locStart` function and use it directly." }, + ], + }, + { + code: "options.locEnd(node)", + output: "locEnd(node)", + errors: [ + { message: "Please import `locEnd` function and use it directly." }, + ], + }, + ], +}); + +test("jsx-identifier-case", { + valid: [ + { + code: "const isJSXNode = true", + options: ["isJSXNode"], + }, + ], + invalid: [ + { + code: "function isJSXNode(){}", + output: "function isJsxNode(){}", + errors: [{ message: "Please rename 'isJSXNode' to 'isJsxNode'." }], + }, + { + code: "const isJSXNode = true", + output: "const isJsxNode = true", + errors: [{ message: "Please rename 'isJSXNode' to 'isJsxNode'." }], + }, + ], +}); + +test("no-node-comments", { + valid: [ + "const comments = node.notComments", + { + code: "function functionName() {return node.comments;}", + filename: path.join(__dirname, "../../..", "a.js"), + options: ["a.js"], + }, + { + code: "function functionName() {return node.comments;}", + filename: path.join(__dirname, "../../..", "a.js"), + options: [{ file: "a.js", functions: ["functionName"] }], + }, + ], + invalid: [ + ...[ + "function functionName() {return node.comments;}", + "const {comments} = node", + "const {comments: nodeComments} = node", + ].map((code) => ({ + code, + output: code, + errors: [{ message: "Do not access node.comments." }], + })), + { + code: "function notFunctionName() {return node.comments;}", + output: "function notFunctionName() {return node.comments;}", + filename: path.join(__dirname, "../../..", "a.js"), + options: [{ file: "a.js", functions: ["functionName"] }], + errors: [{ message: "Do not access node.comments." }], + }, + ], +}); + +test("prefer-fast-path-each", { + valid: ["const foo = path.map()"], + invalid: [ + { + code: "path.map()", + output: "path.each()", + errors: 1, + }, + ], +}); + +test("prefer-is-non-empty-array", { + valid: [ + // `isNonEmptyArray` self is ignored + outdent` + function isNonEmptyArray(object){ + return Array.isArray(object) && object.length; + } + `, + "a.b && a.c.length", + "a.b || !a.b.length", + '!a["b"] || !a.b.length', + ], + invalid: [ + ...[ + "a && a.b && a.b.length", + "a && a.b && a.b.length !== 0", + "a && a.b && a.b.length > 0", + "a && Array.isArray(a.b) && a.b.length", + "a && Array.isArray(a.b) && a.b.length !== 0", + "a && Array.isArray(a.b) && a.b.length > 0", + ].map((code) => ({ + code, + output: "a && isNonEmptyArray(a.b)", + errors: 1, + })), + ...[ + "!a || !a.b || !a.b.length", + "!a || !a.b || a.b.length === 0", + "!a || !Array.isArray(a.b) || !a.b.length", + "!a || !Array.isArray(a.b) || a.b.length === 0", + ].map((code) => ({ + code, + output: "!a || !isNonEmptyArray(a.b)", + errors: 1, + })), + ], +}); + +test("require-json-extensions", { + valid: ['require("./not-exists")', 'require("./index")'], + invalid: [ + { + code: 'require("./package")', + filename: __filename, + output: 'require("./package.json")', + errors: [{ message: 'Missing file extension ".json" for "./package".' }], + }, + ], +});