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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 828ba06a..eb6db487 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: @@ -29,14 +29,14 @@ jobs: strategy: matrix: os: [ubuntu-latest] - eslint: [6, 7, 8] - node: [12.22.0, 14, 16, 17, 18, 19, 20, 21] + eslint: [8] + node: [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: diff --git a/README.md b/README.md index 58dda721..80ac9c31 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,23 +25,72 @@ 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 include 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-legacy` config to enable the Markdown processor on all `.md` files: ```js // .eslintrc.js module.exports = { - extends: "plugin:markdown/recommended" + extends: "plugin:markdown/recommended-legacy" }; ``` #### 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,97 +194,13 @@ 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" - } - } - ] -}; -``` +### Running -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`: +#### ESLint v8+ -```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}"] - // ... - } - ] -}; -``` +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. -### Running +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 @@ -279,10 +278,10 @@ 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 - + @@ -294,9 +293,9 @@ 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`: - + ```js @@ -305,7 +304,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..dc073903 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,47 @@ +"use strict"; + +module.exports = [ + ...require("eslint-config-eslint/cjs").map(config => ({ + ...config, + files: ["**/*.js"] + })), + { + plugins: { + markdown: require(".") + } + }, + { + ignores: [ + "**/examples", + "**/coverage", + "**/tests/fixtures" + ] + }, + { + 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", + "n/no-missing-import": "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..0840eb26 --- /dev/null +++ b/examples/react/eslint.config.js @@ -0,0 +1,47 @@ +"use strict"; + +const { FlatCompat } = require("@eslint/eslintrc"); +const markdown = require("eslint-plugin-markdown"); +const js = require("@eslint/js"); +const globals = require("globals"); + +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +module.exports = [ + js.configs.recommended, + ...markdown.configs.recommended, + require("eslint-plugin-react/configs/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..ab427b72 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -4,8 +4,11 @@ "test": "eslint ." }, "devDependencies": { - "eslint": "^7.5.0", + "@eslint/eslintrc": "^3.0.0", + "@eslint/js": "^8.56.0", + "eslint": "^8.56.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..00362358 --- /dev/null +++ b/examples/typescript/eslint.config.js @@ -0,0 +1,27 @@ +"use strict"; + +const markdown = require("eslint-plugin-markdown"); +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..2f9baddb 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -4,9 +4,11 @@ "test": "eslint ." }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^3.6.1", - "@typescript-eslint/parser": "^3.6.1", - "eslint": "^7.5.0", + "@eslint/eslintrc": "^3.0.0", + "@eslint/js": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "eslint": "^8.56.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..f1156e99 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,12 @@ "lib/processor.js" ], "devDependencies": { + "@eslint/js": "^8.56.0", "chai": "^4.2.0", - "eslint": "^7.32.0", - "eslint-config-eslint": "^7.0.0", - "eslint-plugin-jsdoc": "^37.0.3", - "eslint-plugin-node": "^11.1.0", + "eslint": "^8.56.0", + "eslint-config-eslint": "^9.0.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..443a318e 100644 --- a/tests/lib/plugin.js +++ b/tests/lib/plugin.js @@ -5,660 +5,371 @@ "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"); - }); - -}); - -describe("plugin", () => { - let eslint; - const shortText = [ - "```js", - "console.log(42);", - "```" - ].join("\n"); - - before(() => { - eslint = initESLint("eslintrc.json"); - }); - - 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("should emit correct line numbers", async () => { - const code = [ - "# Hello, world!", - "", - "", - "```js", - "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); +/** + * 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), + ...options }); +} - // 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" }); - assert.strictEqual(results[0].messages[0].line, 5); - }); +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- - 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" }); +describe("LegacyESLint", () => { - 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!", - "", - "", - "", - "", + describe("recommended config", () => { + let eslint; + const shortText = [ "```js", - "var bar = baz", - "", - "var str = 'single quotes'", - "", - "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, 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("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); - }); + before(() => { + 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); + }); + + // 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(actual, expected); + 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 +377,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 +400,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 +429,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 +454,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 +479,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 +500,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 +521,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 +542,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 +563,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 +594,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!')", + "```" + ].join("\r\n"); + const expected = [ + "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"); + "```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 +656,7 @@ describe("plugin", () => { " ```" ].join("\n"); const expected = [ - "- This is a Markdown list.", + "This is Markdown.", "", " ```js", " console.log(\"Hello, world!\")", @@ -964,9 +669,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 +679,7 @@ describe("plugin", () => { " ```" ].join("\n"); const expected = [ - "- This is a Markdown list.", + "This is Markdown.", "", " ```js", " console.log(\"Hello, world!\")", @@ -986,50 +691,1290 @@ 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(() => { + 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("with multiple rules", async () => { - const input = [ - "## Hello!", + 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("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(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("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); + }); + + }); + + }); + });