diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..e5b6d8d6a --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..43b72b358 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config/schema.json", + "changelog": [ + "@svitejs/changesets-changelog-github-compact", + { + "repo": "vuejs/eslint-plugin-vue" + } + ], + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "master", + "bumpVersionsWithWorkspaceProtocolOnly": true, + "ignore": [] +} diff --git a/.changeset/smooth-jokes-eat.md b/.changeset/smooth-jokes-eat.md new file mode 100644 index 000000000..e5f3a14a8 --- /dev/null +++ b/.changeset/smooth-jokes-eat.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-vue": patch +--- + +Updates resources diff --git a/.changeset/true-oranges-heal.md b/.changeset/true-oranges-heal.md new file mode 100644 index 000000000..eba8b9a99 --- /dev/null +++ b/.changeset/true-oranges-heal.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-vue': patch +--- + +[vue/no-restricted-html-elements](https://eslint.vuejs.org/rules/no-restricted-html-elements.html) now also checks SVG and MathML elements. diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index d593e239e..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,86 +0,0 @@ -workflows: - version: 2 - node-multi-build: - jobs: - - node-v8 - - node-v10 - - node-v12 - - node-v14 - - lint - -version: 2 -jobs: - node-base: &node-base - docker: - - image: node - steps: - - run: - name: Versions - command: npm version - - checkout - - restore_cache: - keys: - - v2-npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} - - run: - name: Install dependencies - command: npm install - - save_cache: - key: v2-npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} - paths: - - node_modules - - run: - name: Test - command: npm test - - node-v8: - docker: - - image: node:8 - steps: - - run: - name: Versions - command: npm version - - checkout - - run: - name: Install eslint@6 - command: | - npm install -D eslint@6.2.0 - - run: - name: Install dependencies - command: npm install - - run: - name: Test - command: npm test - node-v10: - <<: *node-base - docker: - - image: node:10 - node-v12: - <<: *node-base - docker: - - image: node:12 - node-v14: - <<: *node-base - docker: - - image: node:14 - - lint: - docker: - - image: node:14 - steps: - - run: - name: Versions - command: npm version - - checkout - - restore_cache: - keys: - - v2-npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} - - run: - name: Install dependencies - command: npm install - - save_cache: - key: v2-npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }} - paths: - - node_modules - - run: - name: Test - command: npm run lint diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 1905aa4a8..000000000 --- a/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -/.nyc_output -/coverage -/node_modules -/tests/fixtures -/tests/integrations/eslint-plugin-import - -!.vuepress -/docs/.vuepress/dist diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 704f09732..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict' - -module.exports = { - root: true, - parserOptions: { - ecmaVersion: 2018 - }, - env: { - es6: true, - node: true, - mocha: true - }, - extends: ['plugin:eslint-plugin/recommended', 'prettier'], - plugins: ['eslint-plugin', 'prettier'], - rules: { - 'accessor-pairs': 2, - camelcase: [2, { properties: 'never' }], - 'constructor-super': 2, - eqeqeq: [2, 'allow-null'], - 'handle-callback-err': [2, '^(err|error)$'], - 'jsx-quotes': [2, 'prefer-single'], - 'new-cap': [2, { newIsCap: true, capIsNew: false }], - 'new-parens': 2, - 'no-array-constructor': 2, - 'no-caller': 2, - 'no-class-assign': 2, - 'no-cond-assign': 2, - 'no-const-assign': 2, - 'no-control-regex': 2, - 'no-delete-var': 2, - 'no-dupe-args': 2, - 'no-dupe-class-members': 2, - 'no-dupe-keys': 2, - 'no-duplicate-case': 2, - 'no-empty-character-class': 2, - 'no-empty-pattern': 2, - 'no-eval': 2, - 'no-ex-assign': 2, - 'no-extend-native': 2, - 'no-extra-bind': 2, - 'no-extra-boolean-cast': 2, - 'no-extra-parens': [2, 'functions'], - 'no-fallthrough': 2, - 'no-floating-decimal': 2, - 'no-func-assign': 2, - 'no-implied-eval': 2, - 'no-inner-declarations': [2, 'functions'], - 'no-invalid-regexp': 2, - 'no-irregular-whitespace': 2, - 'no-iterator': 2, - 'no-label-var': 2, - 'no-labels': [2, { allowLoop: false, allowSwitch: false }], - 'no-lone-blocks': 2, - 'no-multi-spaces': [2, { ignoreEOLComments: true }], - 'no-multi-str': 2, - 'no-native-reassign': 2, - 'no-negated-in-lhs': 2, - 'no-new-object': 2, - 'no-new-require': 2, - 'no-new-symbol': 2, - 'no-new-wrappers': 2, - 'no-obj-calls': 2, - 'no-octal': 2, - 'no-octal-escape': 2, - 'no-path-concat': 2, - 'no-proto': 2, - 'no-redeclare': 2, - 'no-regex-spaces': 2, - 'no-return-assign': [2, 'except-parens'], - 'no-self-assign': 2, - 'no-self-compare': 2, - 'no-sequences': 2, - 'no-shadow-restricted-names': 2, - 'no-sparse-arrays': 2, - 'no-this-before-super': 2, - 'no-throw-literal': 2, - 'no-undef': 2, - 'no-undef-init': 2, - 'no-unexpected-multiline': 2, - 'no-unmodified-loop-condition': 2, - 'no-unneeded-ternary': [2, { defaultAssignment: false }], - 'no-unreachable': 2, - 'no-unsafe-finally': 2, - 'no-unused-vars': [2, { vars: 'all', args: 'none' }], - 'no-useless-call': 2, - 'no-useless-computed-key': 2, - 'no-useless-constructor': 2, - 'no-useless-escape': 0, - 'no-with': 2, - 'one-var': [2, { initialized: 'never' }], - 'use-isnan': 2, - 'valid-typeof': 2, - 'wrap-iife': [2, 'any'], - yoda: [2, 'never'], - 'prefer-const': 2, - - 'prettier/prettier': 'error', - 'eslint-plugin/report-message-format': ['error', "^[A-Z`'{].*\\.$"], - 'eslint-plugin/prefer-placeholders': 'error', - 'eslint-plugin/consistent-output': 'error', - - 'no-debugger': 'error', - 'no-console': 'error', - 'no-alert': 'error', - 'no-void': 'error', - - 'no-warning-comments': 'warn', - 'no-var': 'error', - 'prefer-template': 'error', - 'object-shorthand': 'error', - 'prefer-rest-params': 'error', - 'prefer-arrow-callback': 'error', - 'prefer-spread': 'error', - - 'dot-notation': 'error' - }, - overrides: [ - { - files: ['./**/*.vue'], - parser: require.resolve('vue-eslint-parser'), - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module' - } - }, - { - files: ['lib/rules/*.js'], - rules: { - 'consistent-docs-description': 'error', - 'no-invalid-meta': 'error', - 'no-invalid-meta-docs-categories': 'error', - 'eslint-plugin/require-meta-type': 'error', - 'require-meta-docs-url': [ - 'error', - { - pattern: `https://eslint.vuejs.org/rules/{{name}}.html` - } - ], - - 'eslint-plugin/fixer-return': 'off' - } - } - ] -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..c4d031e9c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: + - ota-meshi + - FloEdelmann diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cc2e88ff0..08d755127 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,6 +26,7 @@ about: Create a report to help us improve - **ESLint version:** - **eslint-plugin-vue version:** +- **Vue version:** - **Node version:** - **Operating System:** @@ -51,4 +52,9 @@ about: Create a report to help us improve --> **Repository to reproduce this issue** + diff --git a/.github/ISSUE_TEMPLATE/change.md b/.github/ISSUE_TEMPLATE/change.md index a6a4a7dc0..02397f6cd 100644 --- a/.github/ISSUE_TEMPLATE/change.md +++ b/.github/ISSUE_TEMPLATE/change.md @@ -13,6 +13,7 @@ about: Request a change that is not a bug fix, rule change, or new rule - **ESLint version:** - **eslint-plugin-vue version:** +- **Vue version:** - **Node version:** **The problem you want to solve.** diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 000000000..57aa5876d --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,68 @@ +name: CI +on: + push: + branches: + - 'master' + pull_request: + types: + - 'opened' + - 'synchronize' + - 'reopened' + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + - name: Install Packages + run: npm install --legacy-peer-deps + - name: Lint + run: npm run lint + + test: + name: Test + strategy: + matrix: + node: [18, 20, 21, 'lts/*'] + eslint: [9] + include: + # On old ESLint version + - node: 18 + eslint: 8 + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js v${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - name: Install Packages + run: npm install -f + - name: Install ESLint v${{ matrix.eslint }} + run: npm install --save-dev eslint@${{ matrix.eslint }} -f + - name: Test + run: npm test + + test-without-eslint-stylistic: + name: Test without ESLint Stylistic + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + - name: Install Packages + run: npm install -f + - name: Uninstall @stylistic/eslint-plugin + run: npm uninstall -D @stylistic/eslint-plugin + - name: Test + run: npm test diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml new file mode 100644 index 000000000..260b73582 --- /dev/null +++ b/.github/workflows/Release.yml @@ -0,0 +1,35 @@ +name: Release + +on: + push: + branches: + - master + +permissions: {} + +jobs: + release: + # prevents this action from running on forks + if: github.repository == 'vuejs/eslint-plugin-vue' + permissions: + contents: write # to create release (changesets/action) + pull-requests: write # to create pull request (changesets/action) + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + - name: Install Dependencies + run: npm install -f + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + version: npm run changeset:version + publish: npm run changeset:publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/check-for-resources-update.yml b/.github/workflows/check-for-resources-update.yml new file mode 100644 index 000000000..31f881b1d --- /dev/null +++ b/.github/workflows/check-for-resources-update.yml @@ -0,0 +1,31 @@ +name: Check for utils resources update +on: + workflow_dispatch: null + schedule: + - cron: 0 0 * * 0 # At 00:00 on Sunday, see https://crontab.guru/#0_0_*_*_0 + +permissions: + contents: write + pull-requests: write + +jobs: + check-for-resources-update: + runs-on: ubuntu-latest + if: ${{ github.repository == 'vuejs/eslint-plugin-vue' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install Packages + run: npm install + - name: Update + run: npm run update-resources + - uses: peter-evans/create-pull-request@v7 + with: + commit-message: Updates resources + branch: update-resources + branch-suffix: timestamp + title: Updates resources diff --git a/.gitignore b/.gitignore index e1401b951..797d0cbcd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,9 @@ /test.* yarn.lock yarn-error.log -docs/.vuepress/dist +/docs/.vitepress/dist +/docs/.vitepress/build-system/shim/eslint.mjs +/docs/.vitepress/build-system/shim/assert.mjs +/docs/.vitepress/.temp +/docs/.vitepress/cache typings/eslint/lib/rules diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 000000000..6591a3543 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,26 @@ +line-length: false +link-fragments: false +single-title: false +no-inline-html: + allowed_elements: + - badge + - eslint-code-block + - sup + - rules-table + - span + +# enforce consistency +code-block-style: + style: fenced +code-fence-style: + style: backtick +emphasis-style: + style: underscore +heading-style: + style: atx +hr-style: + style: --- +strong-style: + style: asterisk +ul-style: + style: dash diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 000000000..e7becf85b --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,2 @@ +node_modules +CHANGELOG.md diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..d341f1772 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +tests/fixtures/ +.github/ISSUE_TEMPLATE/*.md diff --git a/.vscode/launch.json b/.vscode/launch.json index 324a79864..2fa4db9b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,10 +6,7 @@ "request": "launch", "name": "Start testing", "program": "${workspaceFolder}/node_modules/.bin/mocha", - "args": [ - "${file}", - "--watch" - ], + "args": ["${file}", "--watch"], "console": "integratedTerminal" } ] diff --git a/.vscode/settings.json b/.vscode/settings.json index e8e944bef..f80681b66 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,15 @@ { - "editor.tabSize": 2, - "eslint.options": { - "rulePaths": ["eslint-internal-rules"] - }, - "eslint.validate": [ - "javascript", - "javascriptreact", - "vue" - ], - "typescript.tsdk": "node_modules/typescript/lib", - "vetur.validation.script": false + "editor.tabSize": 2, + "eslint.experimental.useFlatConfig": true, + "eslint.validate": ["javascript", "javascriptreact", "vue", "json", "jsonc"], + "typescript.tsdk": "./node_modules/typescript/lib", + "vetur.validation.script": false, + "[typescript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..b21ab6f6f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# eslint-plugin-vue + +## 10.2.0 + +### Minor Changes + +- [vue/no-restricted-html-elements](https://eslint.vuejs.org/rules/no-restricted-html-elements.html) now accepts multiple elements in each entry. ([#2750](https://github.com/vuejs/eslint-plugin-vue/pull/2750)) + +### Patch Changes + +- Updates resources ([#2747](https://github.com/vuejs/eslint-plugin-vue/pull/2747)) diff --git a/README.md b/README.md index c8951f0a3..bdeb5e167 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,54 @@ [![NPM version](https://img.shields.io/npm/v/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue) [![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue) -[![CircleCI](https://img.shields.io/circleci/project/github/vuejs/eslint-plugin-vue/master.svg?style=flat)](https://circleci.com/gh/vuejs/eslint-plugin-vue) -[![License](https://img.shields.io/github/license/vuejs/eslint-plugin-vue.svg?style=flat)](https://github.com/vuejs/eslint-plugin-vue/blob/master/LICENSE.md) +[![CI](https://img.shields.io/github/actions/workflow/status/vuejs/eslint-plugin-vue/CI.yml?style=flat&label=CI)](https://github.com/vuejs/eslint-plugin-vue/actions/workflows/CI.yml) +[![License](https://img.shields.io/github/license/vuejs/eslint-plugin-vue.svg?style=flat)](https://github.com/vuejs/eslint-plugin-vue/blob/master/LICENSE) > Official ESLint plugin for Vue.js ## :book: Documentation -See [the official website](https://eslint.vuejs.org). +Please refer to the [official website](https://eslint.vuejs.org). ## :anchor: Versioning Policy -This plugin is following [Semantic Versioning](https://semver.org/) and [ESLint's Semantic Versioning Policy](https://github.com/eslint/eslint#semantic-versioning-policy). +This plugin follows [Semantic Versioning]. +However, please note that we do not follow [ESLint's Semantic Versioning Policy]. +In minor version releases, this plugin may change the sharable configs provided by the plugin or the default behavior of the plugin's rules in order to add features to the plugin. Because we want to add many features to the plugin soon, so that users can easily take advantage of new features in Vue and Nuxt. -## :newspaper: Changelog +According to our policy, any minor update may report more linting errors than the previous release. As such, we recommend using the [tilde (`~`)](https://semver.npmjs.com/#syntax-examples) in `package.json` to guarantee the results of your builds. -This project uses [GitHub Releases](https://github.com/vuejs/eslint-plugin-vue/releases). +[Semantic Versioning]: https://semver.org/ +[ESLint's Semantic Versioning Policy]: https://github.com/eslint/eslint#semantic-versioning-policy -## :beers: Contribution Guide +## :newspaper: Releases -Contribution is welcome! +This project uses [GitHub Releases](https://github.com/vuejs/eslint-plugin-vue/releases). -See [The ESLint Vue Plugin Developer Guide](https://eslint.vuejs.org/developer-guide/). +## :beers: Contribution Guide -### Working with Rules +Contributing is welcome! See the [ESLint Vue Plugin Developer Guide](https://eslint.vuejs.org/developer-guide). -Before you start writing a new rule, please read [the official ESLint guide](https://eslint.org/docs/developer-guide/working-with-rules). +### Working With Rules -Next, in order to get an idea how does the AST of the code that you want to check looks like, use the [astexplorer.net]. -The [astexplorer.net] is a great tool to inspect ASTs, also Vue templates are supported. +Be sure to read the [official ESLint guide](https://eslint.org/docs/developer-guide/working-with-rules) before you start writing a new rule. -After opening [astexplorer.net], select `Vue` as the syntax and `vue-eslint-parser` as the parser. +To see what an abstract syntax tree (AST) of your code looks like, you may use [AST Explorer](https://astexplorer.net). After opening [AST Explorer](https://astexplorer.net), select `Vue` as the syntax and `vue-eslint-parser` as the parser. -[astexplorer.net]: https://astexplorer.net/ +The default JavaScript parser must be replaced because [Vue.js single file components](https://vuejs.org/guide/scaling-up/sfc.html) are not plain JavaScript, but a custom file format. [`vue-eslint-parser`](https://github.com/vuejs/vue-eslint-parser) is a replacement parser that generates an enhanced AST with nodes that represent specific parts of the template syntax, as well as the contents of the ` + + + + diff --git a/docs/.vitepress/theme/components/eslint-code-block.vue b/docs/.vitepress/theme/components/eslint-code-block.vue new file mode 100644 index 000000000..77d4fd7cf --- /dev/null +++ b/docs/.vitepress/theme/components/eslint-code-block.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/docs/.vitepress/theme/components/rules-table.vue b/docs/.vitepress/theme/components/rules-table.vue new file mode 100644 index 000000000..c517d4591 --- /dev/null +++ b/docs/.vitepress/theme/components/rules-table.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 000000000..757e63cab --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,32 @@ +// @ts-expect-error -- Browser +if (typeof window !== 'undefined') { + if (typeof require === 'undefined') { + // @ts-expect-error -- Browser + ;(window as any).require = () => { + const e = new Error('require is not defined') + ;(e as any).code = 'MODULE_NOT_FOUND' + throw e + } + } +} +// @ts-expect-error -- Cannot change `module` option +import type { Theme } from 'vitepress' +// @ts-expect-error -- Cannot change `module` option +import DefaultTheme from 'vitepress/theme' +// @ts-expect-error -- ignore +import Layout from './Layout.vue' +// @ts-expect-error -- ignore +import ESLintCodeBlock from './components/eslint-code-block.vue' +// @ts-expect-error -- ignore +import RulesTable from './components/rules-table.vue' + +const theme: Theme = { + ...DefaultTheme, + Layout, + enhanceApp(ctx) { + DefaultTheme.enhanceApp(ctx) + ctx.app.component('eslint-code-block', ESLintCodeBlock) + ctx.app.component('rules-table', RulesTable) + } +} +export default theme diff --git a/docs/.vitepress/vite-plugin.mts b/docs/.vitepress/vite-plugin.mts new file mode 100644 index 000000000..cd6811cb6 --- /dev/null +++ b/docs/.vitepress/vite-plugin.mts @@ -0,0 +1,83 @@ +import type { UserConfig } from 'vitepress' +import path from 'pathe' +import { fileURLToPath } from 'url' +import esbuild from 'esbuild' +type Plugin = Extract< + NonNullable['plugins']>[number], + { name: string } +> + +const libRoot = path.join(fileURLToPath(import.meta.url), '../../../lib') +export function vitePluginRequireResolve(): Plugin { + return { + name: 'vite-plugin-require.resolve', + transform(code, id, _options) { + if (id.startsWith(libRoot)) { + return code.replace(/require\.resolve/gu, '(function(){return 0})') + } + return undefined + } + } +} + +export function viteCommonjs(): Plugin { + return { + name: 'vite-plugin-cjs-to-esm', + apply: () => true, + async transform(code, id) { + if (!id.startsWith(libRoot)) { + return undefined + } + const base = transformRequire(code) + try { + const transformed = esbuild.transformSync(base, { + format: 'esm' + }) + return transformed.code + } catch (e) { + console.error('Transform error. base code:\n' + base, e) + } + return undefined + } + } +} + +/** + * Transform `require()` to `import` + */ +function transformRequire(code: string) { + if (!code.includes('require')) { + return code + } + const modules = new Map() + const replaced = code.replace( + /(\/\/[^\n\r]*|\/\*[\s\S]*?\*\/)|\brequire\s*\(\s*(["'].*?["'])\s*\)/gu, + (match, comment, moduleString) => { + if (comment) { + return match + } + + let id = + '__' + + moduleString.replace(/[^a-zA-Z0-9_$]+/gu, '_') + + Math.random().toString(32).substring(2) + while (code.includes(id) || modules.has(id)) { + id += Math.random().toString(32).substring(2) + } + modules.set(id, moduleString) + return id + '()' + } + ) + + return ( + [...modules] + .map(([id, moduleString]) => { + return `import * as __temp_${id} from ${moduleString}; +const ${id} = () => __temp_${id}.default || __temp_${id}; +` + }) + .join('') + + ';\n' + + replaced + ) +} diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue deleted file mode 100644 index 00b694825..000000000 --- a/docs/.vuepress/components/eslint-code-block.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js deleted file mode 100644 index fb7719ff3..000000000 --- a/docs/.vuepress/config.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * @author Toru Nagashima - * See LICENSE file in root directory for full license. - */ -'use strict' - -const rules = require('../../tools/lib/rules') -const path = require('path') - -const uncategorizedRules = rules.filter( - (rule) => - !rule.meta.docs.categories && - !rule.meta.docs.extensionRule && - !rule.meta.deprecated -) -const uncategorizedExtensionRule = rules.filter( - (rule) => - !rule.meta.docs.categories && - rule.meta.docs.extensionRule && - !rule.meta.deprecated -) -const deprecatedRules = rules.filter((rule) => rule.meta.deprecated) - -const sidebarCategories = [ - { title: 'Base Rules', categoryIds: ['base'] }, - { - title: 'Priority A: Essential', - categoryIds: ['vue3-essential', 'essential'] - }, - { - title: 'Priority A: Essential for Vue.js 3.x', - categoryIds: ['vue3-essential'] - }, - { title: 'Priority A: Essential for Vue.js 2.x', categoryIds: ['essential'] }, - { - title: 'Priority B: Strongly Recommended', - categoryIds: ['vue3-strongly-recommended', 'strongly-recommended'] - }, - { - title: 'Priority B: Strongly Recommended for Vue.js 3.x', - categoryIds: ['vue3-strongly-recommended'] - }, - { - title: 'Priority B: Strongly Recommended for Vue.js 2.x', - categoryIds: ['strongly-recommended'] - }, - { - title: 'Priority C: Recommended', - categoryIds: ['vue3-recommended', 'recommended'] - }, - { - title: 'Priority C: Recommended for Vue.js 3.x', - categoryIds: ['vue3-recommended'] - }, - { - title: 'Priority C: Recommended for Vue.js 2.x', - categoryIds: ['recommended'] - } -] - -const categorizedRules = [] -for (const { title, categoryIds } of sidebarCategories) { - const categoryRules = rules - .filter((rule) => rule.meta.docs.categories && !rule.meta.deprecated) - .filter((rule) => - categoryIds.every((categoryId) => - rule.meta.docs.categories.includes(categoryId) - ) - ) - const children = categoryRules - .filter(({ ruleId }) => { - const exists = categorizedRules.some(({ children }) => - children.some(([, alreadyRuleId]) => alreadyRuleId === ruleId) - ) - return !exists - }) - .map(({ ruleId, name }) => [`/rules/${name}`, ruleId]) - - if (children.length === 0) { - continue - } - categorizedRules.push({ - title, - collapsable: false, - children - }) -} - -const extraCategories = [] -if (uncategorizedRules.length > 0) { - extraCategories.push({ - title: 'Uncategorized', - collapsable: false, - children: uncategorizedRules.map(({ ruleId, name }) => [ - `/rules/${name}`, - ruleId - ]) - }) -} -if (uncategorizedExtensionRule.length > 0) { - extraCategories.push({ - title: 'Extension Rules', - collapsable: false, - children: uncategorizedExtensionRule.map(({ ruleId, name }) => [ - `/rules/${name}`, - ruleId - ]) - }) -} -if (deprecatedRules.length > 0) { - extraCategories.push({ - title: 'Deprecated', - collapsable: false, - children: deprecatedRules.map(({ ruleId, name }) => [ - `/rules/${name}`, - ruleId - ]) - }) -} - -module.exports = { - configureWebpack(_config, _isServer) { - return { - resolve: { - alias: { - module: require.resolve('./shim/module'), - eslint: path.resolve(__dirname, './shim/eslint') - } - } - } - }, - - base: '/', - title: 'eslint-plugin-vue', - description: 'Official ESLint plugin for Vue.js', - evergreen: true, - head: [['link', { rel: 'icon', href: '/favicon.png' }]], - - plugins: { - '@vuepress/pwa': { - serviceWorker: true, - updatePopup: true - } - }, - - themeConfig: { - repo: 'vuejs/eslint-plugin-vue', - docsRepo: 'vuejs/eslint-plugin-vue', - docsDir: 'docs', - docsBranch: 'master', - editLinks: true, - lastUpdated: true, - - nav: [ - { text: 'User Guide', link: '/user-guide/' }, - { text: 'Developer Guide', link: '/developer-guide/' }, - { text: 'Rules', link: '/rules/' }, - { text: 'Demo', link: 'https://mysticatea.github.io/vue-eslint-demo' } - ], - - sidebar: { - '/rules/': [ - '/rules/', - - // Rules in each category. - ...categorizedRules, - - // Rules in no category. - ...extraCategories - ], - - '/': ['/', '/user-guide/', '/developer-guide/', '/rules/'] - }, - - algolia: { - apiKey: 'b2b69365da747a9a9635cda391317c36', - indexName: 'eslint-plugin-vue' - } - } -} diff --git a/docs/.vuepress/shim/eslint/index.js b/docs/.vuepress/shim/eslint/index.js deleted file mode 100644 index e32cb598f..000000000 --- a/docs/.vuepress/shim/eslint/index.js +++ /dev/null @@ -1,2 +0,0 @@ -const Linter = require('eslint4b') -module.exports = { Linter } diff --git a/docs/.vuepress/shim/module.js b/docs/.vuepress/shim/module.js deleted file mode 100644 index 66ab9785e..000000000 --- a/docs/.vuepress/shim/module.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - createRequire: () => () => null -} diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl deleted file mode 100644 index 48680bbf3..000000000 --- a/docs/.vuepress/styles/index.styl +++ /dev/null @@ -1,20 +0,0 @@ -.theme-container.rule-details .theme-default-content > h1 { - font-size: 1.8rem; - - + blockquote { - margin-top: -15px; - padding: 0; - border: 0; - font-weight: 500; - font-size: 1.4rem; - color: currentColor; - - ::first-letter { - text-transform: uppercase; - } - - p { - line-height: 1.2; - } - } -} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e19258040..000000000 --- a/docs/README.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# Introduction - -Official ESLint plugin for Vue.js. - -This plugin allows us to check the ` `, + options: [{ default: 'v-slot' }], errors: [ { messageId: 'expectedVSlot', data: { actual: 'v-slot:default', argument: 'default' } } - ], - options: [{ default: 'v-slot' }] + ] }, { @@ -465,13 +456,13 @@ tester.run('v-slot-style', rule, { `, + options: [{ named: 'longform' }], errors: [ { messageId: 'expectedLongform', data: { actual: '#foo', argument: 'foo' } } - ], - options: [{ named: 'longform' }] + ] }, { @@ -511,13 +502,13 @@ tester.run('v-slot-style', rule, { `, + options: [{ named: 'longform' }], errors: [ { messageId: 'expectedLongform', data: { actual: '#[foo]', argument: '[foo]' } } - ], - options: [{ named: 'longform' }] + ] } ] }) diff --git a/tests/lib/rules/valid-attribute-name.js b/tests/lib/rules/valid-attribute-name.js new file mode 100644 index 000000000..bb94ca427 --- /dev/null +++ b/tests/lib/rules/valid-attribute-name.js @@ -0,0 +1,146 @@ +/** + * @author Doug Wade + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/valid-attribute-name') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('valid-attribute-name', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: ` ... ` + }, + { + filename: 'test.vue', + code: `
...
` + }, + { + filename: 'test.vue', + code: ` ... ` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: 'Attribute name 0abc is not valid.', + line: 1, + column: 14 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: 'Attribute name -def is not valid.', + line: 1, + column: 14 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: 'Attribute name !ghi is not valid.', + line: 1, + column: 14 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: 'Attribute name 0abc is not valid.', + line: 1, + column: 14 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: 'Attribute name 0abc is not valid.', + line: 1, + column: 14 + } + ] + } + ] +}) diff --git a/tests/lib/rules/valid-define-emits.js b/tests/lib/rules/valid-define-emits.js new file mode 100644 index 000000000..57a130299 --- /dev/null +++ b/tests/lib/rules/valid-define-emits.js @@ -0,0 +1,225 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/valid-define-emits') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2015, + sourceType: 'module' + } +}) + +tester.run('valid-define-emits', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + } + }, + { + filename: 'test.vue', + code: ` + + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + // https://github.com/vuejs/eslint-plugin-vue/issues/1656 + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineEmits` is referencing locally declared variables.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + message: '`defineEmits` has both a type-only emit and an argument.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineEmits` has been called multiple times.', + line: 4 + }, + { + message: '`defineEmits` has been called multiple times.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: + 'Custom events are defined in both `defineEmits` and `export default {}`.', + line: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Custom events are not defined.', + line: 4 + } + ] + } + ] +}) diff --git a/tests/lib/rules/valid-define-options.js b/tests/lib/rules/valid-define-options.js new file mode 100644 index 000000000..867bcb569 --- /dev/null +++ b/tests/lib/rules/valid-define-options.js @@ -0,0 +1,210 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/valid-define-options') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2015, + sourceType: 'module' + } +}) + +tester.run('valid-define-options', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineOptions` is referencing locally declared variables.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineOptions` has been called multiple times.', + line: 3 + }, + { + message: '`defineOptions` has been called multiple times.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Options are not defined.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + message: 'Options are not defined.', + line: 3 + }, + { + message: '`defineOptions()` cannot accept type arguments.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + '`defineOptions()` cannot be used to declare `props`. Use `defineProps()` instead.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + '`defineOptions()` cannot be used to declare `emits`. Use `defineEmits()` instead.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + '`defineOptions()` cannot be used to declare `expose`. Use `defineExpose()` instead.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + '`defineOptions()` cannot be used to declare `slots`. Use `defineSlots()` instead.', + line: 3 + } + ] + } + ] +}) diff --git a/tests/lib/rules/valid-define-props.js b/tests/lib/rules/valid-define-props.js new file mode 100644 index 000000000..d20209846 --- /dev/null +++ b/tests/lib/rules/valid-define-props.js @@ -0,0 +1,228 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/valid-define-props') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2015, + sourceType: 'module' + } +}) + +tester.run('valid-define-props', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + } + }, + { + filename: 'test.vue', + code: ` + + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + // https://github.com/vuejs/eslint-plugin-vue/issues/1656 + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineProps` is referencing locally declared variables.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + message: '`defineProps` has both a type-only props and an argument.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineProps` has been called multiple times.', + line: 4 + }, + { + message: '`defineProps` has been called multiple times.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: + 'Props are defined in both `defineProps` and `export default {}`.', + line: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Props are not defined.', + line: 4 + } + ] + } + ] +}) diff --git a/tests/lib/rules/valid-model-definition.js b/tests/lib/rules/valid-model-definition.js new file mode 100644 index 000000000..ed6013247 --- /dev/null +++ b/tests/lib/rules/valid-model-definition.js @@ -0,0 +1,143 @@ +/** + * @fileoverview Prevents invalid keys in model option. + * @author Alex Sokolov + */ +'use strict' + +const rule = require('../../../lib/rules/valid-model-definition') +const RuleTester = require('../../eslint-compat').RuleTester + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2018, + sourceType: 'module' + } +}) +ruleTester.run('valid-model-definition', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + export default { + model: { + prop: 'list' + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + event: 'update' + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + prop: 'list', + event: 'update' + } + } + ` + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + model: { + props: 'list' + } + } + `, + errors: ["Invalid key 'props' in model option."] + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + events: 'update' + } + } + `, + errors: ["Invalid key 'events' in model option."] + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + props: 'list', + event: 'update' + } + } + `, + errors: ["Invalid key 'props' in model option."] + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + prop: 'list', + events: 'update' + } + } + `, + errors: ["Invalid key 'events' in model option."] + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + props: 'list', + events: 'update' + } + } + `, + errors: [ + "Invalid key 'props' in model option.", + "Invalid key 'events' in model option." + ] + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + prop: 'checked', + props: 'list', + event: 'update' + } + } + `, + errors: ["Invalid key 'props' in model option."] + }, + { + filename: 'test.vue', + code: ` + export default { + model: { + name: 'checked', + props: 'list', + event: 'update' + } + } + `, + errors: [ + "Invalid key 'name' in model option.", + "Invalid key 'props' in model option." + ] + } + ] +}) diff --git a/tests/lib/rules/valid-next-tick.js b/tests/lib/rules/valid-next-tick.js index 220e43417..2e776e330 100644 --- a/tests/lib/rules/valid-next-tick.js +++ b/tests/lib/rules/valid-next-tick.js @@ -6,20 +6,12 @@ */ 'use strict' -// ------------------------------------------------------------------------------ -// Requirements -// ------------------------------------------------------------------------------ - -const RuleTester = require('eslint').RuleTester +const RuleTester = require('../../eslint-compat').RuleTester const rule = require('../../../lib/rules/valid-next-tick') -// ------------------------------------------------------------------------------ -// Tests -// ------------------------------------------------------------------------------ - const tester = new RuleTester({ - parser: require.resolve('vue-eslint-parser'), - parserOptions: { + languageOptions: { + parser: require('vue-eslint-parser'), ecmaVersion: 2017, sourceType: 'module' } @@ -113,6 +105,35 @@ tester.run('valid-next-tick', rule, { return this.$nextTick(); } }` + }, + + { + filename: 'test.vue', + code: `` + }, + + // https://github.com/vuejs/eslint-plugin-vue/issues/1776 + { + filename: 'test.vue', + code: `` } ], invalid: [ @@ -135,6 +156,7 @@ tester.run('valid-next-tick', rule, { column: 11, suggestions: [ { + messageId: 'addAwait', output: ` + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + + // Resolve component name + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + + // TopLevel await + { + filename: 'test.vue', + code: ` + + + + `, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module' + } + }, + + // ref + { + filename: 'test.vue', + code: ` + + + + ` + }, + + //style vars + { + filename: 'test.vue', + code: ` + + + + ` + }, + // ns + { + filename: 'test.vue', + code: ` + + + + ` + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + + + + `, + errors: [ + { + message: "'Bar' is defined but never used.", + line: 5, + suggestions: semver.gte(ESLint.version, '9.17.0') + ? [ + { + desc: "Remove unused variable 'Bar'.", + output: ` + + + + ` + } + ] + : null + }, + { + message: "'baz' is assigned a value but never used.", + line: 18 + } + ] + }, + + // Resolve component name + { + filename: 'test.vue', + code: ` + + + + `, + errors: [ + { + message: "'camelCase' is defined but never used.", + line: 3, + suggestions: semver.gte(ESLint.version, '9.17.0') + ? [ + { + desc: "Remove unused variable 'camelCase'.", + output: ` + + + + ` + } + ] + : null + } + ] + }, + + // Scope tests + { + filename: 'test.vue', + code: ` + + + + `, + errors: [ + { + message: "'msg' is assigned a value but never used.", + line: 4, + suggestions: semver.gte(ESLint.version, '9.17.0') + ? [ + { + desc: "Remove unused variable 'msg'.", + output: ` + + + + ` + } + ] + : null + } + ] + }, + { + filename: 'test.vue', + code: ` + + + + `, + errors: [ + { + message: "'i' is assigned a value but never used.", + line: 3 + } + ] + }, + + // Not ` + + + `, + errors: [ + { + message: "'msg' is assigned a value but never used.", + line: 3 + } + ] + }, + + //style vars + { + filename: 'test.vue', + code: ` + + + + `, + errors: ["'color' is assigned a value but never used."] + } + ] + }) + + ruleTester.run('no-undef', ruleNoUndef, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'defineUnknown' is not defined.", + line: 3 + }, + { + message: "'defineUnknown' is not defined.", + line: 7 + } + ] + } + ] + }) +}) diff --git a/tests/lib/script-setup.js b/tests/lib/script-setup.js deleted file mode 100644 index ad7409dcb..000000000 --- a/tests/lib/script-setup.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @author Yosuke Ota - * See LICENSE file in root directory for full license. - */ -'use strict' - -const Linter = require('eslint').Linter -const parser = require('vue-eslint-parser') -const assert = require('assert') -const experimentalScriptSetupVars = require('../../lib/rules/experimental-script-setup-vars') - -const baseConfig = { - parser: 'vue-eslint-parser', - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module' - } -} - -describe('script-setup test cases', () => { - const linter = new Linter() - linter.defineParser('vue-eslint-parser', parser) - linter.defineRule( - 'vue/experimental-script-setup-vars', - experimentalScriptSetupVars - ) - - describe('temporary supports.', () => { - const config = Object.assign({}, baseConfig, { - globals: { console: false }, - rules: { - 'vue/experimental-script-setup-vars': 'error', - 'no-undef': 'error' - } - }) - - it('should not be marked.', () => { - const code = ` - ` - const messages = linter.verify(code, config, 'test.vue') - assert.deepStrictEqual(messages, []) - }) - }) -}) diff --git a/tests/lib/utils/comments.js b/tests/lib/utils/comments.js new file mode 100644 index 000000000..9e567f6b1 --- /dev/null +++ b/tests/lib/utils/comments.js @@ -0,0 +1,67 @@ +'use strict' + +const assert = require('assert') +const { + isBlockComment, + isJSDocComment +} = require('../../../lib/utils/comments.js') + +// //foo +const lineCommentNode = { + type: 'Line', + value: 'foo' +} + +// /*foo*/ +const blockCommentNodeWithoutAsterisks = { + type: 'Block', + value: 'foo' +} + +// //** foo */ +const blockCommentNodeWithOneAsterisk = { + type: 'Block', + value: '* foo' +} + +// /*** foo */ +const blockCommentNodeWithTwoAsterisks = { + type: 'Block', + value: '** foo' +} + +describe('isJSDocComment()', () => { + it('returns true for JSDoc comments', () => { + assert.equal(isJSDocComment(blockCommentNodeWithOneAsterisk), true) + }) + + it('returns false for block comments', () => { + assert.equal(isJSDocComment(blockCommentNodeWithoutAsterisks), false) + }) + + it('returns false for line comments', () => { + assert.equal(isJSDocComment(lineCommentNode), false) + }) + + it('returns false for block comments with two asterisks', () => { + assert.equal(isJSDocComment(blockCommentNodeWithTwoAsterisks), false) + }) +}) + +describe('isBlockComment()', () => { + it('returns false for JSDoc comments', () => { + assert.equal(isBlockComment(blockCommentNodeWithOneAsterisk), false) + }) + + it('returns true for block comments', () => { + assert.equal(isBlockComment(blockCommentNodeWithoutAsterisks), true) + }) + + it('returns false for line comments', () => { + assert.equal(isBlockComment(lineCommentNode), false) + }) + + it('returns true for block comments with two asterisks', () => { + assert.equal(isBlockComment(blockCommentNodeWithTwoAsterisks), true) + }) +}) diff --git a/tests/lib/utils/core-rules/wrap-core-rule.js b/tests/lib/utils/core-rules/wrap-core-rule.js new file mode 100644 index 000000000..b07b17e9e --- /dev/null +++ b/tests/lib/utils/core-rules/wrap-core-rule.js @@ -0,0 +1,37 @@ +'use strict' + +const RuleTester = require('../../../eslint-compat').RuleTester +const utils = require('../../../../lib/utils/index') + +const rule = utils.wrapCoreRule('foo') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('wrap-core-rule-with-unknown', rule, { + valid: [ + { + filename: 'test.js', + code: `var a` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Failed to extend ESLint core rule "foo". You may be able to use this rule by upgrading the version of ESLint. If you cannot upgrade it, turn off this rule.', + line: 1, + column: 1 + } + ] + } + ] +}) diff --git a/tests/lib/utils/html-comments.js b/tests/lib/utils/html-comments.js index 066c73eb1..69992b09d 100644 --- a/tests/lib/utils/html-comments.js +++ b/tests/lib/utils/html-comments.js @@ -4,7 +4,7 @@ const fs = require('fs') const path = require('path') const assert = require('assert') -const Linter = require('eslint').Linter +const Linter = require('../../eslint-compat').Linter const htmlComments = require('../../../lib/utils/html-comments') @@ -37,17 +37,25 @@ function tokenize(code, option) { const linter = new Linter() const result = [] - linter.defineRule('vue/html-comments-test', (content) => - htmlComments.defineVisitor(content, option, (commentTokens) => { - result.push(commentTokens) - }) - ) - linter.defineParser('vue-eslint-parser', require('vue-eslint-parser')) linter.verify( code, { - parser: 'vue-eslint-parser', - parserOptions: { ecmaVersion: 2018 }, + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2018 + }, + plugins: { + vue: { + rules: { + 'html-comments-test': { + create: (content) => + htmlComments.defineVisitor(content, option, (commentTokens) => { + result.push(commentTokens) + }) + } + } + } + }, rules: { 'vue/html-comments-test': 'error' } }, undefined, diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js index df45da856..6afecaeaa 100644 --- a/tests/lib/utils/index.js +++ b/tests/lib/utils/index.js @@ -1,15 +1,14 @@ 'use strict' -const babelEslint = require('babel-eslint') const espree = require('espree') const utils = require('../../../lib/utils/index') const assert = require('assert') -describe('getComputedProperties', () => { - const parse = function (code) { - return babelEslint.parse(code).body[0].declarations[0].init - } +function parse(code) { + return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0].init +} +describe('getComputedProperties', () => { it('should return empty array when there is no computed property', () => { const node = parse(`const test = { name: 'test', @@ -111,55 +110,42 @@ describe('getComputedProperties', () => { }) describe('getStaticPropertyName', () => { - const parse = function (code) { - return babelEslint.parse(code).body[0].declarations[0].init - } - it('should parse property expression with identifier', () => { const node = parse(`const test = { computed: { } }`) const parsed = utils.getStaticPropertyName(node.properties[0]) - assert.ok(parsed === 'computed') + assert.strictEqual(parsed, 'computed') }) it('should parse property expression with literal', () => { const node = parse(`const test = { ['computed'] () {} }`) const parsed = utils.getStaticPropertyName(node.properties[0]) - assert.ok(parsed === 'computed') + assert.strictEqual(parsed, 'computed') }) it('should parse property expression with template literal', () => { const node = parse(`const test = { [\`computed\`] () {} }`) const parsed = utils.getStaticPropertyName(node.properties[0]) - assert.ok(parsed === 'computed') + assert.strictEqual(parsed, 'computed') }) }) describe('getStringLiteralValue', () => { - const parse = function (code) { - return babelEslint.parse(code).body[0].declarations[0].init - } - it('should parse literal', () => { const node = parse(`const test = { ['computed'] () {} }`) const parsed = utils.getStringLiteralValue(node.properties[0].key) - assert.ok(parsed === 'computed') + assert.strictEqual(parsed, 'computed') }) it('should parse template literal', () => { const node = parse(`const test = { [\`computed\`] () {} }`) const parsed = utils.getStringLiteralValue(node.properties[0].key) - assert.ok(parsed === 'computed') + assert.strictEqual(parsed, 'computed') }) }) describe('getMemberChaining', () => { - const parse = function (code) { - return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0] - .init - } - const jsonIgnoreKeys = ['expression', 'object'] it('should parse MemberExpression', () => { @@ -274,10 +260,6 @@ describe('getMemberChaining', () => { }) describe('getRegisteredComponents', () => { - const parse = function (code) { - return babelEslint.parse(code).body[0].declarations[0].init - } - it('should return empty array when there are no components registered', () => { const node = parse(`const test = { name: 'test', @@ -333,14 +315,13 @@ describe('getRegisteredComponents', () => { }) }) -describe('getComponentProps', () => { - const parse = function (code) { - const data = babelEslint.parse(code).body[0].declarations[0].init - return utils.getComponentProps(data) - } +function parseProps(code) { + return utils.getComponentPropsFromOptions(parse(code)) +} +describe('getComponentProps', () => { it('should return empty array when there is no component props', () => { - const props = parse(`const test = { + const props = parseProps(`const test = { name: 'test', data() { return {} @@ -351,7 +332,7 @@ describe('getComponentProps', () => { }) it('should return empty array when component props is empty array', () => { - const props = parse(`const test = { + const props = parseProps(`const test = { name: 'test', props: [] }`) @@ -360,7 +341,7 @@ describe('getComponentProps', () => { }) it('should return empty array when component props is empty object', () => { - const props = parse(`const test = { + const props = parseProps(`const test = { name: 'test', props: {} }`) @@ -369,7 +350,7 @@ describe('getComponentProps', () => { }) it('should return computed props', () => { - const props = parse(`const test = { + const props = parseProps(`const test = { name: 'test', ...test, data() { @@ -384,27 +365,31 @@ describe('getComponentProps', () => { } }`) - assert.equal(props.length, 4, 'it detects all props') + assert.equal(props.length, 5, 'it detects all props') + + assert.strictEqual(props[0].key, undefined) + assert.strictEqual(props[0].node.type, 'SpreadElement') + assert.strictEqual(props[0].value, undefined) - assert.ok(props[0].key.type === 'Identifier') - assert.ok(props[0].node.type === 'Property') - assert.ok(props[0].value.type === 'Identifier') + assert.strictEqual(props[1].key.type, 'Identifier') + assert.strictEqual(props[1].node.type, 'Property') + assert.strictEqual(props[1].value.type, 'Identifier') - assert.ok(props[1].key.type === 'Identifier') - assert.ok(props[1].node.type === 'Property') - assert.ok(props[1].value.type === 'ObjectExpression') + assert.strictEqual(props[2].key.type, 'Identifier') + assert.strictEqual(props[2].node.type, 'Property') + assert.strictEqual(props[2].value.type, 'ObjectExpression') - assert.ok(props[2].key.type === 'Identifier') - assert.ok(props[2].node.type === 'Property') - assert.ok(props[2].value.type === 'ArrayExpression') + assert.strictEqual(props[3].key.type, 'Identifier') + assert.strictEqual(props[3].node.type, 'Property') + assert.strictEqual(props[3].value.type, 'ArrayExpression') - assert.deepEqual(props[3].key, props[3].value) - assert.ok(props[3].node.type === 'Property') - assert.ok(props[3].value.type === 'Identifier') + assert.deepEqual(props[4].key, props[4].value) + assert.strictEqual(props[4].node.type, 'Property') + assert.strictEqual(props[4].value.type, 'Identifier') }) it('should return computed from array props', () => { - const props = parse(`const test = { + const props = parseProps(`const test = { name: 'test', data() { return {} @@ -414,21 +399,21 @@ describe('getComponentProps', () => { assert.equal(props.length, 4, 'it detects all props') - assert.ok(props[0].node.type === 'Literal') + assert.strictEqual(props[0].node.type, 'Literal') assert.deepEqual(props[0].key, props[0].node) - assert.ok(!props[0].value) + assert.strictEqual(props[0].value, null) - assert.ok(props[1].node.type === 'Identifier') - assert.ok(!props[1].key) - assert.ok(!props[1].value) + assert.strictEqual(props[1].node.type, 'Identifier') + assert.strictEqual(props[1].key, null) + assert.strictEqual(props[1].value, null) - assert.ok(props[2].node.type === 'TemplateLiteral') + assert.strictEqual(props[2].node.type, 'TemplateLiteral') assert.deepEqual(props[2].key, props[2].node) - assert.ok(!props[2].value) + assert.strictEqual(props[2].value, null) - assert.ok(props[3].node.type === 'Literal') - assert.ok(!props[3].key) - assert.ok(!props[3].value) + assert.strictEqual(props[3].node.type, 'Literal') + assert.strictEqual(props[3].key, null) + assert.strictEqual(props[3].value, null) }) }) diff --git a/tests/lib/utils/ref-object-references.js b/tests/lib/utils/ref-object-references.js new file mode 100644 index 000000000..68c526b36 --- /dev/null +++ b/tests/lib/utils/ref-object-references.js @@ -0,0 +1,202 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const assert = require('assert') +const vueESLintParser = require('vue-eslint-parser') + +const Linter = require('../../eslint-compat').Linter + +const { + extractRefObjectReferences, + extractReactiveVariableReferences +} = require('../../../lib/utils/ref-object-references') + +const FIXTURE_ROOT = path.resolve( + __dirname, + '../../fixtures/utils/ref-object-references' +) +const REF_OBJECTS_FIXTURE_ROOT = path.resolve(FIXTURE_ROOT, 'ref-objects') +const REACTIVE_VARS_FIXTURE_ROOT = path.resolve(FIXTURE_ROOT, 'reactive-vars') + +/** + * @typedef {object} LoadedPattern + * @property {string} code The code to test. + * @property {string} name The name of the pattern. + * @property {string} sourceFilePath + * @property {string} resultFilePath + * @property {object} [options] + * @property {string} [options.parser] + */ +/** + * Load test patterns from fixtures. + * + * @returns {LoadedPattern[]} The loaded patterns. + */ +function loadPatterns(rootDir) { + return fs.readdirSync(rootDir).map((name) => { + for (const [sourceFile, resultFile, options] of [ + ['source.js', 'result.js'], + [ + 'source.vue', + 'result.vue', + { languageOptions: { parser: 'vue-eslint-parser' } } + ] + ]) { + const sourceFilePath = path.join(rootDir, name, sourceFile) + if (fs.existsSync(sourceFilePath)) { + return { + code: fs.readFileSync(sourceFilePath, 'utf8'), + name, + sourceFilePath, + resultFilePath: path.join(rootDir, name, resultFile), + options + } + } + } + }) +} + +function extractRefs(code, extract, options) { + const linter = new Linter() + const references = [] + + const messages = linter.verify( + code, + { + ...options, + plugins: { + vue: { + rules: { + 'extract-test': { + create: (context) => { + const refs = extract(context) + + const processed = new Set() + return { + '*'(node) { + if (processed.has(node)) { + // Old ESLint may be called twice on the same node. + return + } + processed.add(node) + const data = refs.get(node) + if (data) { + references.push(data) + } + } + } + } + } + } + } + }, + languageOptions: { + ...options?.languageOptions, + ...(options?.languageOptions?.parser === 'vue-eslint-parser' + ? { parser: vueESLintParser } + : {}), + ecmaVersion: 2020, + sourceType: 'module', + globals: { + $ref: 'readonly', + $computed: 'readonly', + $shallowRef: 'readonly', + $customRef: 'readonly', + $toRef: 'readonly', + $: 'readonly', + $$: 'readonly' + } + }, + rules: { 'vue/extract-test': 'error' } + }, + undefined, + true + ) + + const errors = messages.map((message) => message.message) + if (errors.length > 0) { + assert.fail(errors.join(',')) + } + + return references +} + +describe('extractRefObjectReferences()', () => { + for (const { code, sourceFilePath, resultFilePath, options } of loadPatterns( + REF_OBJECTS_FIXTURE_ROOT + )) { + describe(sourceFilePath, () => { + it('should to extract the references to match the expected references.', () => { + /** @type {import('../../../lib/utils/ref-object-references').RefObjectReference[]} */ + const references = [ + ...extractRefs(code, extractRefObjectReferences, options) + ] + + let result = '' + let start = 0 + let ref + while ((ref = references.shift())) { + result += code.slice(start, ref.node.range[0]) + result += `/*>*/` + result += code.slice(...ref.node.range) + result += `/*<${JSON.stringify({ + type: ref.type, + method: ref.method + })}*/` + start = ref.node.range[1] + } + result += code.slice(start) + + const actual = result + + if (!fs.existsSync(resultFilePath)) { + // update fixture + fs.writeFileSync(resultFilePath, actual, 'utf8') + } + + const expected = fs.readFileSync(resultFilePath, 'utf8') + assert.strictEqual(actual, expected) + }) + }) + } +}) +describe('extractReactiveVariableReferences()', () => { + for (const { code, sourceFilePath, resultFilePath, options } of loadPatterns( + REACTIVE_VARS_FIXTURE_ROOT + )) { + describe(sourceFilePath, () => { + it('should to extract the references to match the expected references.', () => { + /** @type {import('../../../lib/utils/ref-object-references').ReactiveVariableReference[]} */ + const references = [ + ...extractRefs(code, extractReactiveVariableReferences, options) + ] + + let result = '' + let start = 0 + let ref + while ((ref = references.shift())) { + result += code.slice(start, ref.node.range[0]) + result += `/*>*/` + result += code.slice(...ref.node.range) + result += `/*<${JSON.stringify({ + escape: ref.escape, + method: ref.method + })}*/` + start = ref.node.range[1] + } + result += code.slice(start) + + const actual = result + + if (!fs.existsSync(resultFilePath)) { + // update fixture + fs.writeFileSync(resultFilePath, actual, 'utf8') + } + + const expected = fs.readFileSync(resultFilePath, 'utf8') + assert.strictEqual(actual, expected) + }) + }) + } +}) diff --git a/tests/lib/utils/selector.js b/tests/lib/utils/selector.js new file mode 100644 index 000000000..b5b0ee986 --- /dev/null +++ b/tests/lib/utils/selector.js @@ -0,0 +1,99 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const assert = require('assert') + +const Linter = require('../../eslint-compat').Linter + +const selector = require('../../../lib/utils/selector') +const utils = require('../../../lib/utils') + +const FIXTURE_ROOT = path.resolve(__dirname, '../../fixtures/utils/selector') + +/** + * Load test patterns from fixtures. + * + * @returns {object} The loaded patterns. + */ +function loadPatterns() { + return fs.readdirSync(FIXTURE_ROOT).map((name) => { + const code0 = fs.readFileSync( + path.join(FIXTURE_ROOT, name, 'source.vue'), + 'utf8' + ) + const code = code0.replace(/^/, ``) + const inputSelector = /^/.exec(code0)[1].trim() + return { code, name, inputSelector } + }) +} + +function extractElements(code, inputSelector) { + const linter = new Linter() + const matches = [] + + const messages = linter.verify( + code, + { + plugins: { + vue: { + rules: { + 'selector-test': { + create: (context) => { + const parsed = selector.parseSelector(inputSelector, context) + return utils.defineDocumentVisitor(context, { + VElement(node) { + if (parsed.test(node)) { + matches.push( + context + .getSourceCode() + .text.slice(...node.startTag.range) + ) + } + } + }) + } + } + } + } + }, + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2018 + }, + rules: { 'vue/selector-test': 'error' } + }, + undefined, + true + ) + + return { + selector: inputSelector, + matches, + errors: messages.map((message) => message.message) + } +} + +describe('parseSelector()', () => { + for (const { name, code, inputSelector } of loadPatterns()) { + describe(`'test/fixtures/utils/selector/${name}/source.vue'`, () => { + it('should to parse the selector to match the valid elements.', () => { + const elements = extractElements(code, inputSelector) + const actual = JSON.stringify(elements, null, 4) + + // update fixture + // fs.writeFileSync( + // path.join(FIXTURE_ROOT, name, 'result.json'), + // actual, + // 'utf8' + // ) + + const expected = fs.readFileSync( + path.join(FIXTURE_ROOT, name, 'result.json'), + 'utf8' + ) + assert.strictEqual(actual, expected) + }) + }) + } +}) diff --git a/tests/lib/utils/ts-utils/index/get-component-emits.js b/tests/lib/utils/ts-utils/index/get-component-emits.js new file mode 100644 index 000000000..8c9d5d34c --- /dev/null +++ b/tests/lib/utils/ts-utils/index/get-component-emits.js @@ -0,0 +1,127 @@ +/** + * Test for getComponentEmitsFromTypeDefineTypes + */ +'use strict' + +const path = require('path') +const fs = require('fs') +const Linter = require('../../../../eslint-compat').Linter +const parser = require('vue-eslint-parser') +const tsParser = require('@typescript-eslint/parser') +const utils = require('../../../../../lib/utils/index') +const assert = require('assert') + +const FIXTURES_ROOT = path.resolve( + __dirname, + '../../../../fixtures/utils/ts-utils' +) +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts') + +function extractComponentProps(code, tsFileCode) { + const linter = new Linter() + const result = [] + const config = { + files: ['**/*.vue'], + languageOptions: { + parser, + ecmaVersion: 2020, + parserOptions: { + parser: tsParser, + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + } + }, + plugins: { + test: { + rules: { + test: { + create(context) { + return utils.defineScriptSetupVisitor(context, { + onDefineEmitsEnter(_node, emits) { + result.push( + ...emits.map((emit) => ({ + type: emit.type, + name: emit.emitName + })) + ) + } + }) + } + } + } + } + }, + rules: { + 'test/test': 'error' + } + } + fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8') + // clean './src/test.ts' cache + tsParser.clearCaches() + assert.deepStrictEqual( + linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')), + [] + ) + // reset + fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8') + return result +} + +describe('getComponentEmitsFromTypeDefineTypes', () => { + for (const { scriptCode, tsFileCode, props: expected } of [ + { + scriptCode: `defineEmits<{(e:'foo'):void,(e:'bar'):void}>()`, + props: [ + { type: 'type', name: 'foo' }, + { type: 'type', name: 'bar' } + ] + }, + { + tsFileCode: `export type Emits = {(e:'foo'):void,(e:'bar'):void}`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [ + { type: 'infer-type', name: 'foo' }, + { type: 'infer-type', name: 'bar' } + ] + }, + { + tsFileCode: `export type Emits = any`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [{ type: 'unknown', name: null }] + }, + { + tsFileCode: `export type Emits = {(e:'foo' | 'bar'): void, (e:'baz',payload:number): void}`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [ + { type: 'infer-type', name: 'foo' }, + { type: 'infer-type', name: 'bar' }, + { type: 'infer-type', name: 'baz' } + ] + }, + { + tsFileCode: `export type Emits = { a: [], b: [number], c: [string]}`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [ + { type: 'infer-type', name: 'a' }, + { type: 'infer-type', name: 'b' }, + { type: 'infer-type', name: 'c' } + ] + } + ]) { + const code = `` + it(`should return expected props with :${code}`, () => { + const props = extractComponentProps(code, tsFileCode) + + assert.deepStrictEqual( + props, + expected, + `\n${JSON.stringify(props)}\n === \n${JSON.stringify(expected)}` + ) + }) + } +}) diff --git a/tests/lib/utils/ts-utils/index/get-component-props.js b/tests/lib/utils/ts-utils/index/get-component-props.js new file mode 100644 index 000000000..100d0a18c --- /dev/null +++ b/tests/lib/utils/ts-utils/index/get-component-props.js @@ -0,0 +1,218 @@ +/** + * Test for getComponentPropsFromTypeDefineTypes + */ +'use strict' + +const path = require('path') +const fs = require('fs') +const Linter = require('../../../../eslint-compat').Linter +const parser = require('vue-eslint-parser') +const tsParser = require('@typescript-eslint/parser') +const utils = require('../../../../../lib/utils/index') +const assert = require('assert') + +const FIXTURES_ROOT = path.resolve( + __dirname, + '../../../../fixtures/utils/ts-utils' +) +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts') + +function extractComponentProps(code, tsFileCode) { + const linter = new Linter() + const result = [] + const config = { + files: ['**/*.vue'], + languageOptions: { + parser, + ecmaVersion: 2020, + parserOptions: { + parser: tsParser, + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + } + }, + plugins: { + test: { + rules: { + test: { + create(context) { + return utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(_node, props) { + result.push( + ...props.map((prop) => ({ + type: prop.type, + name: prop.propName, + required: prop.required ?? null, + types: prop.types ?? null + })) + ) + } + }) + } + } + } + } + }, + rules: { + 'test/test': 'error' + } + } + fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8') + // clean './src/test.ts' cache + tsParser.clearCaches() + assert.deepStrictEqual( + linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')), + [] + ) + // reset + fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8') + return result +} + +describe('getComponentPropsFromTypeDefineTypes', () => { + for (const { scriptCode, tsFileCode, props: expected } of [ + { + scriptCode: `defineProps<{foo:string,bar?:number}>()`, + props: [ + { type: 'type', name: 'foo', required: true, types: ['String'] }, + { type: 'type', name: 'bar', required: false, types: ['Number'] } + ] + }, + { + scriptCode: `defineProps<{foo:string,bar?:number} & {baz?:string|number}>()`, + props: [ + { type: 'type', name: 'foo', required: true, types: ['String'] }, + { type: 'type', name: 'bar', required: false, types: ['Number'] }, + { + type: 'type', + name: 'baz', + required: false, + types: ['String', 'Number'] + } + ] + }, + { + tsFileCode: `export type Props = {foo:string,bar?:number}`, + scriptCode: `import { Props } from './test' + defineProps()`, + props: [ + { type: 'infer-type', name: 'foo', required: true, types: ['String'] }, + { type: 'infer-type', name: 'bar', required: false, types: ['Number'] } + ] + }, + { + tsFileCode: `export type Props = any`, + scriptCode: `import { Props } from './test' + defineProps()`, + props: [{ type: 'unknown', name: null, required: null, types: null }] + }, + { + tsFileCode: ` + interface Props { + a?: number; + b?: string; + } + export interface Props2 extends Required { + c?: boolean; + }`, + scriptCode: `import { Props2 } from './test' + defineProps()`, + props: [ + { type: 'infer-type', name: 'c', required: false, types: ['Boolean'] }, + { type: 'infer-type', name: 'a', required: true, types: ['Number'] }, + { type: 'infer-type', name: 'b', required: true, types: ['String'] } + ] + }, + { + tsFileCode: ` + export type Props = { + a: string + b?: number + c?: boolean + d?: boolean + e?: number | string + f?: () => number + g?: { foo?: string } + h?: string[] + i?: readonly string[] + }`, + scriptCode: `import { Props } from './test' + defineProps()`, + props: [ + { type: 'infer-type', name: 'a', required: true, types: ['String'] }, + { type: 'infer-type', name: 'b', required: false, types: ['Number'] }, + { type: 'infer-type', name: 'c', required: false, types: ['Boolean'] }, + { type: 'infer-type', name: 'd', required: false, types: ['Boolean'] }, + { + type: 'infer-type', + name: 'e', + required: false, + types: ['String', 'Number'] + }, + { type: 'infer-type', name: 'f', required: false, types: ['Function'] }, + { type: 'infer-type', name: 'g', required: false, types: ['Object'] }, + { type: 'infer-type', name: 'h', required: false, types: ['Array'] }, + { type: 'infer-type', name: 'i', required: false, types: ['Array'] } + ] + }, + { + tsFileCode: ` + export interface Props { + a?: number; + b?: string; + }`, + scriptCode: `import { Props } from './test' +defineProps()`, + props: [ + { type: 'infer-type', name: 'a', required: false, types: ['Number'] }, + { type: 'infer-type', name: 'b', required: false, types: ['String'] }, + { type: 'type', name: 'foo', required: false, types: ['String'] } + ] + }, + { + tsFileCode: ` + export type A = string | number`, + scriptCode: `import { A } from './test' +defineProps<{foo?:A}>()`, + props: [ + { + type: 'type', + name: 'foo', + required: false, + types: ['String', 'Number'] + } + ] + }, + { + scriptCode: `enum A {a = 'a', b = 'b'} +defineProps<{foo?:A}>()`, + props: [{ type: 'type', name: 'foo', required: false, types: ['String'] }] + }, + { + scriptCode: ` +const foo = 42 +enum A {a = foo, b = 'b'} +defineProps<{foo?:A}>()`, + props: [ + { + type: 'type', + name: 'foo', + required: false, + types: ['Number', 'String'] + } + ] + } + ]) { + const code = `` + it(`should return expected props with :${code}`, () => { + const props = extractComponentProps(code, tsFileCode) + + assert.deepStrictEqual( + props, + expected, + `\n${JSON.stringify(props)}\n === \n${JSON.stringify(expected)}` + ) + }) + } +}) diff --git a/tests/lib/utils/ts-utils/index/get-component-slots.js b/tests/lib/utils/ts-utils/index/get-component-slots.js new file mode 100644 index 000000000..410021b93 --- /dev/null +++ b/tests/lib/utils/ts-utils/index/get-component-slots.js @@ -0,0 +1,115 @@ +/** + * Test for getComponentSlotsFromTypeDefineTypes + */ +'use strict' + +const path = require('path') +const fs = require('fs') +const Linter = require('../../../../eslint-compat').Linter +const parser = require('vue-eslint-parser') +const tsParser = require('@typescript-eslint/parser') +const utils = require('../../../../../lib/utils/index') +const assert = require('assert') + +const FIXTURES_ROOT = path.resolve( + __dirname, + '../../../../fixtures/utils/ts-utils' +) +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts') + +function extractComponentSlots(code, tsFileCode) { + const linter = new Linter() + const result = [] + const config = { + files: ['**/*.vue'], + languageOptions: { + parser, + ecmaVersion: 2020, + parserOptions: { + parser: tsParser, + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + } + }, + plugins: { + test: { + rules: { + test: { + create(context) { + return utils.defineScriptSetupVisitor(context, { + onDefineSlotsEnter(_node, slots) { + result.push( + ...slots.map((prop) => ({ + type: prop.type, + name: prop.slotName + })) + ) + } + }) + } + } + } + } + }, + rules: { + 'test/test': 'error' + } + } + fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8') + // clean './src/test.ts' cache + tsParser.clearCaches() + assert.deepStrictEqual( + linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')), + [] + ) + // reset + fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8') + return result +} + +describe('getComponentSlotsFromTypeDefineTypes', () => { + for (const { scriptCode, tsFileCode, slots: expected } of [ + { + scriptCode: ` + defineSlots<{ + default(props: { msg: string }): any + }>() + `, + slots: [{ type: 'type', name: 'default' }] + }, + { + scriptCode: ` + interface Slots { + default(props: { msg: string }): any + } + defineSlots() + `, + slots: [{ type: 'type', name: 'default' }] + }, + { + scriptCode: ` + type Slots = { + default(props: { msg: string }): any + } + defineSlots() + `, + slots: [{ type: 'type', name: 'default' }] + } + ]) { + const code = ` + + ` + it(`should return expected slots with :${code}`, () => { + const slots = extractComponentSlots(code, tsFileCode) + + assert.deepStrictEqual( + slots, + expected, + `\n${JSON.stringify(slots)}\n === \n${JSON.stringify(expected)}` + ) + }) + } +}) diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js index 91da3d28c..12fcd904f 100644 --- a/tests/lib/utils/vue-component.js +++ b/tests/lib/utils/vue-component.js @@ -5,10 +5,6 @@ const utils = require('../../../lib/utils/index') -// ------------------------------------------------------------------------------ -// Requirements -// ------------------------------------------------------------------------------ - const rule = { create(context) { return utils.executeOnVueComponent(context, (obj) => { @@ -24,8 +20,8 @@ const rule = { } } -const RuleTester = require('eslint').RuleTester -const parserOptions = { +const RuleTester = require('../../eslint-compat').RuleTester +const languageOptions = { ecmaVersion: 6, sourceType: 'module' } @@ -43,77 +39,83 @@ function validTests(ext) { { filename: `test.${ext}`, code: `export const foo = {}`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `export var foo = {}`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `const foo = {}`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `var foo = {}`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `let foo = {}`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `foo({ })`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `foo(() => { return {} })`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `Vue.component('async-example', function (resolve, reject) { })`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `Vue.component('async-example', function (resolve, reject) { resolve({}) })`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `new Vue({ })`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `{ foo: {} }`, - parserOptions + languageOptions }, { filename: `test.${ext}`, code: `export default (Foo as FooConstructor).extend({})`, - parser: require.resolve('@typescript-eslint/parser'), - parserOptions + languageOptions: { + ...languageOptions, + parser: require('@typescript-eslint/parser') + } }, { filename: `test.${ext}`, code: `export default Foo.extend({})`, - parser: require.resolve('@typescript-eslint/parser'), - parserOptions + languageOptions: { + ...languageOptions, + parser: require('@typescript-eslint/parser') + } }, { filename: `test.${ext}`, code: `export default Foo.extend({} as ComponentOptions)`, - parser: require.resolve('@typescript-eslint/parser'), - parserOptions + languageOptions: { + ...languageOptions, + parser: require('@typescript-eslint/parser') + } } ] } @@ -129,64 +131,70 @@ function invalidTests(ext) { }) // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(4)] }, { filename: `test.${ext}`, code: `Vue.component({})`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `Vue.mixin({})`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `Vue.extend({})`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `app.component('name', {})`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `app.mixin({})`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `export default (Vue as VueConstructor).extend({})`, - parser: require.resolve('@typescript-eslint/parser'), - parserOptions, + languageOptions: { + ...languageOptions, + parser: require('@typescript-eslint/parser') + }, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `export default Vue.extend({})`, - parser: require.resolve('@typescript-eslint/parser'), - parserOptions, + languageOptions: { + ...languageOptions, + parser: require('@typescript-eslint/parser') + }, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `export default Vue.extend({} as ComponentOptions)`, - parser: require.resolve('@typescript-eslint/parser'), - parserOptions, + languageOptions: { + ...languageOptions, + parser: require('@typescript-eslint/parser') + }, errors: [makeError(1)] }, { filename: `test.${ext}`, code: `createApp({})`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { @@ -196,7 +204,7 @@ function invalidTests(ext) { export default { } // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(3)] }, { @@ -206,7 +214,7 @@ function invalidTests(ext) { export default { } // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(3)] }, { @@ -219,7 +227,7 @@ function invalidTests(ext) { export default { } // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(6)] }, { @@ -231,7 +239,7 @@ function invalidTests(ext) { export var a = { } // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(3), makeError(5)] }, { @@ -243,7 +251,7 @@ function invalidTests(ext) { export default { } // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(3), makeError(5)] }, { @@ -254,8 +262,8 @@ function invalidTests(ext) { export let foo = { } // ${ext} `, - parserOptions, - errors: (ext === 'js' ? [] : [makeError(2)]).concat([makeError(4)]) + languageOptions, + errors: [...(ext === 'js' ? [] : [makeError(2)]), makeError(4)] }, { filename: `test.${ext}`, @@ -265,7 +273,7 @@ function invalidTests(ext) { export let bar = { } // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(4)] }, { @@ -277,7 +285,7 @@ function invalidTests(ext) { bar({ }) // ${ext} `, - parserOptions, + languageOptions, errors: [makeError(4)] }, { @@ -292,8 +300,8 @@ function invalidTests(ext) { bar({ }) // ${ext} `, - parserOptions, - errors: (ext === 'js' ? [] : [makeError(3)]).concat([makeError(6)]) + languageOptions, + errors: [...(ext === 'js' ? [] : [makeError(3)]), makeError(6)] }, { filename: `test.${ext}`, @@ -309,49 +317,53 @@ function invalidTests(ext) { } // ${ext} `, - parserOptions, - errors: (ext === 'js' ? [] : [makeError(2)]).concat([makeError(8)]) + languageOptions, + errors: [...(ext === 'js' ? [] : [makeError(2)]), makeError(8)] }, { filename: `test.${ext}`, code: `export default defineComponent({})`, - parserOptions, + languageOptions, + errors: [makeError(1)] + }, + { + filename: `test.${ext}`, + code: `export default defineNuxtComponent({})`, + languageOptions, errors: [makeError(1)] } ] } -// ------------------------------------------------------------------------------ -// Tests -// ------------------------------------------------------------------------------ - const ruleTester = new RuleTester() ruleTester.run('vue-component', rule, { valid: [ { filename: 'test.js', code: `export default { }`, - parserOptions - } - ] - .concat(validTests('js')) - .concat(validTests('jsx')) - .concat(validTests('vue')), + languageOptions + }, + ...validTests('js'), + ...validTests('jsx'), + ...validTests('tsx'), + ...validTests('vue') + ], invalid: [ { filename: 'test.vue', code: `export default { }`, - parserOptions, + languageOptions, errors: [makeError(1)] }, { filename: 'test.jsx', code: `export default { }`, - parserOptions, + languageOptions, errors: [makeError(1)] - } + }, + ...invalidTests('js'), + ...invalidTests('jsx'), + ...invalidTests('tsx'), + ...invalidTests('vue') ] - .concat(invalidTests('js')) - .concat(invalidTests('jsx')) - .concat(invalidTests('vue')) }) diff --git a/tests/test-utils/typescript.js b/tests/test-utils/typescript.js new file mode 100644 index 000000000..0c6bbd437 --- /dev/null +++ b/tests/test-utils/typescript.js @@ -0,0 +1,28 @@ +const path = require('path') +const tsParser = require('@typescript-eslint/parser') + +const FIXTURES_ROOT = path.resolve(__dirname, '../fixtures/typescript') +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_VUE_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.vue') + +module.exports = { + getTypeScriptFixtureTestOptions +} + +function getTypeScriptFixtureTestOptions() { + const parser = require('vue-eslint-parser') + const languageOptions = { + parser, + ecmaVersion: 2020, + sourceType: 'module', + parserOptions: { + parser: { ts: tsParser }, + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + } + } + return { + languageOptions, + filename: SRC_VUE_TEST_PATH + } +} diff --git a/tools/lib/categories.js b/tools/lib/categories.js index 9f7fd796e..b0a01aa7d 100644 --- a/tools/lib/categories.js +++ b/tools/lib/categories.js @@ -9,54 +9,31 @@ const rules = require('./rules') const categoryTitles = { base: { - text: 'Base Rules (Enabling Correct ESLint Parsing)', - vuepress: 'Base Rules (Enabling Correct ESLint Parsing)' + text: 'Base Rules (Enabling Correct ESLint Parsing)' }, 'vue3-essential': { - text: 'Priority A: Essential (Error Prevention) for Vue.js 3.x', - vuepress: - 'Priority A: Essential (Error Prevention) for Vue.js 3.x' + text: 'Priority A: Essential (Error Prevention) for Vue.js 3.x' }, 'vue3-strongly-recommended': { - text: - 'Priority B: Strongly Recommended (Improving Readability) for Vue.js 3.x', - vuepress: - 'Priority B: Strongly Recommended (Improving Readability) for Vue.js 3.x' + text: 'Priority B: Strongly Recommended (Improving Readability) for Vue.js 3.x' }, 'vue3-recommended': { - text: - 'Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) for Vue.js 3.x', - vuepress: - 'Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) for Vue.js 3.x' + text: 'Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) for Vue.js 3.x' }, 'vue3-use-with-caution': { - text: - 'Priority D: Use with Caution (Potentially Dangerous Patterns) for Vue.js 3.x', - vuepress: - 'Priority D: Use with Caution (Potentially Dangerous Patterns) for Vue.js 3.x' + text: 'Priority D: Use with Caution (Potentially Dangerous Patterns) for Vue.js 3.x' }, - essential: { - text: 'Priority A: Essential (Error Prevention) for Vue.js 2.x', - vuepress: - 'Priority A: Essential (Error Prevention) for Vue.js 2.x' + 'vue2-essential': { + text: 'Priority A: Essential (Error Prevention) for Vue.js 2.x' }, - 'strongly-recommended': { - text: - 'Priority B: Strongly Recommended (Improving Readability) for Vue.js 2.x', - vuepress: - 'Priority B: Strongly Recommended (Improving Readability) for Vue.js 2.x' + 'vue2-strongly-recommended': { + text: 'Priority B: Strongly Recommended (Improving Readability) for Vue.js 2.x' }, - recommended: { - text: - 'Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) for Vue.js 2.x', - vuepress: - 'Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) for Vue.js 2.x' + 'vue2-recommended': { + text: 'Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) for Vue.js 2.x' }, - 'use-with-caution': { - text: - 'Priority D: Use with Caution (Potentially Dangerous Patterns) for Vue.js 2.x', - vuepress: - 'Priority D: Use with Caution (Potentially Dangerous Patterns) for Vue.js 2.x' + 'vue2-use-with-caution': { + text: 'Priority D: Use with Caution (Potentially Dangerous Patterns) for Vue.js 2.x' } } const categoryIds = Object.keys(categoryTitles) @@ -75,12 +52,38 @@ for (const rule of rules) { } } -module.exports = categoryIds - .map((categoryId) => ({ - categoryId, - title: categoryTitles[categoryId], - rules: (categoryRules[categoryId] || []).filter( - (rule) => !rule.meta.deprecated - ) - })) - .filter((category) => category.rules.length >= 1) +const CONFIG_NAME_CAPTIONS = { + base: ['"plugin:vue/base"', '*.configs["flat/base"]'], + 'vue3-essential': ['"plugin:vue/essential"', '*.configs["flat/essential"]'], + 'vue2-essential': [ + '"plugin:vue/vue2-essential"', + '*.configs["flat/vue2-essential"]' + ], + 'vue3-strongly-recommended': [ + '"plugin:vue/strongly-recommended"', + '*.configs["flat/strongly-recommended"]' + ], + 'vue2-strongly-recommended': [ + '"plugin:vue/vue2-strongly-recommended"', + '*.configs["flat/vue2-strongly-recommended"]' + ], + 'vue3-recommended': [ + '"plugin:vue/recommended"', + '*.configs["flat/recommended"]' + ], + 'vue2-recommended': [ + '"plugin:vue/vue2-recommended"', + '*.configs["flat/vue2-recommended"]' + ] +} + +module.exports = { + CONFIG_NAME_CAPTIONS, + categories: categoryIds + .map((categoryId) => ({ + categoryId, + title: categoryTitles[categoryId], + rules: categoryRules[categoryId] || [] + })) + .filter((category) => category.rules.length > 0) +} diff --git a/tools/lib/configs.js b/tools/lib/configs.js deleted file mode 100644 index 031f41e67..000000000 --- a/tools/lib/configs.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @author Michał Sajnóg - * See LICENSE file in root directory for full license. - */ - -'use strict' - -const fs = require('fs') -const path = require('path') -const ROOT = path.resolve(__dirname, '../../lib/configs') - -module.exports = fs - .readdirSync(ROOT) - .filter((file) => path.extname(file) === '.js') - .map((file) => path.basename(file, '.js')) diff --git a/tools/lib/http.js b/tools/lib/http.js new file mode 100644 index 000000000..27ff970f4 --- /dev/null +++ b/tools/lib/http.js @@ -0,0 +1,6 @@ +module.exports = { + httpGet +} +function httpGet(url) { + return fetch(url).then((res) => res.text()) +} diff --git a/tools/lib/utils.js b/tools/lib/utils.js new file mode 100644 index 000000000..937288ec4 --- /dev/null +++ b/tools/lib/utils.js @@ -0,0 +1,42 @@ +module.exports = { getPresetIds, formatItems } + +const presetCategories = { + base: null, + 'vue2-essential': 'base', + 'vue3-essential': 'base', + 'vue2-strongly-recommended': 'vue2-essential', + 'vue3-strongly-recommended': 'vue3-essential', + 'vue2-recommended': 'vue2-strongly-recommended', + 'vue3-recommended': 'vue3-strongly-recommended' + // 'use-with-caution': 'recommended', + // 'vue3-use-with-caution': 'vue3-recommended' +} + +function formatItems(items, suffix) { + if (items.length === 1) { + return `${items[0]}${suffix ? ` ${suffix[0]}` : ''}` + } + if (items.length === 2) { + return `${items.join(' and ')}${suffix ? ` ${suffix[1]}` : ''}` + } + return `all of ${items.slice(0, -1).join(', ')} and ${[...items].pop()}${ + suffix ? ` ${suffix[1]}` : '' + }` +} + +function getPresetIds(categoryIds) { + const subsetCategoryIds = [] + for (const categoryId of categoryIds) { + for (const [subsetCategoryId, supersetCategoryId] of Object.entries( + presetCategories + )) { + if (supersetCategoryId === categoryId) { + subsetCategoryIds.push(subsetCategoryId) + } + } + } + if (subsetCategoryIds.length === 0) { + return categoryIds + } + return [...new Set([...categoryIds, ...getPresetIds(subsetCategoryIds)])] +} diff --git a/tools/new-rule.js b/tools/new-rule.js new file mode 100644 index 000000000..d9a155795 --- /dev/null +++ b/tools/new-rule.js @@ -0,0 +1,146 @@ +const path = require('path') +const fs = require('fs') +const cp = require('child_process') +const logger = console + +// main +;((ruleName, authorName) => { + if (!ruleName || !authorName) { + logger.error('Usage: npm run new ') + process.exitCode = 1 + return + } + if (!/^[\w-]+$/u.test(ruleName)) { + logger.error("Invalid rule name '%s'.", ruleName) + process.exitCode = 1 + return + } + + const ruleFile = path.resolve(__dirname, `../lib/rules/${ruleName}.js`) + const testFile = path.resolve(__dirname, `../tests/lib/rules/${ruleName}.js`) + const docFile = path.resolve(__dirname, `../docs/rules/${ruleName}.md`) + + fs.writeFileSync( + ruleFile, + `/** + * @author ${authorName} + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +module.exports = { + meta: { + type: 'problem', + docs: { + description: '', + categories: undefined, + url: '' + }, + fixable: null, + schema: [], + messages: { + // ... + } + }, + /** @param {RuleContext} context */ + create(context) { + // ... + + return utils.defineTemplateBodyVisitor(context, { + // ... + }) + } +} +` + ) + fs.writeFileSync( + testFile, + `/** + * @author ${authorName} + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/${ruleName}') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('${ruleName}', rule, { + valid: [ + { + filename: 'test.vue', + code: \` + + \` + }, + ], + invalid: [ + { + filename: 'test.vue', + code: \` + + \`, + errors: [ + { + message: '...', + line: 'line', + column: 'col' + }, + ] + } + ] +}) +` + ) + fs.writeFileSync( + docFile, + `--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/${ruleName} +description: xxx +--- +# vue/${ruleName} + +> xxx + +- :exclamation: _**This rule has not been released yet.**_ + +## :book: Rule Details + +This rule .... + + + +\`\`\`vue + +\`\`\` + + + +## :wrench: Options + +Nothing. + +` + ) + + cp.execSync(`code "${ruleFile}"`) + cp.execSync(`code "${testFile}"`) + cp.execSync(`code "${docFile}"`) +})(process.argv[2], process.argv[3]) diff --git a/tools/update-docs-rules-index.js b/tools/update-docs-rules-index.js index 6e9c0f03f..c456cae44 100644 --- a/tools/update-docs-rules-index.js +++ b/tools/update-docs-rules-index.js @@ -7,66 +7,168 @@ const fs = require('fs') const path = require('path') const rules = require('./lib/rules') -const categories = require('./lib/categories') +const { getPresetIds, formatItems } = require('./lib/utils') +const removedRules = require('../lib/removed-rules') +const { CONFIG_NAME_CAPTIONS } = require('./lib/categories') + +const VUE3_EMOJI = ':three:' +const VUE2_EMOJI = ':two:' // ----------------------------------------------------------------------------- +const categorizedRules = rules.filter( + (rule) => rule.meta.docs.categories && !rule.meta.docs.extensionSource +) const uncategorizedRules = rules.filter( (rule) => !rule.meta.docs.categories && - !rule.meta.docs.extensionRule && + !rule.meta.docs.extensionSource && !rule.meta.deprecated ) const uncategorizedExtensionRule = rules.filter( (rule) => !rule.meta.docs.categories && - rule.meta.docs.extensionRule && + rule.meta.docs.extensionSource && !rule.meta.deprecated ) const deprecatedRules = rules.filter((rule) => rule.meta.deprecated) -function toRuleRow(rule) { - const mark = `${rule.meta.fixable ? ':wrench:' : ''}${ - rule.meta.deprecated ? ':warning:' : '' - }` - const link = `[${rule.ruleId}](./${rule.name}.md)` +const TYPE_MARK = { + problem: ':warning:', + suggestion: ':hammer:', + layout: ':lipstick:' +} + +function toRuleRow(rule, kindMarks = []) { + const mark = [ + rule.meta.fixable ? ':wrench:' : '', + rule.meta.hasSuggestions ? ':bulb:' : '', + rule.meta.deprecated ? ':no_entry_sign:' : '' + ].join('') + const kindMark = [...kindMarks, TYPE_MARK[rule.meta.type]].join('') + const link = `[${rule.ruleId}]` const description = rule.meta.docs.description || '(no description)' - return `| ${link} | ${description} | ${mark} |` + return `| ${link} | ${description} | ${mark} | ${kindMark} |` } function toDeprecatedRuleRow(rule) { - const link = `[${rule.ruleId}](./${rule.name}.md)` - const replacedRules = rule.meta.docs.replacedBy || [] - const replacedBy = replacedRules - .map((name) => `[vue/${name}](./${name}.md)`) - .join(', ') + const link = `[${rule.ruleId}]` + const replacedRules = rule.meta.replacedBy || [] + const replacedBy = replacedRules.map((name) => `[vue/${name}]`).join(', ') return `| ${link} | ${replacedBy || '(no replacement)'} |` } +function toRemovedRuleRow({ + ruleName, + replacedBy, + deprecatedInVersion, + removedInVersion +}) { + const link = `[vue/${ruleName}]` + const replacement = + replacedBy.map((name) => `[vue/${name}]`).join(', ') || '(no replacement)' + const deprecatedVersionLink = `[${deprecatedInVersion}]` + const removedVersionLink = `[${removedInVersion}]` + + return `| ${link} | ${replacement} | ${deprecatedVersionLink} | ${removedVersionLink} |` +} + +const categoryGroups = [ + { + title: 'Base Rules (Enabling Correct ESLint Parsing)', + description: + 'Rules in this category are enabled for all presets provided by eslint-plugin-vue.', + categoryIdForVue3: 'base', + categoryIdForVue2: 'base', + useMark: false + }, + { + title: 'Priority A: Essential (Error Prevention)', + categoryIdForVue3: 'vue3-essential', + categoryIdForVue2: 'vue2-essential', + useMark: true + }, + { + title: 'Priority B: Strongly Recommended (Improving Readability)', + categoryIdForVue3: 'vue3-strongly-recommended', + categoryIdForVue2: 'vue2-strongly-recommended', + useMark: true + }, + { + title: 'Priority C: Recommended (Potentially Dangerous Patterns)', + categoryIdForVue3: 'vue3-recommended', + categoryIdForVue2: 'vue2-recommended', + useMark: true + } +] + // ----------------------------------------------------------------------------- -let rulesTableContent = categories - .map( - (category) => ` -## ${category.title.vuepress} +let rulesTableContent = categoryGroups + .map((group) => { + const rules = categorizedRules.filter( + (rule) => + rule.meta.docs.categories.includes(group.categoryIdForVue3) || + rule.meta.docs.categories.includes(group.categoryIdForVue2) + ) + let content = ` +## ${group.title} +` + + if (group.description) { + content += ` +${group.description} +` + } + if (group.useMark) { + const presetsForVue3 = getPresetIds([group.categoryIdForVue3]).flatMap( + (categoryId) => CONFIG_NAME_CAPTIONS[categoryId]?.map((c) => `\`${c}\``) + ) + const presetsForVue2 = getPresetIds([group.categoryIdForVue2]).map( + (categoryId) => CONFIG_NAME_CAPTIONS[categoryId]?.map((c) => `\`${c}\``) + ) + content += ` +- ${VUE3_EMOJI} Indicates that the rule is for Vue 3 and is included in ${formatItems( + presetsForVue3, + ['preset', 'presets'] + )}. +- ${VUE2_EMOJI} Indicates that the rule is for Vue 2 and is included in ${formatItems( + presetsForVue2, + ['preset', 'presets'] + )}. +` + } -Enforce all the rules in this category, as well as all higher priority rules, with: + content += ` + -\`\`\`json -{ - "extends": "plugin:vue/${category.categoryId}" -} -\`\`\` +| Rule ID | Description | | | +|:--------|:------------|:--:|:--:| +${rules + .map((rule) => { + const mark = group.useMark + ? [ + rule.meta.docs.categories.includes(group.categoryIdForVue3) + ? [VUE3_EMOJI] + : [], + rule.meta.docs.categories.includes(group.categoryIdForVue2) + ? [VUE2_EMOJI] + : [] + ].flat() + : [] + return toRuleRow(rule, mark) + }) + .join('\n')} -| Rule ID | Description | | -|:--------|:------------|:---| -${category.rules.map(toRuleRow).join('\n')} + ` - ) + + return content + }) .join('') // ----------------------------------------------------------------------------- -if (uncategorizedRules.length || uncategorizedExtensionRule.length) { +if (uncategorizedRules.length > 0 || uncategorizedExtensionRule.length > 0) { rulesTableContent += ` ## Uncategorized @@ -86,31 +188,39 @@ For example: \`\`\` ` } -if (uncategorizedRules.length) { +if (uncategorizedRules.length > 0) { rulesTableContent += ` -| Rule ID | Description | | -|:--------|:------------|:---| -${uncategorizedRules.map(toRuleRow).join('\n')} + + +| Rule ID | Description | | | +|:--------|:------------|:--:|:--:| +${uncategorizedRules.map((rule) => toRuleRow(rule)).join('\n')} + + ` } -if (uncategorizedExtensionRule.length) { +if (uncategorizedExtensionRule.length > 0) { rulesTableContent += ` ### Extension Rules The following rules extend the rules provided by ESLint itself and apply them to the expressions in the \`