From afc9c2354b73a1c3d119b56670bca6042930f7ca Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 30 Jan 2024 13:55:40 -0500 Subject: [PATCH 01/21] feat!: Switch to flat config - Switch recommended config to flat config format - Rename old recommended config to recommended-legacy - Updated documentation - Upgraded ESLint - Updated tests fixes #231 --- .eslintignore | 3 - .eslintrc.js | 50 - README.md | 195 +-- eslint.config.js | 52 + examples/react/.eslintrc.js | 43 - examples/react/eslint.config.js | 47 + examples/react/package.json | 5 +- examples/typescript/.eslintrc.js | 22 - examples/typescript/eslint.config.js | 27 + examples/typescript/package.json | 6 +- lib/index.js | 86 +- lib/processor.js | 27 +- package.json | 11 +- tests/.eslintrc.yml | 2 - tests/examples/all.js | 16 +- tests/fixtures/eslint.config.js | 24 + tests/fixtures/long.md | 2 +- tests/fixtures/recommended.js | 13 + tests/fixtures/recommended.json | 2 +- tests/lib/plugin.js | 2427 ++++++++++++++++++-------- 20 files changed, 2067 insertions(+), 993 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.js create mode 100644 eslint.config.js delete mode 100644 examples/react/.eslintrc.js create mode 100644 examples/react/eslint.config.js delete mode 100644 examples/typescript/.eslintrc.js create mode 100644 examples/typescript/eslint.config.js delete mode 100644 tests/.eslintrc.yml create mode 100644 tests/fixtures/eslint.config.js create mode 100644 tests/fixtures/recommended.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index a35916ef..00000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -coverage -tests/fixtures diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 2b419350..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,50 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const PACKAGE_NAME = require("./package").name; -const SYMLINK_LOCATION = path.join(__dirname, "node_modules", PACKAGE_NAME); - -// Symlink node_modules/eslint-plugin-markdown to this directory so that ESLint -// resolves this plugin name correctly. -if (!fs.existsSync(SYMLINK_LOCATION)) { - fs.symlinkSync(__dirname, SYMLINK_LOCATION); -} - -module.exports = { - root: true, - - parserOptions: { - ecmaVersion: 2018 - }, - - plugins: [ - PACKAGE_NAME - ], - - env: { - node: true - }, - - extends: "eslint", - - ignorePatterns: ["examples"], - - overrides: [ - { - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - files: ["**/*.md/*.js"], - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - }, - rules: { - "lines-around-comment": "off" - } - } - ] -}; diff --git a/README.md b/README.md index 58dda721..b1d34d09 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Lint JS, JSX, TypeScript, and more inside Markdown. ### Installing -Install the plugin alongside ESLint v6 or greater: +Install the plugin alongside ESLint v8 or greater: ```sh npm install --save-dev eslint eslint-plugin-markdown @@ -25,7 +25,20 @@ npm install --save-dev eslint eslint-plugin-markdown ### Configuring -Extending the `plugin:markdown/recommended` config will enable the Markdown processor on all `.md` files: +In your `eslint.config.js` file, import `eslint-plugin-markdown` and included the recommended config to enable the Markdown processor on all `.md` files: + +```js +// / eslint.config.js +import markdown from "eslint-plugin-markdown"; + +export default [ + ...markdown.configs.recommended + + // your other configs here +]; +``` + +If you are still using the deprecated `.eslintrc.js` file format for ESLint, you can extend the `plugin:markdown/recommended` config to enable the Markdown processor on all `.md` files: ```js // .eslintrc.js @@ -36,12 +49,48 @@ module.exports = { #### Advanced Configuration -Add the plugin to your `.eslintrc` and use the `processor` option in an `overrides` entry to enable the plugin's `markdown/markdown` processor on Markdown files. +You can manually include the Markdown processor by setting the `processor` option in your configuration file for all `.md` files. + Each fenced code block inside a Markdown document has a virtual filename appended to the Markdown file's path. + The virtual filename's extension will match the fenced code block's syntax tag, so for example, ```js code blocks in README.md would match README.md/*.js. -[`overrides` glob patterns](https://eslint.org/docs/user-guide/configuring#configuration-based-on-glob-patterns) for these virtual filenames can customize configuration for code blocks without affecting regular code. +You can use glob patterns for these virtual filenames to customize configuration for code blocks without affecting regular code. For more information on configuring processors, refer to the [ESLint documentation](https://eslint.org/docs/user-guide/configuring#specifying-processor). +Here's an example: + +```js +// / eslint.config.js +import markdown from "eslint-plugin-markdown"; + +export default [ + { + // 1. Add the plugin + plugins: { + markdown + } + }, + { + // 2. Enable the Markdown processor for all .md files. + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + // 3. Optionally, customize the configuration ESLint uses for ```js + // fenced code blocks inside .md files. + files: ["**/*.md/*.js"], + // ... + rules: { + // ... + } + } + + // your other configs here +]; +``` + +In the deprecated `.eslintrc.js` format: + ```js // .eslintrc.js module.exports = { @@ -77,13 +126,47 @@ The `plugin:markdown/recommended` config disables these rules in Markdown files: - [`no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars) - [`padded-blocks`](https://eslint.org/docs/rules/padded-blocks) -Use [`overrides` glob patterns](https://eslint.org/docs/user-guide/configuring#configuration-based-on-glob-patterns) to disable more rules just for Markdown code blocks: +Use glob patterns to disable more rules just for Markdown code blocks: + +```js +// / eslint.config.js +import markdown from "eslint-plugin-markdown"; + +export default [ + { + plugins: { + markdown + } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + // 1. Target ```js code blocks in .md files. + files: ["**/*.md/*.js"], + rules: { + // 2. Disable other rules. + "no-console": "off", + "import/no-unresolved": "off" + } + } + + // your other configs here +]; +``` + +And in the deprecated `.eslintrc.js` format: ```js +// .eslintrc.js module.exports = { - // ... + plugins: ["markdown"], overrides: [ - // ... + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, { // 1. Target ```js code blocks in .md files. files: ["**/*.md/*.js"], @@ -111,99 +194,9 @@ The `plugin:markdown/recommended` config disables these rules in Markdown files: - [`eol-last`](https://eslint.org/docs/rules/eol-last): The Markdown parser trims trailing newlines from code blocks. - [`unicode-bom`](https://eslint.org/docs/rules/unicode-bom): Markdown code blocks do not have Unicode Byte Order Marks. -#### Migrating from `eslint-plugin-markdown` v1 - -`eslint-plugin-markdown` v1 used an older version of ESLint's processor API. -The Markdown processor automatically ran on `.md`, `.mkdn`, `.mdown`, and `.markdown` files, and it only extracted fenced code blocks marked with `js`, `javascript`, `jsx`, or `node` syntax. -Configuration specifically for fenced code blocks went inside an `overrides` entry with a `files` pattern matching the containing Markdown document's filename that applied to all fenced code blocks inside the file. - -```js -// .eslintrc.js for eslint-plugin-markdown v1 -module.exports = { - plugins: ["markdown"], - overrides: [ - { - files: ["**/*.md"], - // In v1, configuration for fenced code blocks went inside an - // `overrides` entry with a .md pattern, for example: - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - }, - rules: { - "no-console": "off" - } - } - ] -}; -``` - -[RFC3](https://github.com/eslint/rfcs/blob/master/designs/2018-processors-improvements/README.md) designed a new processor API to remove these limitations, and the new API was [implemented](https://github.com/eslint/eslint/pull/11552) as part of ESLint v6. -`eslint-plugin-markdown` v2 uses this new API. - -```bash -$ npm install --save-dev eslint@latest eslint-plugin-markdown@latest -``` - -All of the Markdown file extensions that were previously hard-coded are now fully configurable in `.eslintrc.js`. -Use the new `processor` option to apply the `markdown/markdown` processor on any Markdown documents matching a `files` pattern. -Each fenced code block inside a Markdown document has a virtual filename appended to the Markdown file's path. -The virtual filename's extension will match the fenced code block's syntax tag, so for example, ```js code blocks in README.md would match README.md/*.js. - -```js -// eslintrc.js for eslint-plugin-markdown v2 -module.exports = { - plugins: ["markdown"], - overrides: [ - { - // In v2, explicitly apply eslint-plugin-markdown's `markdown` - // processor on any Markdown files you want to lint. - files: ["**/*.md"], - processor: "markdown/markdown" - }, - { - // In v2, configuration for fenced code blocks is separate from the - // containing Markdown file. Each code block has a virtual filename - // appended to the Markdown file's path. - files: ["**/*.md/*.js"], - // Configuration for fenced code blocks goes with the override for - // the code block's virtual filename, for example: - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - }, - rules: { - "no-console": "off" - } - } - ] -}; -``` - -If you need to precisely mimic the behavior of v1 with the hard-coded Markdown extensions and fenced code block syntaxes, you can use those as glob patterns in `overrides[].files`: - -```js -// eslintrc.js for v2 mimicking v1 behavior -module.exports = { - plugins: ["markdown"], - overrides: [ - { - files: ["**/*.{md,mkdn,mdown,markdown}"], - processor: "markdown/markdown" - }, - { - files: ["**/*.{md,mkdn,mdown,markdown}/*.{js,javascript,jsx,node}"] - // ... - } - ] -}; -``` - ### Running -#### ESLint v7 +#### ESLint v7+ You can run ESLint as usual and do not need to use the `--ext` option. ESLint v7 [automatically lints file extensions specified in `overrides[].files` patterns in config files](https://github.com/eslint/rfcs/blob/0253e3a95511c65d622eaa387eb73f824249b467/designs/2019-additional-lint-targets/README.md). @@ -282,7 +275,7 @@ Comment bodies are passed through unmodified, so the plugin supports any [config This example enables the `browser` environment, disables the `no-alert` rule, and configures the `quotes` rule to prefer single quotes: ````markdown - + @@ -296,7 +289,7 @@ Each code block in a file is linted separately, so configuration comments apply ````markdown Assuming `no-alert` is enabled in `.eslintrc`, the first code block will have no error from `no-alert`: - + ```js @@ -305,7 +298,7 @@ alert("Hello, world!"); But the next code block will have an error from `no-alert`: - + ```js alert("Hello, world!"); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..f9103b15 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,52 @@ +"use strict"; + +module.exports = [ + ...require("eslint-config-eslint").map(config => ({ + ...config, + files: ["**/*.js"] + })), + { + plugins: { + markdown: require(".") + } + }, + { + ignores: [ + "**/examples", + "**/coverage", + "**/tests/fixtures" + ] + }, + { + files: ["**/*.js"], + languageOptions: { + sourceType: "commonjs" + } + }, + { + files: ["tests/**/*.js"], + languageOptions: { + globals: { + ...require("globals").mocha + } + } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + files: ["**/*.md/*.js"], + languageOptions: { + sourceType: "module", + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + }, + rules: { + "lines-around-comment": "off" + } + } +]; diff --git a/examples/react/.eslintrc.js b/examples/react/.eslintrc.js deleted file mode 100644 index fb07a1d9..00000000 --- a/examples/react/.eslintrc.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -module.exports = { - root: true, - extends: [ - "eslint:recommended", - "plugin:markdown/recommended", - "plugin:react/recommended", - ], - settings: { - react: { - version: "16.8.0" - } - }, - parserOptions: { - ecmaFeatures: { - jsx: true - }, - ecmaVersion: 2015, - sourceType: "module" - }, - env: { - browser: true, - es6: true - }, - overrides: [ - { - files: [".eslintrc.js"], - env: { - node: true - } - }, - { - files: ["**/*.md/*.jsx"], - globals: { - // For code examples, `import React from "react";` at the top - // of every code block is distracting, so pre-define the - // `React` global. - React: false - }, - } - ] -}; diff --git a/examples/react/eslint.config.js b/examples/react/eslint.config.js new file mode 100644 index 00000000..ee4245b3 --- /dev/null +++ b/examples/react/eslint.config.js @@ -0,0 +1,47 @@ +"use strict"; + +const { FlatCompat } = require("@eslint/eslintrc"); +const markdown = require("../.."); +const js = require("@eslint/js"); +const globals = require("globals"); + +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +module.exports = [ + js.configs.recommended, + ...markdown.configs.recommended, + ...compat.extends("plugin:react/recommended"), + { + settings: { + react: { + version: "16.8.0" + } + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + ecmaVersion: 2015, + sourceType: "module", + globals: globals.browser + } + }, + { + files: ["eslint.config.js"], + languageOptions: { + sourceType: "commonjs" + } + }, + { + files: ["**/*.md/*.jsx"], + languageOptions: { + globals: { + React: false + } + } + } +]; diff --git a/examples/react/package.json b/examples/react/package.json index 9ddc0524..c3107010 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -4,8 +4,11 @@ "test": "eslint ." }, "devDependencies": { + "@eslint/eslintrc": "^3.0.0", + "@eslint/js": "^8.56.0", "eslint": "^7.5.0", "eslint-plugin-markdown": "file:../..", - "eslint-plugin-react": "^7.20.3" + "eslint-plugin-react": "^7.20.3", + "globals": "^13.24.0" } } diff --git a/examples/typescript/.eslintrc.js b/examples/typescript/.eslintrc.js deleted file mode 100644 index d512301b..00000000 --- a/examples/typescript/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; - -module.exports = { - root: true, - extends: [ - "eslint:recommended", - "plugin:markdown/recommended", - ], - overrides: [ - { - files: [".eslintrc.js"], - env: { - node: true - } - }, - { - files: ["*.ts"], - parser: "@typescript-eslint/parser", - extends: ["plugin:@typescript-eslint/recommended"] - }, - ] -}; diff --git a/examples/typescript/eslint.config.js b/examples/typescript/eslint.config.js new file mode 100644 index 00000000..6ee9381a --- /dev/null +++ b/examples/typescript/eslint.config.js @@ -0,0 +1,27 @@ +"use strict"; + +const markdown = require("../.."); +const js = require("@eslint/js") +const { FlatCompat } = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +module.exports = [ + js.configs.recommended, + ...markdown.configs.recommended, + { + files: ["eslint.config.js"], + languageOptions: { + sourceType: "commonjs" + } + }, + ...compat.config({ + parser: "@typescript-eslint/parser", + extends: ["plugin:@typescript-eslint/recommended"] + }).map(config => ({ + ...config, + files: ["**/*.ts"] + })) +]; diff --git a/examples/typescript/package.json b/examples/typescript/package.json index 45df326c..dd20a1f0 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -4,8 +4,10 @@ "test": "eslint ." }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^3.6.1", - "@typescript-eslint/parser": "^3.6.1", + "@eslint/eslintrc": "^3.0.0", + "@eslint/js": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", "eslint": "^7.5.0", "eslint-plugin-markdown": "file:../..", "typescript": "^3.9.7" diff --git a/lib/index.js b/lib/index.js index d66a7ddd..47aa0b61 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,9 +7,35 @@ const processor = require("./processor"); -module.exports = { +const rulesConfig = { + + // The Markdown parser automatically trims trailing + // newlines from code blocks. + "eol-last": "off", + + // In code snippets and examples, these rules are often + // counterproductive to clarity and brevity. + "no-undef": "off", + "no-unused-expressions": "off", + "no-unused-vars": "off", + "padded-blocks": "off", + + // Adding a "use strict" directive at the top of every + // code block is tedious and distracting. The config + // opts into strict mode parsing without the directive. + strict: "off", + + // The processor will not receive a Unicode Byte Order + // Mark from the Markdown parser. + "unicode-bom": "off" +}; + +const plugin = { + processors: { + markdown: processor + }, configs: { - recommended: { + "recommended-legacy": { plugins: ["markdown"], overrides: [ { @@ -29,32 +55,42 @@ module.exports = { } }, rules: { - - // The Markdown parser automatically trims trailing - // newlines from code blocks. - "eol-last": "off", - - // In code snippets and examples, these rules are often - // counterproductive to clarity and brevity. - "no-undef": "off", - "no-unused-expressions": "off", - "no-unused-vars": "off", - "padded-blocks": "off", - - // Adding a "use strict" directive at the top of every - // code block is tedious and distracting. The config - // opts into strict mode parsing without the directive. - strict: "off", - - // The processor will not receive a Unicode Byte Order - // Mark from the Markdown parser. - "unicode-bom": "off" + ...rulesConfig } } ] } - }, - processors: { - markdown: processor } }; + +plugin.configs.recommended = [ + { + plugins: { + markdown: plugin + } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + files: ["**/*.md/**"], + languageOptions: { + parserOptions: { + ecmaFeatures: { + + // Adding a "use strict" directive at the top of + // every code block is tedious and distracting, so + // opt into strict mode parsing without the + // directive. + impliedStrict: true + } + } + }, + rules: { + ...rulesConfig + } + } +]; + +module.exports = plugin; diff --git a/lib/processor.js b/lib/processor.js index 63aa9159..d5fdc34d 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -5,11 +5,9 @@ /** * @typedef {import('eslint/lib/shared/types').LintMessage} Message - * * @typedef {Object} ASTNode - * @property {string} type - * @property {string} [lang] - * + * @property {string} type The type of node. + * @property {string} [lang] The language that the node is in * @typedef {Object} RangeMap * @property {number} indent Number of code block indent characters trimmed from * the beginning of the line during extraction. @@ -17,12 +15,13 @@ * extracted JS. * @property {number} md Offset from the start of the code block's range in the * original Markdown. - * * @typedef {Object} BlockBase - * @property {string} baseIndentText - * @property {string[]} comments - * @property {RangeMap[]} rangeMap - * + * @property {string} baseIndentText Leading whitespace text for the block. + * @property {string[]} comments Comments inside of the JavaScript code. + * @property {RangeMap[]} rangeMap A list of offset-based adjustments, where + * lookups are done based on the `js` key, which represents the range in the + * linted JS, and the `md` key is the offset delta that, when added to the JS + * range, returns the corresponding location in the original Markdown source. * @typedef {ASTNode & BlockBase} Block */ @@ -30,10 +29,10 @@ const parse = require("mdast-util-from-markdown"); -const UNSATISFIABLE_RULES = [ +const UNSATISFIABLE_RULES = new Set([ "eol-last", // The Markdown parser strips trailing newlines in code fences "unicode-bom" // Code blocks will begin in the middle of Markdown files -]; +]); const SUPPORTS_AUTOFIX = true; /** @@ -376,7 +375,7 @@ function adjustBlock(block) { * @returns {boolean} True if the message should be included in output. */ function excludeUnsatisfiableRules(message) { - return message && UNSATISFIABLE_RULES.indexOf(message.ruleId) < 0; + return message && !UNSATISFIABLE_RULES.has(message.ruleId); } /** @@ -391,11 +390,11 @@ function postprocess(messages, filename) { blocksCache.delete(filename); - return [].concat(...messages.map((group, i) => { + return messages.flatMap((group, i) => { const adjust = adjustBlock(blocks[i]); return group.map(adjust).filter(excludeUnsatisfiableRules); - })); + }); } module.exports = { diff --git a/package.json b/package.json index 7446546a..60a97f41 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,14 @@ "linter" ], "scripts": { - "lint": "eslint --ext js,md .", + "lint": "eslint .", "prepare": "node ./npm-prepare.js", "release:generate:latest": "eslint-generate-release", "release:generate:alpha": "eslint-generate-prerelease alpha", "release:generate:beta": "eslint-generate-prerelease beta", "release:generate:rc": "eslint-generate-prerelease rc", "release:publish": "eslint-publish-release", - "test": "nyc _mocha -- -c tests/{examples,lib}/**/*.js" + "test": "nyc _mocha -- -c tests/{examples,lib}/**/*.js --timeout 30000" }, "main": "index.js", "files": [ @@ -36,12 +36,15 @@ "lib/processor.js" ], "devDependencies": { + "@eslint/eslintrc": "^3.0.0", + "@eslint/js": "^8.56.0", "chai": "^4.2.0", - "eslint": "^7.32.0", - "eslint-config-eslint": "^7.0.0", + "eslint": "^8.56.0", + "eslint-config-eslint": "^9.0.0", "eslint-plugin-jsdoc": "^37.0.3", "eslint-plugin-node": "^11.1.0", "eslint-release": "^3.1.2", + "globals": "^13.24.0", "mocha": "^6.2.2", "nyc": "^14.1.1" }, diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml deleted file mode 100644 index 72b9c8cc..00000000 --- a/tests/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -env: - mocha: true diff --git a/tests/examples/all.js b/tests/examples/all.js index 9f8821ad..2efdf192 100644 --- a/tests/examples/all.js +++ b/tests/examples/all.js @@ -22,19 +22,19 @@ for (const example of examples) { paths: [cwd] }); const eslintPackageJson = require(eslintPackageJsonPath); + if (semver.satisfies(process.version, eslintPackageJson.engines.node)) { - describe("examples", function () { + describe("examples", () => { describe(example, () => { it("reports errors on code blocks in .md files", async () => { - const { ESLint } = require( - require.resolve("eslint", { paths: [cwd] }) + const { FlatESLint } = require( + require.resolve("eslint/use-at-your-own-risk", { paths: [cwd] }) ); - const eslint = new ESLint({ cwd }); - - const results = await eslint.lintFiles(["."]); + const eslint = new FlatESLint({ cwd }); + const results = await eslint.lintFiles(["README.md"]); const readme = results.find(result => - path.basename(result.filePath) == "README.md" - ); + path.basename(result.filePath) == "README.md"); + assert.isNotNull(readme); assert.isAbove(readme.messages.length, 0); }); diff --git a/tests/fixtures/eslint.config.js b/tests/fixtures/eslint.config.js new file mode 100644 index 00000000..0cd96371 --- /dev/null +++ b/tests/fixtures/eslint.config.js @@ -0,0 +1,24 @@ +const markdown = require("../../"); +const globals = require("globals"); + +module.exports = [ + { + plugins: { + markdown + }, + languageOptions: { + globals: globals.browser + }, + rules: { + "eol-last": "error", + "no-console": "error", + "no-undef": "error", + "quotes": "error", + "spaced-comment": "error" + } + }, + { + "files": ["*.md", "*.mkdn", "*.mdown", "*.markdown", "*.custom"], + "processor": "markdown/markdown" + } +]; diff --git a/tests/fixtures/long.md b/tests/fixtures/long.md index c1f5c3f4..78f2940a 100644 --- a/tests/fixtures/long.md +++ b/tests/fixtures/long.md @@ -17,7 +17,7 @@ function foo() { } ``` - + ```js diff --git a/tests/fixtures/recommended.js b/tests/fixtures/recommended.js new file mode 100644 index 00000000..d2d72b84 --- /dev/null +++ b/tests/fixtures/recommended.js @@ -0,0 +1,13 @@ +const markdown = require("../../"); +const js = require("@eslint/js") + +module.exports = [ + js.configs.recommended, + ...markdown.configs.recommended, + { + "rules": { + "no-console": "error" + } + } + +]; diff --git a/tests/fixtures/recommended.json b/tests/fixtures/recommended.json index 9aea6099..a21edeb9 100644 --- a/tests/fixtures/recommended.json +++ b/tests/fixtures/recommended.json @@ -1,6 +1,6 @@ { "root": true, - "extends": ["eslint:recommended", "plugin:markdown/recommended"], + "extends": ["eslint:recommended", "plugin:markdown/recommended-legacy"], "rules": { "no-console": "error" } diff --git a/tests/lib/plugin.js b/tests/lib/plugin.js index e554395b..6818d14e 100644 --- a/tests/lib/plugin.js +++ b/tests/lib/plugin.js @@ -5,660 +5,397 @@ "use strict"; +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + const assert = require("chai").assert; const execSync = require("child_process").execSync; -const { CLIEngine, ESLint } = require("eslint"); +const { LegacyESLint, FlatESLint } = require("eslint/use-at-your-own-risk"); const path = require("path"); const plugin = require("../.."); -/** - * @typedef {import('eslint/lib/cli-engine/cli-engine').CLIEngineOptions} CLIEngineOptions - */ +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- /** - * Helper function which creates CLIEngine instance with enabled/disabled autofix feature. + * Helper function which creates ESLint instance with enabled/disabled autofix feature. * @param {string} fixtureConfigName ESLint JSON config fixture filename. - * @param {CLIEngineOptions} [options={}] Whether to enable autofix feature. - * @returns {ESLint} ESLint instance to execute in tests. + * @param {Object} [options={}] Whether to enable autofix feature. + * @returns {LegacyESLint} ESLint instance to execute in tests. */ -function initESLint(fixtureConfigName, options = {}) { - if (ESLint) { // ESLint v7+ - return new ESLint({ - cwd: path.resolve(__dirname, "../fixtures/"), - ignore: false, - useEslintrc: false, - overrideConfigFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), - plugins: { markdown: plugin }, - ...options - }); - } - - const cli = new CLIEngine({ +function initLegacyESLint(fixtureConfigName, options = {}) { + return new LegacyESLint({ cwd: path.resolve(__dirname, "../fixtures/"), ignore: false, useEslintrc: false, - configFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), + overrideConfigFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), + plugins: { markdown: plugin }, ...options }); - - cli.addPlugin("markdown", plugin); - return { - async calculateConfigForFile(filename) { - return cli.getConfigForFile(filename); - }, - async lintFiles(files) { - return cli.executeOnFiles(files).results; - }, - async lintText(text, { filePath }) { - return cli.executeOnText(text, filePath).results; - } - }; } -describe("recommended config", () => { - let eslint; - const shortText = [ - "```js", - "var unusedVar = console.log(undef);", - "'unused expression';", - "```" - ].join("\n"); - - before(function() { - try { - - // The tests for the recommended config will have ESLint import - // the plugin, so we need to make sure it's resolvable and link it - // if not. - // eslint-disable-next-line node/no-extraneous-require - require.resolve("eslint-plugin-markdown"); - } catch (error) { - if (error.code === "MODULE_NOT_FOUND") { - - // The npm link step can take longer than Mocha's default 2s - // timeout, so give it more time. Mocha's API for customizing - // hook-level timeouts uses `this`, so disable the rule. - // https://mochajs.org/#hook-level - // eslint-disable-next-line no-invalid-this - this.timeout(30000); - - execSync("npm link && npm link eslint-plugin-markdown --legacy-peer-deps"); - } else { - throw error; - } - } - - eslint = initESLint("recommended.json"); - }); - - it("should include the plugin", async () => { - const config = await eslint.calculateConfigForFile("test.md"); - - assert.include(config.plugins, "markdown"); - }); - - it("applies convenience configuration", async () => { - const config = await eslint.calculateConfigForFile("subdir/test.md/0.js"); - - assert.deepStrictEqual(config.parserOptions, { - ecmaFeatures: { - impliedStrict: true - } - }); - assert.deepStrictEqual(config.rules["eol-last"], ["off"]); - assert.deepStrictEqual(config.rules["no-undef"], ["off"]); - assert.deepStrictEqual(config.rules["no-unused-expressions"], ["off"]); - assert.deepStrictEqual(config.rules["no-unused-vars"], ["off"]); - assert.deepStrictEqual(config.rules["padded-blocks"], ["off"]); - assert.deepStrictEqual(config.rules.strict, ["off"]); - assert.deepStrictEqual(config.rules["unicode-bom"], ["off"]); - }); - - it("overrides configure processor to parse .md file code blocks", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].ruleId, "no-console"); +/** + * Helper function which creates ESLint instance with enabled/disabled autofix feature. + * @param {string} fixtureConfigName ESLint config filename. + * @param {Object} [options={}] Whether to enable autofix feature. + * @returns {FlatESLint} ESLint instance to execute in tests. + */ +function initFlatESLint(fixtureConfigName, options = {}) { + return new FlatESLint({ + cwd: path.resolve(__dirname, "../fixtures/"), + ignore: false, + overrideConfigFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), + plugins: { markdown: plugin }, + ...options }); +} -}); - -describe("plugin", () => { - let eslint; - const shortText = [ - "```js", - "console.log(42);", - "```" - ].join("\n"); - before(() => { - eslint = initESLint("eslintrc.json"); - }); +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- - it("should run on .md files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.md" }); +describe("LegacyESLint", () => { - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); - it("should emit correct line numbers", async () => { - const code = [ - "# Hello, world!", - "", - "", + describe("recommended config", () => { + let eslint; + const shortText = [ "```js", - "var bar = baz", - "", - "", - "var foo = blah", + "var unusedVar = console.log(undef);", + "'unused expression';", "```" ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); - assert.strictEqual(results[0].messages[0].line, 5); - assert.strictEqual(results[0].messages[0].endLine, 5); - assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); - assert.strictEqual(results[0].messages[1].line, 8); - assert.strictEqual(results[0].messages[1].endLine, 8); - }); - // https://github.com/eslint/eslint-plugin-markdown/issues/77 - it("should emit correct line numbers with leading blank line", async () => { - const code = [ - "### Heading", - "", - "```js", - "", - "console.log('a')", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); + before(function() { + try { - assert.strictEqual(results[0].messages[0].line, 5); - }); + // The tests for the recommended config will have ESLint import + // the plugin, so we need to make sure it's resolvable and link it + // if not. - it("doesn't add end locations to messages without them", async () => { - const code = [ - "```js", - "!@#$%^&*()", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.notProperty(results[0].messages[0], "endLine"); - assert.notProperty(results[0].messages[0], "endColumn"); - }); + require.resolve("eslint-plugin-markdown"); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { - it("should emit correct line numbers with leading comments", async () => { - const code = [ - "# Hello, world!", - "", - "", - "", - "", - "```js", - "var bar = baz", - "", - "var str = 'single quotes'", - "", - "var foo = blah", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); - assert.strictEqual(results[0].messages[0].line, 7); - assert.strictEqual(results[0].messages[0].endLine, 7); - assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); - assert.strictEqual(results[0].messages[1].line, 11); - assert.strictEqual(results[0].messages[1].endLine, 11); - }); + // The npm link step can take longer than Mocha's default 2s + // timeout, so give it more time. Mocha's API for customizing + // hook-level timeouts uses `this`, so disable the rule. + // https://mochajs.org/#hook-level + // eslint-disable-next-line no-invalid-this -- need more time + this.timeout(30000); - it("should run on .mkdn files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.mkdn" }); + execSync("npm link && npm link eslint-plugin-markdown --legacy-peer-deps"); + } else { + throw error; + } + } - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); + eslint = initLegacyESLint("recommended.json"); + }); - it("should run on .mdown files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.mdown" }); + it("should include the plugin", async () => { + const config = await eslint.calculateConfigForFile("test.md"); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); + assert.include(config.plugins, "markdown"); + }); - it("should run on .markdown files", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.markdown" }); + it("applies convenience configuration", async () => { + const config = await eslint.calculateConfigForFile("subdir/test.md/0.js"); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); + assert.deepStrictEqual(config.parserOptions, { + ecmaFeatures: { + impliedStrict: true + } + }); + assert.deepStrictEqual(config.rules["eol-last"], ["off"]); + assert.deepStrictEqual(config.rules["no-undef"], ["off"]); + assert.deepStrictEqual(config.rules["no-unused-expressions"], ["off"]); + assert.deepStrictEqual(config.rules["no-unused-vars"], ["off"]); + assert.deepStrictEqual(config.rules["padded-blocks"], ["off"]); + assert.deepStrictEqual(config.rules.strict, ["off"]); + assert.deepStrictEqual(config.rules["unicode-bom"], ["off"]); + }); - it("should run on files with any custom extension", async () => { - const results = await eslint.lintText(shortText, { filePath: "test.custom" }); + it("overrides configure processor to parse .md file code blocks", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.md" }); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 2); - }); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + }); - it("should extract blocks and remap messages", async () => { - const results = await eslint.lintFiles([path.resolve(__dirname, "../fixtures/long.md")]); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 5); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 10); - assert.strictEqual(results[0].messages[0].column, 1); - assert.strictEqual(results[0].messages[1].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[1].line, 16); - assert.strictEqual(results[0].messages[1].column, 5); - assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[2].line, 24); - assert.strictEqual(results[0].messages[2].column, 1); - assert.strictEqual(results[0].messages[3].message, "Strings must use singlequote."); - assert.strictEqual(results[0].messages[3].line, 38); - assert.strictEqual(results[0].messages[3].column, 13); - assert.strictEqual(results[0].messages[4].message, "Parsing error: Unexpected character '@'"); - assert.strictEqual(results[0].messages[4].line, 46); - assert.strictEqual(results[0].messages[4].column, 2); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/181 - it("should work when called on nested code blocks in the same file", async () => { - - /* - * As of this writing, the nested code block, though it uses the same - * Markdown processor, must use a different extension or ESLint will not - * re-apply the processor on the nested code block. To work around that, - * a file named `test.md` contains a nested `markdown` code block in - * this test. - * - * https://github.com/eslint/eslint/pull/14227/files#r602802758 - */ - const code = [ - "", - "", - "````markdown", - "", - "", - "This test only repros if the MD files have a different number of lines before code blocks.", - "", + describe("plugin", () => { + let eslint; + const shortText = [ "```js", - "// test.md/0_0.markdown/0_0.js", - "console.log('single quotes')", - "```", - "````" + "console.log(42);", + "```" ].join("\n"); - const recursiveCli = initESLint("eslintrc.json", { - extensions: [".js", ".markdown", ".md"] + + before(() => { + eslint = initLegacyESLint("eslintrc.json"); }); - const results = await recursiveCli.lintText(code, { filePath: "test.md" }); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 2); - assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[0].line, 10); - assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); - assert.strictEqual(results[0].messages[1].line, 10); - }); - describe("configuration comments", () => { - it("apply only to the code block immediately following", async () => { - const code = [ - "", - "", - "", - "```js", - "var single = 'single';", - "console.log(single);", - "var double = \"double\";", - "console.log(double);", - "```", - "", - "```js", - "var single = 'single';", - "console.log(single);", - "var double = \"double\";", - "console.log(double);", - "```" - ].join("\n"); - const results = await eslint.lintText(code, { filePath: "test.md" }); + it("should run on .md files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.md" }); assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 4); - assert.strictEqual(results[0].messages[0].message, "Strings must use singlequote."); - assert.strictEqual(results[0].messages[0].line, 7); - assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); - assert.strictEqual(results[0].messages[1].line, 12); - assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[2].line, 13); - assert.strictEqual(results[0].messages[3].message, "Unexpected console statement."); - assert.strictEqual(results[0].messages[3].line, 15); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - // https://github.com/eslint/eslint-plugin-markdown/issues/78 - it("preserves leading empty lines", async () => { + it("should emit correct line numbers", async () => { const code = [ - "", + "# Hello, world!", + "", "", "```js", + "var bar = baz", "", - "\"use strict\";", + "", + "var foo = blah", "```" ].join("\n"); const results = await eslint.lintText(code, { filePath: "test.md" }); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].message, "Unexpected newline before \"use strict\" directive."); + assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); assert.strictEqual(results[0].messages[0].line, 5); - }); - }); - - describe("should fix code", () => { - before(() => { - eslint = initESLint("eslintrc.json", { fix: true }); + assert.strictEqual(results[0].messages[0].endLine, 5); + assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); + assert.strictEqual(results[0].messages[1].line, 8); + assert.strictEqual(results[0].messages[1].endLine, 8); }); - it("in the simplest case", async () => { - const input = [ - "This is Markdown.", + // https://github.com/eslint/eslint-plugin-markdown/issues/77 + it("should emit correct line numbers with leading blank line", async () => { + const code = [ + "### Heading", "", "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", "", - "```js", - "console.log(\"Hello, world!\")", + "console.log('a')", "```" ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + const results = await eslint.lintText(code, { filePath: "test.md" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results[0].messages[0].line, 5); }); - it("across multiple lines", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, world!')", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", + it("doesn't add end locations to messages without them", async () => { + const code = [ "```js", - "console.log(\"Hello, world!\")", - "console.log(\"Hello, world!\")", + "!@#$%^&*()", "```" ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + const results = await eslint.lintText(code, { filePath: "test.md" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.notProperty(results[0].messages[0], "endLine"); + assert.notProperty(results[0].messages[0], "endColumn"); }); - it("across multiple blocks", async () => { - const input = [ - "This is Markdown.", + it("should emit correct line numbers with leading comments", async () => { + const code = [ + "# Hello, world!", "", - "```js", - "console.log('Hello, world!')", - "```", + "", + "", "", "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", + "var bar = baz", "", - "```js", - "console.log(\"Hello, world!\")", - "```", + "var str = 'single quotes'", "", - "```js", - "console.log(\"Hello, world!\")", + "var foo = blah", "```" ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + const results = await eslint.lintText(code, { filePath: "test.md" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[0].endLine, 7); + assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); + assert.strictEqual(results[0].messages[1].line, 11); + assert.strictEqual(results[0].messages[1].endLine, 11); }); - it("with lines indented by spaces", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "function test() {", - " console.log('Hello, world!')", - "}", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "function test() {", - " console.log(\"Hello, world!\")", - "}", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + it("should run on .mkdn files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.mkdn" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - it("with lines indented by tabs", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "function test() {", - "\tconsole.log('Hello, world!')", - "}", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "function test() {", - "\tconsole.log(\"Hello, world!\")", - "}", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + it("should run on .mdown files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.mdown" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - it("at the very start of a block", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "'use strict'", - "```" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "\"use strict\"", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + it("should run on .markdown files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.markdown" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - it("in blocks with extra backticks", async () => { - const input = [ - "This is Markdown.", - "", - "````js", - "console.log('Hello, world!')", - "````" - ].join("\n"); - const expected = [ - "This is Markdown.", - "", - "````js", - "console.log(\"Hello, world!\")", - "````" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + it("should run on files with any custom extension", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.custom" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - it("with configuration comments", async () => { - const input = [ - "", - "", - "```js", - "console.log('Hello, world!')", - "```" - ].join("\n"); - const expected = [ - "", - "", - "```js", - "console.log(\"Hello, world!\");", - "```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + it("should extract blocks and remap messages", async () => { + const results = await eslint.lintFiles([path.resolve(__dirname, "../fixtures/long.md")]); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual(results[0].messages[0].column, 1); + assert.strictEqual(results[0].messages[1].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[1].line, 16); + assert.strictEqual(results[0].messages[1].column, 5); + assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[2].line, 24); + assert.strictEqual(results[0].messages[2].column, 1); + assert.strictEqual(results[0].messages[3].message, "Strings must use singlequote."); + assert.strictEqual(results[0].messages[3].line, 38); + assert.strictEqual(results[0].messages[3].column, 13); + assert.strictEqual(results[0].messages[4].message, "Parsing error: Unexpected character '@'"); + assert.strictEqual(results[0].messages[4].line, 46); + assert.strictEqual(results[0].messages[4].column, 2); }); - it("inside a list single line", async () => { - const input = [ - "- Inside a list", - "", - " ```js", - " console.log('Hello, world!')", - " ```" - ].join("\n"); - const expected = [ - "- Inside a list", + // https://github.com/eslint/eslint-plugin-markdown/issues/181 + it("should work when called on nested code blocks in the same file", async () => { + + /* + * As of this writing, the nested code block, though it uses the same + * Markdown processor, must use a different extension or ESLint will not + * re-apply the processor on the nested code block. To work around that, + * a file named `test.md` contains a nested `markdown` code block in + * this test. + * + * https://github.com/eslint/eslint/pull/14227/files#r602802758 + */ + const code = [ + "", "", - " ```js", - " console.log(\"Hello, world!\")", - " ```" - ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; - - assert.strictEqual(actual, expected); - }); - - it("inside a list multi line", async () => { - const input = [ - "- Inside a list", + "````markdown", + "", "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ", - " var obj = {", - " hello: 'value'", - " }", - " ```" - ].join("\n"); - const expected = [ - "- Inside a list", + "This test only repros if the MD files have a different number of lines before code blocks.", "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ", - " var obj = {", - " hello: \"value\"", - " }", - " ```" + "```js", + "// test.md/0_0.markdown/0_0.js", + "console.log('single quotes')", + "```", + "````" ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + const recursiveCli = initLegacyESLint("eslintrc.json", { + extensions: [".js", ".markdown", ".md"] + }); + const results = await recursiveCli.lintText(code, { filePath: "test.md" }); - assert.strictEqual(actual, expected); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); + assert.strictEqual(results[0].messages[1].line, 10); }); - it("with multiline autofix and CRLF", async () => { - const input = [ - "This is Markdown.", - "", - "```js", - "console.log('Hello, \\", - "world!')", - "console.log('Hello, \\", - "world!')", - "```" - ].join("\r\n"); - const expected = [ - "This is Markdown.", - "", - "```js", - "console.log(\"Hello, \\", - "world!\")", - "console.log(\"Hello, \\", - "world!\")", - "```" - ].join("\r\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + describe("configuration comments", () => { + it("apply only to the code block immediately following", async () => { + const code = [ + "", + "", + "", + "```js", + "var single = 'single';", + "console.log(single);", + "var double = \"double\";", + "console.log(double);", + "```", + "", + "```js", + "var single = 'single';", + "console.log(single);", + "var double = \"double\";", + "console.log(double);", + "```" + ].join("\n"); + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual(results[0].messages[0].message, "Strings must use singlequote."); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); + assert.strictEqual(results[0].messages[1].line, 12); + assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[2].line, 13); + assert.strictEqual(results[0].messages[3].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[3].line, 15); + }); - assert.strictEqual(actual, expected); + // https://github.com/eslint/eslint-plugin-markdown/issues/78 + it("preserves leading empty lines", async () => { + const code = [ + "", + "", + "```js", + "", + "\"use strict\";", + "```" + ].join("\n"); + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected newline before \"use strict\" directive."); + assert.strictEqual(results[0].messages[0].line, 5); + }); }); - // https://spec.commonmark.org/0.28/#fenced-code-blocks - describe("when indented", () => { - it("by one space", async () => { + describe("should fix code", () => { + before(() => { + eslint = initLegacyESLint("eslintrc.json", { fix: true }); + }); + + it("in the simplest case", async () => { const input = [ "This is Markdown.", "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" + "```js", + "console.log('Hello, world!')", + "```" ].join("\n"); const expected = [ "This is Markdown.", "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" + "```js", + "console.log(\"Hello, world!\")", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -666,22 +403,22 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("by two spaces", async () => { + it("across multiple lines", async () => { const input = [ "This is Markdown.", "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" + "```js", + "console.log('Hello, world!')", + "console.log('Hello, world!')", + "```" ].join("\n"); const expected = [ "This is Markdown.", "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" + "```js", + "console.log(\"Hello, world!\")", + "console.log(\"Hello, world!\")", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -689,22 +426,28 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("by three spaces", async () => { + it("across multiple blocks", async () => { const input = [ "This is Markdown.", "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" + "```js", + "console.log('Hello, world!')", + "```", + "", + "```js", + "console.log('Hello, world!')", + "```" ].join("\n"); const expected = [ "This is Markdown.", "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" + "```js", + "console.log(\"Hello, world!\")", + "```", + "", + "```js", + "console.log(\"Hello, world!\")", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -712,22 +455,24 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("and the closing fence is differently indented", async () => { + it("with lines indented by spaces", async () => { const input = [ "This is Markdown.", "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" + "```js", + "function test() {", + " console.log('Hello, world!')", + "}", + "```" ].join("\n"); const expected = [ "This is Markdown.", "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" + "```js", + "function test() {", + " console.log(\"Hello, world!\")", + "}", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -735,24 +480,24 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("underindented", async () => { + it("with lines indented by tabs", async () => { const input = [ "This is Markdown.", "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" + "```js", + "function test() {", + "\tconsole.log('Hello, world!')", + "}", + "```" ].join("\n"); const expected = [ "This is Markdown.", "", - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, world!\")", - " ```" + "```js", + "function test() {", + "\tconsole.log(\"Hello, world!\")", + "}", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -760,26 +505,20 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("multiline autofix", async () => { + it("at the very start of a block", async () => { const input = [ "This is Markdown.", "", - " ```js", - " console.log('Hello, \\", - " world!')", - " console.log('Hello, \\", - " world!')", - " ```" + "```js", + "'use strict'", + "```" ].join("\n"); const expected = [ "This is Markdown.", "", - " ```js", - " console.log(\"Hello, \\", - " world!\")", - " console.log(\"Hello, \\", - " world!\")", - " ```" + "```js", + "\"use strict\"", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -787,27 +526,20 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("underindented multiline autofix", async () => { + it("in blocks with extra backticks", async () => { const input = [ - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, \\", - " world!')", - " console.log('Hello, world!')", - " ```" + "This is Markdown.", + "", + "````js", + "console.log('Hello, world!')", + "````" ].join("\n"); - - // The Markdown parser doesn't have any concept of a "negative" - // indent left of the opening code fence, so autofixes move - // lines that were previously underindented to the same level - // as the opening code fence. const expected = [ - " ```js", - " console.log(\"Hello, world!\")", - " console.log(\"Hello, \\", - " world!\")", - " console.log(\"Hello, world!\")", - " ```" + "This is Markdown.", + "", + "````js", + "console.log(\"Hello, world!\")", + "````" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -815,26 +547,20 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("multiline autofix in blockquote", async () => { + it("with configuration comments", async () => { const input = [ - "This is Markdown.", + "", "", - "> ```js", - "> console.log('Hello, \\", - "> world!')", - "> console.log('Hello, \\", - "> world!')", - "> ```" + "```js", + "console.log('Hello, world!')", + "```" ].join("\n"); const expected = [ - "This is Markdown.", + "", "", - "> ```js", - "> console.log(\"Hello, \\", - "> world!\")", - "> console.log(\"Hello, \\", - "> world!\")", - "> ```" + "```js", + "console.log(\"Hello, world!\");", + "```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -842,37 +568,20 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("multiline autofix in nested blockquote", async () => { + it("inside a list single line", async () => { const input = [ - "This is Markdown.", + "- Inside a list", "", - "> This is a nested blockquote.", - ">", - "> > ```js", - "> > console.log('Hello, \\", - "> > new\\", - "> > world!')", - "> > console.log('Hello, \\", - "> > world!')", - "> > ```" + " ```js", + " console.log('Hello, world!')", + " ```" ].join("\n"); - - // The Markdown parser doesn't have any concept of a "negative" - // indent left of the opening code fence, so autofixes move - // lines that were previously underindented to the same level - // as the opening code fence. const expected = [ - "This is Markdown.", + "- Inside a list", "", - "> This is a nested blockquote.", - ">", - "> > ```js", - "> > console.log(\"Hello, \\", - "> > new\\", - "> > world!\")", - "> > console.log(\"Hello, \\", - "> > world!\")", - "> > ```" + " ```js", + " console.log(\"Hello, world!\")", + " ```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -880,28 +589,30 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("by one space with comments", async () => { + it("inside a list multi line", async () => { const input = [ - "This is Markdown.", + "- Inside a list", "", - "", - "", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ", + " var obj = {", + " hello: 'value'", + " }", + " ```" ].join("\n"); const expected = [ - "This is Markdown.", - "", - "", - "", + "- Inside a list", "", - " ```js", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " ```" + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ", + " var obj = {", + " hello: \"value\"", + " }", + " ```" ].join("\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; @@ -909,41 +620,61 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("unevenly by two spaces with comments", async () => { + it("with multiline autofix and CRLF", async () => { const input = [ "This is Markdown.", "", - "", - "", - "", - " ```js", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " console.log('Hello, world!')", - " ```" - ].join("\n"); + "```js", + "console.log('Hello, \\", + "world!')", + "console.log('Hello, \\", + "world!')", + "```" + ].join("\r\n"); const expected = [ "This is Markdown.", "", - "", - "", - "", - " ```js", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " console.log(\"Hello, world!\");", - " ```" - ].join("\n"); + "```js", + "console.log(\"Hello, \\", + "world!\")", + "console.log(\"Hello, \\", + "world!\")", + "```" + ].join("\r\n"); const results = await eslint.lintText(input, { filePath: "test.md" }); const actual = results[0].output; assert.strictEqual(actual, expected); }); - describe("inside a list", () => { - it("normally", async () => { + // https://spec.commonmark.org/0.28/#fenced-code-blocks + describe("when indented", () => { + it("by one space", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by two spaces", async () => { const input = [ - "- This is a Markdown list.", + "This is Markdown.", "", " ```js", " console.log('Hello, world!')", @@ -951,7 +682,7 @@ describe("plugin", () => { " ```" ].join("\n"); const expected = [ - "- This is a Markdown list.", + "This is Markdown.", "", " ```js", " console.log(\"Hello, world!\")", @@ -964,9 +695,9 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); - it("by one space", async () => { + it("by three spaces", async () => { const input = [ - "- This is a Markdown list.", + "This is Markdown.", "", " ```js", " console.log('Hello, world!')", @@ -974,7 +705,7 @@ describe("plugin", () => { " ```" ].join("\n"); const expected = [ - "- This is a Markdown list.", + "This is Markdown.", "", " ```js", " console.log(\"Hello, world!\")", @@ -986,50 +717,1314 @@ describe("plugin", () => { assert.strictEqual(actual, expected); }); + + it("and the closing fence is differently indented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, \\", + " world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, \\", + " world!\")", + " console.log(\"Hello, \\", + " world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented multiline autofix", async () => { + const input = [ + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, \\", + " world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> ```js", + "> console.log('Hello, \\", + "> world!')", + "> console.log('Hello, \\", + "> world!')", + "> ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "> ```js", + "> console.log(\"Hello, \\", + "> world!\")", + "> console.log(\"Hello, \\", + "> world!\")", + "> ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in nested blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + "> > console.log('Hello, \\", + "> > new\\", + "> > world!')", + "> > console.log('Hello, \\", + "> > world!')", + "> > ```" + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + "> > console.log(\"Hello, \\", + "> > new\\", + "> > world!\")", + "> > console.log(\"Hello, \\", + "> > world!\")", + "> > ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log(\"Hello, world!\");", + " console.log(\"Hello, world!\");", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("unevenly by two spaces with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log(\"Hello, world!\");", + " console.log(\"Hello, world!\");", + " console.log(\"Hello, world!\");", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + describe("inside a list", () => { + it("normally", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + }); + }); + + it("with multiple rules", async () => { + const input = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + " some: 'value'", + "}", + "", + "console.log('opop');", + "", + "function hello() {", + " return false", + "};", + "```" + ].join("\n"); + const expected = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + " some: \"value\"", + "};", + "", + "console.log(\"opop\");", + "", + "function hello() {", + " return false;", + "};", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + }); + + }); + +}); + + +describe("FlatESLint", () => { + + + describe("recommended config", () => { + let eslint; + const shortText = [ + "```js", + "var unusedVar = console.log(undef);", + "'unused expression';", + "```" + ].join("\n"); + + before(function() { + try { + + // The tests for the recommended config will have ESLint import + // the plugin, so we need to make sure it's resolvable and link it + // if not. + + + require.resolve("eslint-plugin-markdown"); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + + // The npm link step can take longer than Mocha's default 2s + // timeout, so give it more time. Mocha's API for customizing + // hook-level timeouts uses `this`, so disable the rule. + // https://mochajs.org/#hook-level + // eslint-disable-next-line no-invalid-this -- need more time + this.timeout(30000); + + execSync("npm link && npm link eslint-plugin-markdown --legacy-peer-deps"); + } else { + throw error; + } + } + + eslint = initFlatESLint("recommended.js"); + }); + + it("should include the plugin", async () => { + const config = await eslint.calculateConfigForFile("test.md"); + + assert.isDefined(config.plugins.markdown); + }); + + it("applies convenience configuration", async () => { + const config = await eslint.calculateConfigForFile("subdir/test.md/0.js"); + + assert.deepStrictEqual(config.languageOptions.parserOptions, { + ecmaFeatures: { + impliedStrict: true + } }); + assert.deepStrictEqual(config.rules["eol-last"], [0]); + assert.deepStrictEqual(config.rules["no-undef"], [0]); + assert.deepStrictEqual(config.rules["no-unused-expressions"], [0]); + assert.deepStrictEqual(config.rules["no-unused-vars"], [0]); + assert.deepStrictEqual(config.rules["padded-blocks"], [0]); + assert.deepStrictEqual(config.rules.strict, [0]); + assert.deepStrictEqual(config.rules["unicode-bom"], [0]); + }); + + it("overrides configure processor to parse .md file code blocks", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + }); + + }); + + describe("plugin", () => { + let eslint; + const shortText = [ + "```js", + "console.log(42);", + "```" + ].join("\n"); + + before(() => { + eslint = initFlatESLint("eslint.config.js"); + }); + + it("should run on .md files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - it("with multiple rules", async () => { - const input = [ - "## Hello!", + it("should emit correct line numbers", async () => { + const code = [ + "# Hello, world!", "", - "", "", "```js", - "var obj = {", - " some: 'value'", - "}", + "var bar = baz", + "", + "", + "var foo = blah", + "```" + ].join("\n"); + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); + assert.strictEqual(results[0].messages[0].line, 5); + assert.strictEqual(results[0].messages[0].endLine, 5); + assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); + assert.strictEqual(results[0].messages[1].line, 8); + assert.strictEqual(results[0].messages[1].endLine, 8); + }); + + // https://github.com/eslint/eslint-plugin-markdown/issues/77 + it("should emit correct line numbers with leading blank line", async () => { + const code = [ + "### Heading", "", - "console.log('opop');", + "```js", "", - "function hello() {", - " return false", - "};", + "console.log('a')", + "```" + ].join("\n"); + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results[0].messages[0].line, 5); + }); + + it("doesn't add end locations to messages without them", async () => { + const code = [ + "```js", + "!@#$%^&*()", "```" ].join("\n"); - const expected = [ - "## Hello!", + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.notProperty(results[0].messages[0], "endLine"); + assert.notProperty(results[0].messages[0], "endColumn"); + }); + + it("should emit correct line numbers with leading comments", async () => { + const code = [ + "# Hello, world!", "", - "", + "", + "", "", "```js", - "var obj = {", - " some: \"value\"", - "};", + "var bar = baz", "", - "console.log(\"opop\");", + "var str = 'single quotes'", "", - "function hello() {", - " return false;", - "};", + "var foo = blah", "```" ].join("\n"); - const results = await eslint.lintText(input, { filePath: "test.md" }); - const actual = results[0].output; + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results[0].messages[0].message, "'baz' is not defined."); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[0].endLine, 7); + assert.strictEqual(results[0].messages[1].message, "'blah' is not defined."); + assert.strictEqual(results[0].messages[1].line, 11); + assert.strictEqual(results[0].messages[1].endLine, 11); + }); - assert.strictEqual(actual, expected); + it("should run on .mkdn files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.mkdn" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); }); - }); + it("should run on .mdown files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.mdown" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on .markdown files", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.markdown" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should run on files with any custom extension", async () => { + const results = await eslint.lintText(shortText, { filePath: "test.custom" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should extract blocks and remap messages", async () => { + const results = await eslint.lintFiles([path.resolve(__dirname, "../fixtures/long.md")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual(results[0].messages[0].column, 1); + assert.strictEqual(results[0].messages[1].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[1].line, 16); + assert.strictEqual(results[0].messages[1].column, 5); + assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[2].line, 24); + assert.strictEqual(results[0].messages[2].column, 1); + assert.strictEqual(results[0].messages[3].message, "Strings must use singlequote."); + assert.strictEqual(results[0].messages[3].line, 38); + assert.strictEqual(results[0].messages[3].column, 13); + assert.strictEqual(results[0].messages[4].message, "Parsing error: Unexpected character '@'"); + assert.strictEqual(results[0].messages[4].line, 46); + assert.strictEqual(results[0].messages[4].column, 2); + }); + + // https://github.com/eslint/eslint-plugin-markdown/issues/181 + it("should work when called on nested code blocks in the same file", async () => { + + /* + * As of this writing, the nested code block, though it uses the same + * Markdown processor, must use a different extension or ESLint will not + * re-apply the processor on the nested code block. To work around that, + * a file named `test.md` contains a nested `markdown` code block in + * this test. + * + * https://github.com/eslint/eslint/pull/14227/files#r602802758 + */ + const code = [ + "", + "", + "````markdown", + "", + "", + "This test only repros if the MD files have a different number of lines before code blocks.", + "", + "```js", + "// test.md/0_0.markdown/0_0.js", + "console.log('single quotes')", + "```", + "````" + ].join("\n"); + const recursiveCli = initLegacyESLint("eslintrc.json", { + extensions: [".js", ".markdown", ".md"] + }); + const results = await recursiveCli.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[0].line, 10); + assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); + assert.strictEqual(results[0].messages[1].line, 10); + }); + + describe("configuration comments", () => { + it("apply only to the code block immediately following", async () => { + const code = [ + "", + "", + "", + "```js", + "var single = 'single';", + "console.log(single);", + "var double = \"double\";", + "console.log(double);", + "```", + "", + "```js", + "var single = 'single';", + "console.log(single);", + "var double = \"double\";", + "console.log(double);", + "```" + ].join("\n"); + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual(results[0].messages[0].message, "Strings must use singlequote."); + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[1].message, "Strings must use doublequote."); + assert.strictEqual(results[0].messages[1].line, 12); + assert.strictEqual(results[0].messages[2].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[2].line, 13); + assert.strictEqual(results[0].messages[3].message, "Unexpected console statement."); + assert.strictEqual(results[0].messages[3].line, 15); + }); + + // https://github.com/eslint/eslint-plugin-markdown/issues/78 + it("preserves leading empty lines", async () => { + const code = [ + "", + "", + "```js", + "", + "\"use strict\";", + "```" + ].join("\n"); + const results = await eslint.lintText(code, { filePath: "test.md" }); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Unexpected newline before \"use strict\" directive."); + assert.strictEqual(results[0].messages[0].line, 5); + }); + }); + + describe("should fix code", () => { + before(() => { + eslint = initLegacyESLint("eslintrc.json", { fix: true }); + }); + + it("in the simplest case", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "console.log(\"Hello, world!\")", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("across multiple lines", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "console.log('Hello, world!')", + "```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "console.log(\"Hello, world!\")", + "console.log(\"Hello, world!\")", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("across multiple blocks", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, world!')", + "```", + "", + "```js", + "console.log('Hello, world!')", + "```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "console.log(\"Hello, world!\")", + "```", + "", + "```js", + "console.log(\"Hello, world!\")", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with lines indented by spaces", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "function test() {", + " console.log('Hello, world!')", + "}", + "```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "function test() {", + " console.log(\"Hello, world!\")", + "}", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with lines indented by tabs", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "function test() {", + "\tconsole.log('Hello, world!')", + "}", + "```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "function test() {", + "\tconsole.log(\"Hello, world!\")", + "}", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("at the very start of a block", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "'use strict'", + "```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "\"use strict\"", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("in blocks with extra backticks", async () => { + const input = [ + "This is Markdown.", + "", + "````js", + "console.log('Hello, world!')", + "````" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "````js", + "console.log(\"Hello, world!\")", + "````" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with configuration comments", async () => { + const input = [ + "", + "", + "```js", + "console.log('Hello, world!')", + "```" + ].join("\n"); + const expected = [ + "", + "", + "```js", + "console.log(\"Hello, world!\");", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("inside a list single line", async () => { + const input = [ + "- Inside a list", + "", + " ```js", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "- Inside a list", + "", + " ```js", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("inside a list multi line", async () => { + const input = [ + "- Inside a list", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ", + " var obj = {", + " hello: 'value'", + " }", + " ```" + ].join("\n"); + const expected = [ + "- Inside a list", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ", + " var obj = {", + " hello: \"value\"", + " }", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("with multiline autofix and CRLF", async () => { + const input = [ + "This is Markdown.", + "", + "```js", + "console.log('Hello, \\", + "world!')", + "console.log('Hello, \\", + "world!')", + "```" + ].join("\r\n"); + const expected = [ + "This is Markdown.", + "", + "```js", + "console.log(\"Hello, \\", + "world!\")", + "console.log(\"Hello, \\", + "world!\")", + "```" + ].join("\r\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + // https://spec.commonmark.org/0.28/#fenced-code-blocks + describe("when indented", () => { + it("by one space", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by two spaces", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by three spaces", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("and the closing fence is differently indented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix", async () => { + const input = [ + "This is Markdown.", + "", + " ```js", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, \\", + " world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + " ```js", + " console.log(\"Hello, \\", + " world!\")", + " console.log(\"Hello, \\", + " world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("underindented multiline autofix", async () => { + const input = [ + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, \\", + " world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, \\", + " world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> ```js", + "> console.log('Hello, \\", + "> world!')", + "> console.log('Hello, \\", + "> world!')", + "> ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "> ```js", + "> console.log(\"Hello, \\", + "> world!\")", + "> console.log(\"Hello, \\", + "> world!\")", + "> ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("multiline autofix in nested blockquote", async () => { + const input = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + "> > console.log('Hello, \\", + "> > new\\", + "> > world!')", + "> > console.log('Hello, \\", + "> > world!')", + "> > ```" + ].join("\n"); + + // The Markdown parser doesn't have any concept of a "negative" + // indent left of the opening code fence, so autofixes move + // lines that were previously underindented to the same level + // as the opening code fence. + const expected = [ + "This is Markdown.", + "", + "> This is a nested blockquote.", + ">", + "> > ```js", + "> > console.log(\"Hello, \\", + "> > new\\", + "> > world!\")", + "> > console.log(\"Hello, \\", + "> > world!\")", + "> > ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log(\"Hello, world!\");", + " console.log(\"Hello, world!\");", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("unevenly by two spaces with comments", async () => { + const input = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "This is Markdown.", + "", + "", + "", + "", + " ```js", + " console.log(\"Hello, world!\");", + " console.log(\"Hello, world!\");", + " console.log(\"Hello, world!\");", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + describe("inside a list", () => { + it("normally", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + it("by one space", async () => { + const input = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log('Hello, world!')", + " console.log('Hello, world!')", + " ```" + ].join("\n"); + const expected = [ + "- This is a Markdown list.", + "", + " ```js", + " console.log(\"Hello, world!\")", + " console.log(\"Hello, world!\")", + " ```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + }); + }); + + it("with multiple rules", async () => { + const input = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + " some: 'value'", + "}", + "", + "console.log('opop');", + "", + "function hello() {", + " return false", + "};", + "```" + ].join("\n"); + const expected = [ + "## Hello!", + "", + "", + "", + "```js", + "var obj = {", + " some: \"value\"", + "};", + "", + "console.log(\"opop\");", + "", + "function hello() {", + " return false;", + "};", + "```" + ].join("\n"); + const results = await eslint.lintText(input, { filePath: "test.md" }); + const actual = results[0].output; + + assert.strictEqual(actual, expected); + }); + + }); + + }); + }); From e5f793e6f9eb68497a827184359ae9af4464d4bc Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 1 Feb 2024 12:51:04 -0500 Subject: [PATCH 02/21] Fix linting errors --- .github/workflows/ci.yml | 2 +- README.md | 2 +- eslint.config.js | 3 ++- tests/lib/plugin.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 828ba06a..3132584a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: lts/* - name: Install Packages run: npm install env: diff --git a/README.md b/README.md index b1d34d09..fa17bb21 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ npm install --save-dev eslint eslint-plugin-markdown In your `eslint.config.js` file, import `eslint-plugin-markdown` and included the recommended config to enable the Markdown processor on all `.md` files: ```js -// / eslint.config.js +// eslint.config.js import markdown from "eslint-plugin-markdown"; export default [ diff --git a/eslint.config.js b/eslint.config.js index f9103b15..9758d80b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -46,7 +46,8 @@ module.exports = [ } }, rules: { - "lines-around-comment": "off" + "lines-around-comment": "off", + "n/no-missing-import": "off" } } ]; diff --git a/tests/lib/plugin.js b/tests/lib/plugin.js index 6818d14e..296fb2a7 100644 --- a/tests/lib/plugin.js +++ b/tests/lib/plugin.js @@ -76,7 +76,7 @@ describe("LegacyESLint", () => { // the plugin, so we need to make sure it's resolvable and link it // if not. - + // eslint-disable-next-line n/no-missing-require -- Known possible failure. require.resolve("eslint-plugin-markdown"); } catch (error) { if (error.code === "MODULE_NOT_FOUND") { From 7136ea3218bd783f2521d4fbb1963ec107553be1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 1 Feb 2024 12:53:42 -0500 Subject: [PATCH 03/21] Update eslint version --- examples/react/package.json | 2 +- examples/typescript/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/react/package.json b/examples/react/package.json index c3107010..ab427b72 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -6,7 +6,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.0.0", "@eslint/js": "^8.56.0", - "eslint": "^7.5.0", + "eslint": "^8.56.0", "eslint-plugin-markdown": "file:../..", "eslint-plugin-react": "^7.20.3", "globals": "^13.24.0" diff --git a/examples/typescript/package.json b/examples/typescript/package.json index dd20a1f0..2f9baddb 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -8,7 +8,7 @@ "@eslint/js": "^8.56.0", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", - "eslint": "^7.5.0", + "eslint": "^8.56.0", "eslint-plugin-markdown": "file:../..", "typescript": "^3.9.7" } From 630a191dd72d81df22a430eb84e28f3c93cf9208 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 1 Feb 2024 12:56:12 -0500 Subject: [PATCH 04/21] Fix lint error --- tests/lib/plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/plugin.js b/tests/lib/plugin.js index 296fb2a7..3ea7640f 100644 --- a/tests/lib/plugin.js +++ b/tests/lib/plugin.js @@ -1062,7 +1062,7 @@ describe("FlatESLint", () => { // the plugin, so we need to make sure it's resolvable and link it // if not. - + // eslint-disable-next-line n/no-missing-require -- Known possible failure. require.resolve("eslint-plugin-markdown"); } catch (error) { if (error.code === "MODULE_NOT_FOUND") { From d908c510003d4740b68f4493bc4ed4ae77c03f7d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 1 Feb 2024 12:58:43 -0500 Subject: [PATCH 05/21] Update ESLint CI installation --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3132584a..d47e1121 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,14 +29,14 @@ jobs: strategy: matrix: os: [ubuntu-latest] - eslint: [6, 7, 8] + eslint: [8] node: [12.22.0, 14, 16, 17, 18, 19, 20, 21] include: - os: windows-latest - eslint: 7 + eslint: 8 node: 16 - os: macOS-latest - eslint: 7 + eslint: 8 node: 16 runs-on: ${{ matrix.os }} steps: From c4e5bd3de488f4077c06eb7c7815ba6df8a35196 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 1 Feb 2024 13:01:31 -0500 Subject: [PATCH 06/21] Omit Node.js 12 from CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d47e1121..a56da905 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: matrix: os: [ubuntu-latest] eslint: [8] - node: [12.22.0, 14, 16, 17, 18, 19, 20, 21] + node: [14, 16, 17, 18, 19, 20, 21] include: - os: windows-latest eslint: 8 From 2343d98288f478b7baed532a05ddd19be92a87c1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 1 Feb 2024 13:14:04 -0500 Subject: [PATCH 07/21] Remove Node.js 14 from CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a56da905..eb6db487 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: matrix: os: [ubuntu-latest] eslint: [8] - node: [14, 16, 17, 18, 19, 20, 21] + node: [16, 17, 18, 19, 20, 21] include: - os: windows-latest eslint: 8 From de0a5d0e3d0effa1b89dbe9daafd3ea12e061aa6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 5 Feb 2024 13:12:22 -0500 Subject: [PATCH 08/21] Update README.md Co-authored-by: Milos Djermanovic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa17bb21..dc88968f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ npm install --save-dev eslint eslint-plugin-markdown ### Configuring -In your `eslint.config.js` file, import `eslint-plugin-markdown` and included the recommended config to enable the Markdown processor on all `.md` files: +In your `eslint.config.js` file, import `eslint-plugin-markdown` and include the recommended config to enable the Markdown processor on all `.md` files: ```js // eslint.config.js From 3cc79eff5d467a93491f1fd4455d37c9d33b079c Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 5 Feb 2024 13:13:56 -0500 Subject: [PATCH 09/21] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 唯然 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc88968f..915b4445 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ export default [ ]; ``` -If you are still using the deprecated `.eslintrc.js` file format for ESLint, you can extend the `plugin:markdown/recommended` config to enable the Markdown processor on all `.md` files: +If you are still using the deprecated `.eslintrc.js` file format for ESLint, you can extend the `plugin:markdown/recommended-legacy` config to enable the Markdown processor on all `.md` files: ```js // .eslintrc.js From 9449cc593623bcd370d58b26aadb5f94390b5072 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 5 Feb 2024 13:16:46 -0500 Subject: [PATCH 10/21] Clean up README --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 915b4445..e43af1c3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ If you are still using the deprecated `.eslintrc.js` file format for ESLint, you ```js // .eslintrc.js module.exports = { - extends: "plugin:markdown/recommended" + extends: "plugin:markdown/recommended-legacy" }; ``` @@ -196,7 +196,13 @@ The `plugin:markdown/recommended` config disables these rules in Markdown files: ### Running -#### ESLint v7+ +#### ESLint v8+ + +If you are using an `eslint.config.js` file, then you can run ESLint as usual and it will pick up file patterns in your config file. The `--ext` option is not available when using flat config. + +If you are using an `.eslintrc.*` file, then you can run ESLint as usual and it will work as in ESLint v7.x. + +#### ESLint v7 You can run ESLint as usual and do not need to use the `--ext` option. ESLint v7 [automatically lints file extensions specified in `overrides[].files` patterns in config files](https://github.com/eslint/rfcs/blob/0253e3a95511c65d622eaa387eb73f824249b467/designs/2019-additional-lint-targets/README.md). From 4c6094d280626d469ee0332d1d5dbaed393160f7 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:42:18 -0500 Subject: [PATCH 11/21] Update README.md Co-authored-by: Milos Djermanovic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e43af1c3..5db0da92 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ For more information on configuring processors, refer to the [ESLint documentati Here's an example: ```js -// / eslint.config.js +// eslint.config.js import markdown from "eslint-plugin-markdown"; export default [ From 0d2271316c405b67abd66018669ec18974be7d48 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:42:26 -0500 Subject: [PATCH 12/21] Update README.md Co-authored-by: Milos Djermanovic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5db0da92..7d85ca4f 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ The processor will convert HTML comments immediately preceding a code block into This permits configuring ESLint via configuration comments while keeping the configuration comments themselves hidden when the markdown is rendered. Comment bodies are passed through unmodified, so the plugin supports any [configuration comments](http://eslint.org/docs/user-guide/configuring) supported by ESLint itself. -This example enables the `browser` environment, disables the `no-alert` rule, and configures the `quotes` rule to prefer single quotes: +This example enables the `alert` global variable, disables the `no-alert` rule, and configures the `quotes` rule to prefer single quotes: ````markdown From 25b7f44a474b684728b9d4ac5842d4ce8745c548 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:42:43 -0500 Subject: [PATCH 13/21] Update README.md Co-authored-by: Milos Djermanovic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d85ca4f..80ac9c31 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ alert('Hello, world!'); Each code block in a file is linted separately, so configuration comments apply only to the code block that immediately follows. ````markdown -Assuming `no-alert` is enabled in `.eslintrc`, the first code block will have no error from `no-alert`: +Assuming `no-alert` is enabled in `eslint.config.js`, the first code block will have no error from `no-alert`: From 5c097b156f97ebb68a42de11adbf66502a61497a Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:42:54 -0500 Subject: [PATCH 14/21] Update package.json Co-authored-by: Milos Djermanovic --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 60a97f41..40a71476 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "lib/processor.js" ], "devDependencies": { - "@eslint/eslintrc": "^3.0.0", "@eslint/js": "^8.56.0", "chai": "^4.2.0", "eslint": "^8.56.0", From 7cc35778d1ae141d8007d455ab0ca4bf04fd85b4 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:43:03 -0500 Subject: [PATCH 15/21] Update package.json Co-authored-by: Milos Djermanovic --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 40a71476..f1156e99 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,6 @@ "chai": "^4.2.0", "eslint": "^8.56.0", "eslint-config-eslint": "^9.0.0", - "eslint-plugin-jsdoc": "^37.0.3", - "eslint-plugin-node": "^11.1.0", "eslint-release": "^3.1.2", "globals": "^13.24.0", "mocha": "^6.2.2", From 593601f268d543d730d9ec5e461ed3d2ff768039 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:43:23 -0500 Subject: [PATCH 16/21] Update eslint.config.js Co-authored-by: Milos Djermanovic --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 9758d80b..50cdcc67 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,7 @@ "use strict"; module.exports = [ - ...require("eslint-config-eslint").map(config => ({ + ...require("eslint-config-eslint/cjs").map(config => ({ ...config, files: ["**/*.js"] })), From 0d472d6d030d87f31bcf552b597c26ce9313b3c3 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 13:43:37 -0500 Subject: [PATCH 17/21] Update eslint.config.js Co-authored-by: Milos Djermanovic --- eslint.config.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 50cdcc67..dc073903 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,12 +17,6 @@ module.exports = [ "**/tests/fixtures" ] }, - { - files: ["**/*.js"], - languageOptions: { - sourceType: "commonjs" - } - }, { files: ["tests/**/*.js"], languageOptions: { From 4515554055277ac7d350a2a02357b1749a8bc1e6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 15:16:37 -0500 Subject: [PATCH 18/21] Update examples/react/eslint.config.js Co-authored-by: Milos Djermanovic --- examples/react/eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react/eslint.config.js b/examples/react/eslint.config.js index ee4245b3..5458bbbc 100644 --- a/examples/react/eslint.config.js +++ b/examples/react/eslint.config.js @@ -1,7 +1,7 @@ "use strict"; const { FlatCompat } = require("@eslint/eslintrc"); -const markdown = require("../.."); +const markdown = require("eslint-plugin-markdown"); const js = require("@eslint/js"); const globals = require("globals"); From 539f4133d3b88db92e3cc89ee58fb9106d8dee33 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 15:16:47 -0500 Subject: [PATCH 19/21] Update examples/typescript/eslint.config.js Co-authored-by: Milos Djermanovic --- examples/typescript/eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/typescript/eslint.config.js b/examples/typescript/eslint.config.js index 6ee9381a..00362358 100644 --- a/examples/typescript/eslint.config.js +++ b/examples/typescript/eslint.config.js @@ -1,6 +1,6 @@ "use strict"; -const markdown = require("../.."); +const markdown = require("eslint-plugin-markdown"); const js = require("@eslint/js") const { FlatCompat } = require("@eslint/eslintrc"); From ce3a4470238249e449f2c1619d5a6f11c95c30e1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 12 Feb 2024 16:33:25 -0500 Subject: [PATCH 20/21] Apply feedback --- examples/react/eslint.config.js | 2 +- tests/lib/plugin.js | 53 ++------------------------------- 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/examples/react/eslint.config.js b/examples/react/eslint.config.js index 5458bbbc..0840eb26 100644 --- a/examples/react/eslint.config.js +++ b/examples/react/eslint.config.js @@ -12,7 +12,7 @@ const compat = new FlatCompat({ module.exports = [ js.configs.recommended, ...markdown.configs.recommended, - ...compat.extends("plugin:react/recommended"), + require("eslint-plugin-react/configs/recommended"), { settings: { react: { diff --git a/tests/lib/plugin.js b/tests/lib/plugin.js index 3ea7640f..cb71c670 100644 --- a/tests/lib/plugin.js +++ b/tests/lib/plugin.js @@ -47,7 +47,6 @@ function initFlatESLint(fixtureConfigName, options = {}) { cwd: path.resolve(__dirname, "../fixtures/"), ignore: false, overrideConfigFile: path.resolve(__dirname, "../fixtures/", fixtureConfigName), - plugins: { markdown: plugin }, ...options }); } @@ -69,31 +68,7 @@ describe("LegacyESLint", () => { "```" ].join("\n"); - before(function() { - try { - - // The tests for the recommended config will have ESLint import - // the plugin, so we need to make sure it's resolvable and link it - // if not. - - // eslint-disable-next-line n/no-missing-require -- Known possible failure. - require.resolve("eslint-plugin-markdown"); - } catch (error) { - if (error.code === "MODULE_NOT_FOUND") { - - // The npm link step can take longer than Mocha's default 2s - // timeout, so give it more time. Mocha's API for customizing - // hook-level timeouts uses `this`, so disable the rule. - // https://mochajs.org/#hook-level - // eslint-disable-next-line no-invalid-this -- need more time - this.timeout(30000); - - execSync("npm link && npm link eslint-plugin-markdown --legacy-peer-deps"); - } else { - throw error; - } - } - + before(() => { eslint = initLegacyESLint("recommended.json"); }); @@ -1055,31 +1030,7 @@ describe("FlatESLint", () => { "```" ].join("\n"); - before(function() { - try { - - // The tests for the recommended config will have ESLint import - // the plugin, so we need to make sure it's resolvable and link it - // if not. - - // eslint-disable-next-line n/no-missing-require -- Known possible failure. - require.resolve("eslint-plugin-markdown"); - } catch (error) { - if (error.code === "MODULE_NOT_FOUND") { - - // The npm link step can take longer than Mocha's default 2s - // timeout, so give it more time. Mocha's API for customizing - // hook-level timeouts uses `this`, so disable the rule. - // https://mochajs.org/#hook-level - // eslint-disable-next-line no-invalid-this -- need more time - this.timeout(30000); - - execSync("npm link && npm link eslint-plugin-markdown --legacy-peer-deps"); - } else { - throw error; - } - } - + before(() => { eslint = initFlatESLint("recommended.js"); }); From 47dc905dce539fa824dee22507c3921d3215b06f Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 14 Feb 2024 13:19:19 -0500 Subject: [PATCH 21/21] Update tests/lib/plugin.js Co-authored-by: Milos Djermanovic --- tests/lib/plugin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/lib/plugin.js b/tests/lib/plugin.js index cb71c670..443a318e 100644 --- a/tests/lib/plugin.js +++ b/tests/lib/plugin.js @@ -10,7 +10,6 @@ //----------------------------------------------------------------------------- const assert = require("chai").assert; -const execSync = require("child_process").execSync; const { LegacyESLint, FlatESLint } = require("eslint/use-at-your-own-risk"); const path = require("path"); const plugin = require("../..");