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"