diff --git a/.changeset/calm-islands-fetch.md b/.changeset/calm-islands-fetch.md new file mode 100644 index 00000000..f8c9ba35 --- /dev/null +++ b/.changeset/calm-islands-fetch.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-es-roikoren": patch +--- + +ci: update diff --git a/.eslintrc.js b/.eslintrc.js index db1a23fb..150a4be8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,7 +39,6 @@ module.exports = { 'no-case-declarations': OFF, 'no-console': WARN, 'no-constructor-return': ERROR, - 'no-continue': WARN, 'no-else-return': ERROR, 'no-eval': ERROR, 'no-extend-native': ERROR, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f5809768 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: 0 0 * * 0 + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 16 + - name: Install Packages + run: yarn install --frozen-lockfile + - name: Deduplicate yarn.lock + run: yarn deduplicate --list --fail + - name: Lint + run: yarn lint + - name: Type + run: yarn type + + test: + name: Test + + strategy: + matrix: + eslint: [5, 6, 7, 8] + node: [12, 14, 16] + os: [ubuntu-latest] + include: + # On other platforms + - eslint: 8 + node: 16 + os: windows-latest + - eslint: 8 + node: 16 + os: macos-latest + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Node.js ${{ matrix.node }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - name: Install Packages + run: yarn install --frozen-lockfile + - name: Install ESLint ${{ matrix.eslint }} + run: yarn add eslint@${{ matrix.eslint }} -D + - name: Test + run: yarn test + - name: Coverage + run: yarn coverage + - name: Codecov + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index f82368b5..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Test - -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [12, 14, 16] - - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: yarn cache directory - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: Setup node_modules cache - uses: actions/cache@v2 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: yarn install --frozen-lockfile - - name: Deduplicate yarn.lock - run: yarn deduplicate --list --fail - - name: Lint - run: yarn lint - - name: Type - run: yarn type - - name: Test - run: yarn test - - name: Build - run: yarn build diff --git a/.husky/pre-commit b/.husky/pre-commit index e6176d25..155ccb70 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,3 +3,6 @@ yarn lint-staged yarn deduplicate --list --fail + +yarn update +git add docs/ src/configs/ src/index.ts diff --git a/README.md b/README.md index bdd812be..9c545bc4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # eslint-plugin-es-roikoren [![Test Status](https://github.com/roikoren755/eslint-plugin-es/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/roikoren755/eslint-plugin-es/actions/workflows/test.yml?query=branch%3Amain) +[![codecov](https://codecov.io/gh/roikoren755/eslint-plugin-es/branch/main/graph/badge.svg?token=RF5L5KQQN6)](https://codecov.io/gh/roikoren755/eslint-plugin-es) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=bugs)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=code_smells)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) @@ -23,15 +24,142 @@ [![Lines of code](https://img.shields.io/tokei/lines/github/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) [![GitHub top language](https://img.shields.io/github/languages/top/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) -A re-implementation of `eslint-plugin-es`, compatible with `eslint@8`. +A re-implementation of `eslint-plugin-es` in TypeScript. -### Installation +## Disclaimer +First off, I would like to deeply thank [@mistycatea](https://github.com/mysticatea) and everyone else involved in the original `eslin-plugin-es`. None of this would have been possible without them, and all credit should go to them. -Run `npm i -D eslint-plugin-es-roikoren` (or `yarn add -D eslint-plugin-es-roikoren`) to add this package to your project's `devDependencies`. +This package is an attempt to migrate `eslint-config-es` to be written in TypeScript, and to use the great [`@typescript-eslint`](https://github.com/typescript-eslint) repository for plugin development. -### Usage +Below is taken verbatim from [`eslint-plugin-es`](https://github.com/mysticatea/eslint-plugin-es), and will be updated as needed. -In your `.eslintrc.js` (or any other file you use to configure eslint), -add the config you want from this package to the `extends` field. +## 🏁 Goal -TODO +[Espree](https://github.com/eslint/espree#readme), the default parser of [ESLint](https://eslint.org/), has supported `ecmaVersion` option. +However, the error messages of new syntax are not readable (e.g., "unexpected token" or something like). + +When we use this plugin along with the latest `ecmaVersion` option value, it tells us the readable error message for the new syntax, such as "ES2020 BigInt is forbidden." +Plus, this plugin lets us disable each syntactic feature individually. + +## 💿 Installation + +Use [npm](https://www.npmjs.com/) or a compatible tool. + +```console +npm install --save-dev eslint eslint-plugin-es-roikoren + +yarn add -D eslint eslint-plugin-es-roikoren +``` + +**IMPORTANT** + +If you installed `eslint` globally, you should install this plugin in the same way. + +::: tip Requirements +- Node.js `12.22.0` or newer. +- ESLint `5.16.0` or newer. + ::: + +## 📖 Usage + +Configure your `.eslintrc.*` file. + +For example, to enable only Rest/Spread Properties in ES2018, as similar to legacy `experimentalObjectRestSpread` option: + +```json +{ + "plugins": ["es-roikoren"], + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "es-roikoren/no-async-iteration": "error", + "es-roikoren/no-malformed-template-literals": "error", + "es-roikoren/no-regexp-lookbehind-assertions": "error", + "es-roikoren/no-regexp-named-capture-groups": "error", + "es-roikoren/no-regexp-s-flag": "error", + "es-roikoren/no-regexp-unicode-property-escapes": "error" + } +} +``` + +### Presets + +This plugin provides the following configs. + +| Config ID | Description | +|:----------|:------------| +| `plugin:es-roikoren/restrict-to-es2019` | disallow new stuff that ES2019 doesn't include. +| `plugin:es-roikoren/restrict-to-es2018` | disallow new stuff that ES2018 doesn't include. +| `plugin:es-roikoren/restrict-to-es2017` | disallow new stuff that ES2017 doesn't include. +| `plugin:es-roikoren/restrict-to-es2016` | disallow new stuff that ES2016 doesn't include. +| `plugin:es-roikoren/restrict-to-es2015` | disallow new stuff that ES2015 doesn't include. +| `plugin:es-roikoren/restrict-to-es5` | disallow new stuff that ES5 doesn't include. +| `plugin:es-roikoren/restrict-to-es3` | disallow new stuff that ES3 doesn't include. +| `plugin:es-roikoren/no-new-in-es2020` | disallow the new stuff in ES2020. +| `plugin:es-roikoren/no-new-in-es2019` | disallow the new stuff in ES2019. +| `plugin:es-roikoren/no-new-in-es2018` | disallow the new stuff in ES2018. +| `plugin:es-roikoren/no-new-in-es2017` | disallow the new stuff in ES2017. +| `plugin:es-roikoren/no-new-in-es2016` | disallow the new stuff in ES2016. +| `plugin:es-roikoren/no-new-in-es2015` | disallow the new stuff in ES2015. +| `plugin:es-roikoren/no-new-in-es5` | disallow the new stuff in ES5. +| `plugin:es-roikoren/no-new-in-esnext` | disallow the new stuff to be planned for the next yearly ECMAScript snapshot.
⚠️ This config will be changed in the minor versions of this plugin. + +For example: + +```json +{ + "parserOptions": { + "ecmaVersion": 2021 + }, + "extends": [ + "eslint:recommended", + "plugin:es-roikoren/restrict-to-es2018" + ], + "rules": { + "es-roikoren/no-rest-spread-properties": "off" + } +} +``` + +### The `aggressive` mode + +This plugin never reports prototype methods by default. Because it's hard to know the type of objects, it will cause false positives. + +If you configured the `aggressive` mode, this plugin reports prototype methods even if the rules couldn't know the type of objects. +For example: + +```json +{ + "plugins": ["es-roikoren"], + "rules": { + "es-roikoren/no-string-prototype-codepointat": "error" + }, + + // `settings.es.aggressive = true` means the aggressive mode. + "settings": { + "es": { "aggressive": true } + } +} +``` + +If using this plugin and TypeScript, this plugin reports prototype methods by default because we can easily know types. +For example: + +```json +{ + "plugins": ["es-roikoren"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json" + }, + "rules": { + "es-roikoren/no-string-prototype-codepointat": "error" + } + + // If you configured the `aggressive` mode, this plugin reports prototype methods on `any` types as well. + // "settings": { + // "es": { "aggressive": true } + // } +} +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9c545bc4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,165 @@ +# eslint-plugin-es-roikoren + +[![Test Status](https://github.com/roikoren755/eslint-plugin-es/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/roikoren755/eslint-plugin-es/actions/workflows/test.yml?query=branch%3Amain) +[![codecov](https://codecov.io/gh/roikoren755/eslint-plugin-es/branch/main/graph/badge.svg?token=RF5L5KQQN6)](https://codecov.io/gh/roikoren755/eslint-plugin-es) + +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=bugs)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=code_smells)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=security_rating)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=sqale_index)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=roikoren755_eslint-plugin-es&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=roikoren755_eslint-plugin-es) + +[![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/roikoren755/eslint-plugin-es)](https://app.snyk.io/org/roikoren755/project/fe8ed5b1-7498-4f48-abdc-132b863963e4) + +[![npm](https://img.shields.io/npm/v/eslint-plugin-es-roikoren)](https://www.npmjs.com/package/eslint-plugin-es-roikoren) +[![NPM](https://img.shields.io/npm/l/eslint-plugin-es-roikoren)](https://www.npmjs.com/package/eslint-plugin-es-roikoren) +[![npm](https://img.shields.io/npm/dm/eslint-plugin-es-roikoren)](https://www.npmjs.com/package/eslint-plugin-es-roikoren) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/eslint-plugin-es-roikoren)](https://www.npmjs.com/package/eslint-plugin-es-roikoren) + +[![GitHub issues](https://img.shields.io/github/issues-raw/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) +[![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) +[![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) +[![Lines of code](https://img.shields.io/tokei/lines/github/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) +[![GitHub top language](https://img.shields.io/github/languages/top/roikoren755/eslint-plugin-es)](https://www.github.com/roikoren755/eslint-plugin-es) + +A re-implementation of `eslint-plugin-es` in TypeScript. + +## Disclaimer +First off, I would like to deeply thank [@mistycatea](https://github.com/mysticatea) and everyone else involved in the original `eslin-plugin-es`. None of this would have been possible without them, and all credit should go to them. + +This package is an attempt to migrate `eslint-config-es` to be written in TypeScript, and to use the great [`@typescript-eslint`](https://github.com/typescript-eslint) repository for plugin development. + +Below is taken verbatim from [`eslint-plugin-es`](https://github.com/mysticatea/eslint-plugin-es), and will be updated as needed. + +## 🏁 Goal + +[Espree](https://github.com/eslint/espree#readme), the default parser of [ESLint](https://eslint.org/), has supported `ecmaVersion` option. +However, the error messages of new syntax are not readable (e.g., "unexpected token" or something like). + +When we use this plugin along with the latest `ecmaVersion` option value, it tells us the readable error message for the new syntax, such as "ES2020 BigInt is forbidden." +Plus, this plugin lets us disable each syntactic feature individually. + +## 💿 Installation + +Use [npm](https://www.npmjs.com/) or a compatible tool. + +```console +npm install --save-dev eslint eslint-plugin-es-roikoren + +yarn add -D eslint eslint-plugin-es-roikoren +``` + +**IMPORTANT** + +If you installed `eslint` globally, you should install this plugin in the same way. + +::: tip Requirements +- Node.js `12.22.0` or newer. +- ESLint `5.16.0` or newer. + ::: + +## 📖 Usage + +Configure your `.eslintrc.*` file. + +For example, to enable only Rest/Spread Properties in ES2018, as similar to legacy `experimentalObjectRestSpread` option: + +```json +{ + "plugins": ["es-roikoren"], + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "es-roikoren/no-async-iteration": "error", + "es-roikoren/no-malformed-template-literals": "error", + "es-roikoren/no-regexp-lookbehind-assertions": "error", + "es-roikoren/no-regexp-named-capture-groups": "error", + "es-roikoren/no-regexp-s-flag": "error", + "es-roikoren/no-regexp-unicode-property-escapes": "error" + } +} +``` + +### Presets + +This plugin provides the following configs. + +| Config ID | Description | +|:----------|:------------| +| `plugin:es-roikoren/restrict-to-es2019` | disallow new stuff that ES2019 doesn't include. +| `plugin:es-roikoren/restrict-to-es2018` | disallow new stuff that ES2018 doesn't include. +| `plugin:es-roikoren/restrict-to-es2017` | disallow new stuff that ES2017 doesn't include. +| `plugin:es-roikoren/restrict-to-es2016` | disallow new stuff that ES2016 doesn't include. +| `plugin:es-roikoren/restrict-to-es2015` | disallow new stuff that ES2015 doesn't include. +| `plugin:es-roikoren/restrict-to-es5` | disallow new stuff that ES5 doesn't include. +| `plugin:es-roikoren/restrict-to-es3` | disallow new stuff that ES3 doesn't include. +| `plugin:es-roikoren/no-new-in-es2020` | disallow the new stuff in ES2020. +| `plugin:es-roikoren/no-new-in-es2019` | disallow the new stuff in ES2019. +| `plugin:es-roikoren/no-new-in-es2018` | disallow the new stuff in ES2018. +| `plugin:es-roikoren/no-new-in-es2017` | disallow the new stuff in ES2017. +| `plugin:es-roikoren/no-new-in-es2016` | disallow the new stuff in ES2016. +| `plugin:es-roikoren/no-new-in-es2015` | disallow the new stuff in ES2015. +| `plugin:es-roikoren/no-new-in-es5` | disallow the new stuff in ES5. +| `plugin:es-roikoren/no-new-in-esnext` | disallow the new stuff to be planned for the next yearly ECMAScript snapshot.
⚠️ This config will be changed in the minor versions of this plugin. + +For example: + +```json +{ + "parserOptions": { + "ecmaVersion": 2021 + }, + "extends": [ + "eslint:recommended", + "plugin:es-roikoren/restrict-to-es2018" + ], + "rules": { + "es-roikoren/no-rest-spread-properties": "off" + } +} +``` + +### The `aggressive` mode + +This plugin never reports prototype methods by default. Because it's hard to know the type of objects, it will cause false positives. + +If you configured the `aggressive` mode, this plugin reports prototype methods even if the rules couldn't know the type of objects. +For example: + +```json +{ + "plugins": ["es-roikoren"], + "rules": { + "es-roikoren/no-string-prototype-codepointat": "error" + }, + + // `settings.es.aggressive = true` means the aggressive mode. + "settings": { + "es": { "aggressive": true } + } +} +``` + +If using this plugin and TypeScript, this plugin reports prototype methods by default because we can easily know types. +For example: + +```json +{ + "plugins": ["es-roikoren"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json" + }, + "rules": { + "es-roikoren/no-string-prototype-codepointat": "error" + } + + // If you configured the `aggressive` mode, this plugin reports prototype methods on `any` types as well. + // "settings": { + // "es": { "aggressive": true } + // } +} +``` diff --git a/package.json b/package.json index 783f012e..b82ef167 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "license": "MIT", "main": "./src/index.js", "files": [ - "**/*.js" + "./src/**/*.js", + "./docs/**/*" ], "scripts": { "build": "tsc", @@ -27,7 +28,7 @@ "release": "changeset publish", "test": "nyc mocha tests/**/*.ts --reporter dot", "type": "tsc --noEmit --noImplicitAny", - "update": "yarn lint --fix && yarn update:configs && yarn update:index && yarn update:doc && yarn update:ruledocs", + "update": "yarn update:configs && yarn update:index && yarn update:doc && yarn update:ruledocs", "update:configs": "ts-node scripts/update-src-configs", "update:doc": "ts-node scripts/update-docs-readme", "update:index": "ts-node scripts/update-src-index", diff --git a/scripts/new-rule.ts b/scripts/new-rule.ts index 819fce91..51604b10 100644 --- a/scripts/new-rule.ts +++ b/scripts/new-rule.ts @@ -1,8 +1,8 @@ -import { execSync } from 'child_process'; import { writeFileSync } from 'fs'; import path from 'path'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; -((ruleId: string): void => { +const run = async (ruleId: string): Promise => { if (!ruleId) { console.error('Usage: npm run new '); process.exitCode = 1; @@ -23,7 +23,7 @@ import path from 'path'; writeFileSync( ruleFile, - `import { createRule } from '../utils/create-rule'; + `import { createRule } from '../util/create-rule'; export const category = 'ES2021'; export default createRule<[], 'forbidden'>({ @@ -43,7 +43,7 @@ export default createRule<[], 'forbidden'>({ ); writeFileSync( testFile, - `import { RuleTester } = from '../../tester'; + `import { RuleTester } from '../../tester'; import rule from '../../../src/rules/${ruleId}'; if (!RuleTester.isSupported(2021)) { @@ -73,7 +73,7 @@ This rule reports ??? as errors. `, ); - execSync(`code "${ruleFile}"`); - execSync(`code "${testFile}"`); - execSync(`code "${docFile}"`); -})(process.argv[2]); + await TSESLint.ESLint.outputFixes((await new TSESLint.ESLint({ fix: true }).lintFiles([ruleFile, testFile])) as never); +}; + +run(process.argv[2]).catch(console.error); diff --git a/scripts/rules.ts b/scripts/rules.ts index d9c49dde..49713342 100644 --- a/scripts/rules.ts +++ b/scripts/rules.ts @@ -39,7 +39,7 @@ const rules: IRule[] = []; for (const entry of readdirSync(dirPath, { withFileTypes: true })) { if (entry.isDirectory()) { walk(path.join(dirPath, entry.name)); - // eslint-disable-next-line no-continue + continue; } diff --git a/scripts/update-src-index.ts b/scripts/update-src-index.ts index 99c249e0..99092c47 100644 --- a/scripts/update-src-index.ts +++ b/scripts/update-src-index.ts @@ -14,6 +14,10 @@ const run = async (): Promise => { .map((f) => path.basename(f, '.ts')) .sort(collator.compare.bind(collator)); const ruleIds = rules.map((r) => r.ruleId).sort(collator.compare.bind(collator)); + const configImports = configIds.map((id) => `import ${camelcase(id)} from './configs/${id}';`).join('\n'); + const ruleImports = ruleIds.map((id) => `import ${camelcase(id)} from './rules/${id}';`).join('\n'); + const configs = configIds.map((id) => `'${id}': ${camelcase(id)}`).join(','); + const rulesField = ruleIds.map((id) => `'${id}': ${camelcase(id)}`).join(','); fs.writeFileSync( 'src/index.ts', @@ -21,15 +25,15 @@ const run = async (): Promise => { * DON'T EDIT THIS FILE. * This file was generated automatically by 'scripts/update-src-index.ts'. */ -${configIds.map((id) => `import ${camelcase(id)} from './configs/${id}';`).join('\n')} -${ruleIds.map((id) => `import ${camelcase(id)} from './rules/${id}';`).join('\n')} +${configImports} +${ruleImports} export default { configs: { - ${configIds.map((id) => `'${id}': ${camelcase(id)}`).join(',')}, + ${configs}, }, rules: { - ${ruleIds.map((id) => `'${id}': ${camelcase(id)}`).join(',')} + ${rulesField} }, }; `, diff --git a/src/rules/no-malformed-template-literals.ts b/src/rules/no-malformed-template-literals.ts index 9c1053fb..3097357b 100644 --- a/src/rules/no-malformed-template-literals.ts +++ b/src/rules/no-malformed-template-literals.ts @@ -17,9 +17,9 @@ export default createRule<[], 'forbidden'>({ return { 'TemplateElement[value.cooked=null]'(elementNode: TSESTree.TemplateElement) { - const node = elementNode.parent as TSESTree.Node; + const node = elementNode.parent; - if (!reported.has(node)) { + if (node && !reported.has(node)) { reported.add(node); context.report({ node, messageId: 'forbidden' }); } diff --git a/src/rules/no-property-shorthands.ts b/src/rules/no-property-shorthands.ts index 579bd3e1..cb0a88c0 100644 --- a/src/rules/no-property-shorthands.ts +++ b/src/rules/no-property-shorthands.ts @@ -37,11 +37,11 @@ export default createRule<[], 'forbidden'>({ const keyText = sourceCode.text.slice(firstKeyToken?.range[0], lastKeyToken?.range[1]); let functionHeader = 'function'; - if ((node.value as TSESTree.FunctionExpression).async) { + if ('async' in node.value && node.value.async) { functionHeader = `async ${functionHeader}`; } - if ((node.value as TSESTree.FunctionExpression).generator) { + if ('generator' in node.value && node.value.generator) { functionHeader = `${functionHeader}*`; } @@ -55,13 +55,15 @@ export default createRule<[], 'forbidden'>({ 'ObjectExpression > :matches(Property[method=true], Property[shorthand=true])'( node: TSESTree.PropertyNonComputedName, ) { - context.report({ - node, - messageId: 'forbidden', - fix: node.method - ? (fixer) => makeFunctionLongform(fixer, node) - : (fixer) => fixer.insertTextAfter(node.key, `: ${(node.key as TSESTree.Identifier).name}`), - }); + if (node.method || 'name' in node.key) { + context.report({ + node, + messageId: 'forbidden', + fix: node.method + ? (fixer) => makeFunctionLongform(fixer, node) + : (fixer) => fixer.insertTextAfter(node.key, `: ${'name' in node.key ? node.key.name : ''}`), + }); + } }, }; }, diff --git a/src/rules/no-subclassing-builtins.ts b/src/rules/no-subclassing-builtins.ts index 71c13305..83f2f36c 100644 --- a/src/rules/no-subclassing-builtins.ts +++ b/src/rules/no-subclassing-builtins.ts @@ -1,5 +1,4 @@ import { ASTUtils } from '@typescript-eslint/experimental-utils'; -import type { TSESTree } from '@typescript-eslint/typescript-estree'; import { createRule } from '../util/create-rule'; @@ -30,7 +29,7 @@ export default createRule<[], 'forbidden'>({ Set: { [ASTUtils.ReferenceTracker.READ]: true }, String: { [ASTUtils.ReferenceTracker.READ]: true }, })) { - if (node.parent?.type.startsWith('Class') && (node.parent as TSESTree.ClassExpression).superClass === node) { + if (node.parent?.type.startsWith('Class') && 'superClass' in node.parent && node.parent.superClass === node) { context.report({ node, messageId: 'forbidden', data: { name: path.join('.') } }); } } diff --git a/src/util/define-prototype-method-handler.ts b/src/util/define-prototype-method-handler.ts index c6b89780..290ab30d 100644 --- a/src/util/define-prototype-method-handler.ts +++ b/src/util/define-prototype-method-handler.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import { ASTUtils } from '@typescript-eslint/experimental-utils'; import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; +import type { ParserServices } from '@typescript-eslint/typescript-estree'; import type * as TypeScript from 'typescript'; import { optionalRequire } from './optional-require'; @@ -119,168 +120,188 @@ const isUnionOrIntersection = (type: TypeScript.Type): type is TypeScript.UnionO */ const isUnknown = (type: TypeScript.Type): boolean => (type.flags & (ts as TS).TypeFlags.Unknown) !== 0; // eslint-disable-line no-bitwise +interface IOptions { + aggressive: boolean; + checker?: TypeScript.TypeChecker | undefined; + hasFullType: boolean; + isTS: boolean; + tsNodeMap?: ParserServices['esTreeNodeToTSNodeMap'] | undefined; +} + /** - * Define handlers to disallow prototype methods. - * @param {TSESLint.RuleContext<'forbidden', readonly []>} context The rule context. - * @param {Record} nameMap The method names to disallow. The key is class names and that value is method names. - * @returns {TSESLint.RuleFunction} The defined handlers. + * Get the constraint type of a given type parameter type if exists. + * + * `type.getConstraint()` method doesn't return the constraint type of the + * type parameter for some reason. So this gets the constraint type via AST. + * + * @param {TypeScript.TypeParameter} type The type parameter type to get. + * @param {Pick} options The options containing the type checker to use. + * @returns {TypeScript.Type | undefined} The constraint type. */ -export const definePrototypeMethodHandler = ( - context: TSESLint.RuleContext<'forbidden', [options: IAggressive]>, - nameMap: Record, -): TSESLint.RuleListener => { - const aggressive = getAggressiveOption(context); - - const tsNodeMap = context.parserServices?.esTreeNodeToTSNodeMap; - const checker = context.parserServices?.program?.getTypeChecker(); +const getConstraintType = ( + type: TypeScript.TypeParameter, + { checker }: Pick, +): TypeScript.Type | undefined => { + const { symbol } = type; + const declarations = symbol?.declarations; + const declaration = declarations?.[0]; + + if (declaration && ts?.isTypeParameterDeclaration(declaration) && declaration.constraint) { + return checker?.getTypeFromTypeNode(declaration.constraint); + } - const isTS = Boolean(ts && tsNodeMap && checker); - const hasFullType = isTS && context.parserServices?.hasFullTypeInformation !== false; + // eslint-disable-next-line consistent-return + return undefined; +}; - /** - * Get the constraint type of a given type parameter type if exists. - * - * `type.getConstraint()` method doesn't return the constraint type of the - * type parameter for some reason. So this gets the constraint type via AST. - * - * @param {TypeScript.TypeParameter} type The type parameter type to get. - * @returns {TypeScript.Type | undefined} The constraint type. - */ - const getConstraintType = (type: TypeScript.TypeParameter): TypeScript.Type | undefined => { - const { symbol } = type; - const declarations = symbol?.declarations; - const declaration = declarations?.[0]; - - if ( - ts?.isTypeParameterDeclaration(declaration as TypeScript.Declaration) && - (declaration as TypeScript.TypeParameterDeclaration).constraint - ) { - return checker?.getTypeFromTypeNode( - (declaration as TypeScript.TypeParameterDeclaration).constraint as TypeScript.TypeNode, - ); - } +/** + * Check if the name of the given type is expected or not. + * @param {TypeScript.Type} type The type to check. + * @param {string} className The expected type name. + * @param {IOptions} options The options to use. + * @returns {boolean} `true` if should disallow it. + */ +const typeEquals = (type: TypeScript.Type, className: string, options: IOptions): boolean => { + if (isAny(type) || isUnknown(type)) { + return options.aggressive; + } - // eslint-disable-next-line consistent-return - return undefined; - }; + if (isAnonymousObject(type)) { + // In non full-type mode, array types (e.g. `any[]`) become anonymous object type. + return options.hasFullType ? false : options.aggressive; + } - /** - * Check if the name of the given type is expected or not. - * @param {TypeScript.Type} type The type to check. - * @param {string} className The expected type name. - * @returns {boolean} `true` if should disallow it. - */ - const typeEquals = (type: TypeScript.Type, className: string): boolean => { - if (isAny(type) || isUnknown(type)) { - return aggressive; - } + if (isStringLike(type)) { + return className === 'String'; + } - if (isAnonymousObject(type)) { - // In non full-type mode, array types (e.g. `any[]`) become anonymous object type. - return hasFullType ? false : aggressive; - } + if (isArrayLikeObject(type)) { + return className === 'Array'; + } - if (isStringLike(type)) { - return className === 'String'; - } + if (isReferenceObject(type) && type.target !== type) { + return typeEquals(type.target, className, options); + } - if (isArrayLikeObject(type)) { - return className === 'Array'; - } + if (isTypeParameter(type)) { + const constraintType = getConstraintType(type, options); - if (isReferenceObject(type) && type.target !== type) { - return typeEquals(type.target, className); + if (constraintType) { + return typeEquals(constraintType, className, options); } - if (isTypeParameter(type)) { - const constraintType = getConstraintType(type); + return options.hasFullType ? false : options.aggressive; + } - if (constraintType) { - return typeEquals(constraintType, className); - } + if (isUnionOrIntersection(type)) { + return type.types.some((t) => typeEquals(t, className, options)); + } - return hasFullType ? false : aggressive; - } + if (isClassOrInterface(type)) { + const name = type.symbol.escapedName; - if (isUnionOrIntersection(type)) { - return type.types.some((t) => typeEquals(t, className)); - } + return name === className || name === `Readonly${className}`; + } - if (isClassOrInterface(type)) { - const name = type.symbol.escapedName; + return options.checker?.typeToString(type) === className; +}; - return name === className || name === `Readonly${className}`; +/** + * Check if the type of the given node by the declaration of `node.property`. + * @param {MemberExpression} memberAccessNode The MemberExpression node. + * @param {string} className The class name to disallow. + * @param {IOptions} options The options to use. + * @returns {boolean} `true` if should disallow it. + */ +const checkByPropertyDeclaration = ( + memberAccessNode: TSESTree.MemberExpression, + className: string, + options: IOptions, +): boolean => { + const tsNode = options.tsNodeMap?.get(memberAccessNode.property); + const symbol = tsNode && options.checker?.getSymbolAtLocation(tsNode); + const declarations = symbol?.declarations; + + if (declarations) { + for (const declaration of declarations) { + const type = options.checker?.getTypeAtLocation(declaration.parent); + + if (type && typeEquals(type, className, options)) { + return true; + } } + } - return checker?.typeToString(type) === className; - }; + return false; +}; - /** - * Check if the type of the given node by the declaration of `node.property`. - * @param {MemberExpression} memberAccessNode The MemberExpression node. - * @param {string} className The class name to disallow. - * @returns {boolean} `true` if should disallow it. - */ - const checkByPropertyDeclaration = (memberAccessNode: TSESTree.MemberExpression, className: string): boolean => { - const tsNode = tsNodeMap?.get(memberAccessNode.property); - const symbol = tsNode && checker?.getSymbolAtLocation(tsNode); - const declarations = symbol?.declarations; - - if (declarations) { - for (const declaration of declarations) { - const type = checker?.getTypeAtLocation(declaration.parent); - - if (type && typeEquals(type, className)) { - return true; - } - } - } +/** + * Check if the type of the given node by the type of `node.object`. + * @param {MemberExpression} memberAccessNode The MemberExpression node. + * @param {string} className The class name to disallow. + * @param {IOptions} options The options to use. + * @returns {boolean} `true` if should disallow it. + */ +const checkByObjectExpressionType = ( + memberAccessNode: TSESTree.MemberExpression, + className: string, + options: IOptions, +): boolean => { + const tsNode = options.tsNodeMap?.get(memberAccessNode.object); + const type = options.checker?.getTypeAtLocation(tsNode as TypeScript.Node); + + return typeEquals(type as TypeScript.Type, className, options); +}; - return false; - }; +/** + * Check if the type of the given node is one of given class or not. + * @param {MemberExpression} memberAccessNode The MemberExpression node. + * @param {string} className The class name to disallow. + * @param {IOptions} options The options to use. + * @returns {boolean} `true` if should disallow it. + */ +const checkObjectType = (memberAccessNode: TSESTree.MemberExpression, className: string, options: IOptions): boolean => { + // If it's obvious, shortcut. + if (memberAccessNode.object.type === 'ArrayExpression') { + return className === 'Array'; + } - /** - * Check if the type of the given node by the type of `node.object`. - * @param {MemberExpression} memberAccessNode The MemberExpression node. - * @param {string} className The class name to disallow. - * @returns {boolean} `true` if should disallow it. - */ - const checkByObjectExpressionType = (memberAccessNode: TSESTree.MemberExpression, className: string): boolean => { - const tsNode = tsNodeMap?.get(memberAccessNode.object); - const type = checker?.getTypeAtLocation(tsNode as TypeScript.Node); - - return typeEquals(type as TypeScript.Type, className); - }; + if (memberAccessNode.object.type === 'Literal' && 'regex' in memberAccessNode.object) { + return className === 'RegExp'; + } - /** - * Check if the type of the given node is one of given class or not. - * @param {MemberExpression} memberAccessNode The MemberExpression node. - * @param {string} className The class name to disallow. - * @returns {boolean} `true` if should disallow it. - */ - const checkObjectType = (memberAccessNode: TSESTree.MemberExpression, className: string): boolean => { - // If it's obvious, shortcut. - if (memberAccessNode.object.type === 'ArrayExpression') { - return className === 'Array'; - } + if ( + (memberAccessNode.object.type === 'Literal' && typeof memberAccessNode.object.value === 'string') || + memberAccessNode.object.type === 'TemplateLiteral' + ) { + return className === 'String'; + } - if (memberAccessNode.object.type === 'Literal' && (memberAccessNode.object as TSESTree.RegExpLiteral).regex) { - return className === 'RegExp'; - } + // Test object type. + return options.isTS + ? checkByPropertyDeclaration(memberAccessNode, className, options) || + checkByObjectExpressionType(memberAccessNode, className, options) + : options.aggressive; +}; - if ( - (memberAccessNode.object.type === 'Literal' && typeof memberAccessNode.object.value === 'string') || - memberAccessNode.object.type === 'TemplateLiteral' - ) { - return className === 'String'; - } +/** + * Define handlers to disallow prototype methods. + * @param {TSESLint.RuleContext<'forbidden', readonly []>} context The rule context. + * @param {Record} nameMap The method names to disallow. The key is class names and that value is method names. + * @returns {TSESLint.RuleFunction} The defined handlers. + */ +export const definePrototypeMethodHandler = ( + context: TSESLint.RuleContext<'forbidden', [options: IAggressive]>, + nameMap: Record, +): TSESLint.RuleListener => { + const aggressive = getAggressiveOption(context); - // Test object type. - return isTS - ? checkByPropertyDeclaration(memberAccessNode, className) || - checkByObjectExpressionType(memberAccessNode, className) - : aggressive; - }; + const tsNodeMap = context.parserServices?.esTreeNodeToTSNodeMap; + const checker = context.parserServices?.program?.getTypeChecker(); + + const isTS = Boolean(ts && tsNodeMap && checker); + const hasFullType = isTS && context.parserServices?.hasFullTypeInformation !== false; + const options: IOptions = { aggressive, checker, hasFullType, isTS, tsNodeMap }; // For performance const nameMapEntries = Object.entries(nameMap); @@ -292,7 +313,7 @@ export const definePrototypeMethodHandler = ( MemberExpression(node) { const propertyName = ASTUtils.getPropertyName(node, context.getScope()); - if (methodNames.includes(propertyName as string) && checkObjectType(node, className)) { + if (methodNames.includes(propertyName as string) && checkObjectType(node, className, options)) { context.report({ node, messageId: 'forbidden', @@ -308,7 +329,7 @@ export const definePrototypeMethodHandler = ( const propertyName = ASTUtils.getPropertyName(node, context.getScope()); for (const [className, methodNames] of nameMapEntries) { - if (methodNames.includes(propertyName as string) && checkObjectType(node, className)) { + if (methodNames.includes(propertyName as string) && checkObjectType(node, className, options)) { context.report({ node, messageId: 'forbidden', diff --git a/src/util/get-regexp-calls.ts b/src/util/get-regexp-calls.ts index bd664954..ce466dd2 100644 --- a/src/util/get-regexp-calls.ts +++ b/src/util/get-regexp-calls.ts @@ -15,7 +15,11 @@ export function* getRegExpCalls( for (const { node } of tracker.iterateGlobalReferences({ RegExp: { [ASTUtils.ReferenceTracker.CALL]: true, [ASTUtils.ReferenceTracker.CONSTRUCT]: true }, })) { - const [patternNode, flagsNode] = (node as TSESTree.CallExpression).arguments; + if (!('arguments' in node)) { + continue; + } + + const [patternNode, flagsNode] = node.arguments; yield { node, diff --git a/tests/src/rules/no-optional-chaining.ts b/tests/src/rules/no-optional-chaining.ts index 321d1b99..40c120b9 100644 --- a/tests/src/rules/no-optional-chaining.ts +++ b/tests/src/rules/no-optional-chaining.ts @@ -1,9 +1,16 @@ -import { AST_TOKEN_TYPES } from '@typescript-eslint/types'; +// import { AST_TOKEN_TYPES } from '@typescript-eslint/types'; import { RuleTester } from '../../tester'; import rule from '../../../src/rules/no-optional-chaining'; -const error = { messageId: 'forbidden' as const, line: 1, type: AST_TOKEN_TYPES.Punctuator, data: {} }; +const error = { + messageId: 'forbidden' as const, + line: 1, + // TODO - type should be AST_TOKEN_TYPES.Punctuator, but it doesn't return as such in eslint@6 + // TODO - we should revert this change when dropping support for eslint@6 + // type: AST_TOKEN_TYPES.Punctuator, + data: {}, +}; if (!RuleTester.isSupported(2020)) { console.log('Skip the tests of no-optional-chaining.'); diff --git a/tests/src/rules/no-regexp-unicode-property-escapes-2019.ts b/tests/src/rules/no-regexp-unicode-property-escapes-2019.ts index 8d5eda12..5204c50a 100644 --- a/tests/src/rules/no-regexp-unicode-property-escapes-2019.ts +++ b/tests/src/rules/no-regexp-unicode-property-escapes-2019.ts @@ -4,12 +4,12 @@ import { AST_NODE_TYPES } from '@typescript-eslint/types'; import { RuleTester } from '../../tester'; import rule from '../../../src/rules/no-regexp-unicode-property-escapes-2019'; -const error = (options?: { value?: string; notLiteral?: boolean }): TSESLint.TestCaseError<'forbidden'> => ({ - messageId: 'forbidden', - line: 1, - type: options?.notLiteral ? AST_NODE_TYPES.NewExpression : AST_NODE_TYPES.Literal, - data: { value: `\\p{${options?.value ? `Script=${options.value}` : 'Extended_Pictographic'}}` }, -}); +const error = (options?: { value?: string; notLiteral?: boolean }): TSESLint.TestCaseError<'forbidden'> => { + const value = options?.value ? `Script=${options.value}` : 'Extended_Pictographic'; + const type = options?.notLiteral ? AST_NODE_TYPES.NewExpression : AST_NODE_TYPES.Literal; + + return { messageId: 'forbidden', line: 1, type, data: { value: `\\p{${value}}` } }; +}; if (!RuleTester.isSupported(2019)) { console.log('Skip the tests of no-regexp-unicode-property-escapes-2019.'); diff --git a/yarn.lock b/yarn.lock index 25dcb4f0..33f328c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -593,9 +593,9 @@ minimatch "^3.0.4" "@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -1937,9 +1937,9 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.1.0: - version "3.2.2" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" - integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== + version "3.2.4" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== foreground-child@^2.0.0: version "2.0.0"