From 212d201ed2d551d9ba55cee265132a3070fb32f7 Mon Sep 17 00:00:00 2001 From: Spencer Snyder Date: Wed, 20 Jul 2022 17:29:51 -0400 Subject: [PATCH] feat: include rulesMeta by default for all linting --- cli.js | 2 +- index.js | 9 +++- lib/report.js | 4 +- test/lint-files.js | 24 +++++++--- test/lint-text.js | 111 ++++++++++++++++++++++++--------------------- 5 files changed, 89 insertions(+), 61 deletions(-) diff --git a/cli.js b/cli.js index a12b8d0c..1284a2dc 100755 --- a/cli.js +++ b/cli.js @@ -149,7 +149,7 @@ if (process.env.GITHUB_ACTIONS && !options.fix && !options.reporter) { const log = async report => { const reporter = options.reporter || process.env.GITHUB_ACTIONS ? await xo.getFormatter(options.reporter || 'compact') : formatterPretty; - process.stdout.write(reporter(report.results)); + process.stdout.write(reporter(report.results, {rulesMeta: report.rulesMeta})); process.exitCode = report.errorCount === 0 ? 0 : 1; }; diff --git a/index.js b/index.js index 142ff668..44e77b22 100644 --- a/index.js +++ b/index.js @@ -66,7 +66,10 @@ const lintText = async (string, options) => { } const report = await eslint.lintText(string, {filePath, warnIgnored}); - return processReport(report, {isQuiet}); + + const rulesMeta = eslint.getRulesMetaForResults(report); + + return processReport(report, {isQuiet, rulesMeta}); }; const lintFiles = async (patterns, options) => { @@ -102,7 +105,9 @@ const lintFiles = async (patterns, options) => { const report = await eslint.lintFiles(files); - return processReport(report, {isQuiet: options.isQuiet}); + const rulesMeta = eslint.getRulesMetaForResults(report); + + return processReport(report, {isQuiet: options.isQuiet, rulesMeta}); })); const report = mergeReports(reports); diff --git a/lib/report.js b/lib/report.js index f6daf0fa..a6faf102 100644 --- a/lib/report.js +++ b/lib/report.js @@ -13,18 +13,20 @@ const mergeReports = reports => { report.results.push(...currentReport.results); report.errorCount += currentReport.errorCount; report.warningCount += currentReport.warningCount; + report.rulesMeta = {...report.rulesMeta, ...currentReport.rulesMeta}; } return report; }; -const processReport = (report, {isQuiet = false} = {}) => { +const processReport = (report, {isQuiet = false, rulesMeta} = {}) => { if (isQuiet) { report = ESLint.getErrorResults(report); } const result = { results: report, + rulesMeta, ...getReportStatistics(report), }; diff --git a/test/lint-files.js b/test/lint-files.js index fab99a84..f1b4e455 100644 --- a/test/lint-files.js +++ b/test/lint-files.js @@ -7,9 +7,11 @@ import xo from '../index.js'; const {__dirname} = createEsmUtils(import.meta); process.chdir(__dirname); -const hasRule = (results, filePath, ruleId) => { +const hasRule = (results, filePath, ruleId, rulesMeta) => { const result = results.find(x => x.filePath === filePath); - return result ? result.messages.some(x => x.ruleId === ruleId) : false; + const hasRuleInResults = result ? result.messages.some(x => x.ruleId === ruleId) : false; + const hasRuleInResultsMeta = rulesMeta ? typeof rulesMeta[ruleId] === 'object' : true; + return hasRuleInResults && hasRuleInResultsMeta; }; test('only accepts allowed extensions', async t => { @@ -120,7 +122,7 @@ test('multiple negative patterns should act as positive patterns', async t => { }); test.failing('enable rules based on nodeVersion', async t => { - const {results} = await xo.lintFiles('**/*', {cwd: 'fixtures/engines-overrides'}); + const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: 'fixtures/engines-overrides'}); // The transpiled file (as specified in `overrides`) should use `await` t.true( @@ -128,6 +130,7 @@ test.failing('enable rules based on nodeVersion', async t => { results, path.resolve('fixtures/engines-overrides/promise-then-transpile.js'), 'promise/prefer-await-to-then', + rulesMeta, ), ); // The non transpiled files can use `.then` @@ -136,6 +139,7 @@ test.failing('enable rules based on nodeVersion', async t => { results, path.resolve('fixtures/engines-overrides/promise-then.js'), 'promise/prefer-await-to-then', + rulesMeta, ), ); }); @@ -152,13 +156,14 @@ test('do not lint eslintignored files', async t => { }); test('find configurations close to linted file', async t => { - const {results} = await xo.lintFiles('**/*', {cwd: 'fixtures/nested-configs'}); + const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: 'fixtures/nested-configs'}); t.true( hasRule( results, path.resolve('fixtures/nested-configs/child/semicolon.js'), 'semi', + rulesMeta, ), ); @@ -167,6 +172,7 @@ test('find configurations close to linted file', async t => { results, path.resolve('fixtures/nested-configs/child-override/child-prettier-override/semicolon.js'), 'prettier/prettier', + rulesMeta, ), ); @@ -175,6 +181,7 @@ test('find configurations close to linted file', async t => { results, path.resolve('fixtures/nested-configs/no-semicolon.js'), 'semi', + rulesMeta, ), ); @@ -183,18 +190,20 @@ test('find configurations close to linted file', async t => { results, path.resolve('fixtures/nested-configs/child-override/two-spaces.js'), 'indent', + rulesMeta, ), ); }); test.serial('typescript files', async t => { - const {results} = await xo.lintFiles('**/*', {cwd: 'fixtures/typescript'}); + const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: 'fixtures/typescript'}); t.true( hasRule( results, path.resolve('fixtures/typescript/two-spaces.tsx'), '@typescript-eslint/indent', + rulesMeta, ), ); @@ -203,6 +212,7 @@ test.serial('typescript files', async t => { results, path.resolve('fixtures/typescript/child/extra-semicolon.ts'), '@typescript-eslint/no-extra-semi', + rulesMeta, ), ); @@ -211,6 +221,7 @@ test.serial('typescript files', async t => { results, path.resolve('fixtures/typescript/child/sub-child/four-spaces.ts'), '@typescript-eslint/indent', + rulesMeta, ), ); }); @@ -287,13 +298,14 @@ test('webpack import resolver is used if {webpack: true}', async t => { }); async function configType(t, {dir}) { - const {results} = await xo.lintFiles('**/*', {cwd: path.resolve('fixtures', 'config-files', dir)}); + const {results, rulesMeta} = await xo.lintFiles('**/*', {cwd: path.resolve('fixtures', 'config-files', dir)}); t.true( hasRule( results, path.resolve('fixtures', 'config-files', dir, 'file.js'), 'indent', + rulesMeta, ), ); } diff --git a/test/lint-text.js b/test/lint-text.js index a4950b1a..d5e6b32a 100644 --- a/test/lint-text.js +++ b/test/lint-text.js @@ -8,11 +8,15 @@ import xo from '../index.js'; const {__dirname} = createEsmUtils(import.meta); process.chdir(__dirname); -const hasRule = (results, expectedRuleId) => results[0].messages.some(({ruleId}) => ruleId === expectedRuleId); +const hasRule = (results, expectedRuleId, rulesMeta) => { + const hasRuleInResults = results[0].messages.some(({ruleId}) => ruleId === expectedRuleId); + const hasRuleInMeta = rulesMeta ? typeof rulesMeta[expectedRuleId] === 'object' : true; + return hasRuleInResults && hasRuleInMeta; +}; test('.lintText()', async t => { - const {results} = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n'); - t.true(hasRule(results, 'semi')); + const {results, rulesMeta} = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n'); + t.true(hasRule(results, 'semi', rulesMeta)); }); test('default `ignores`', async t => { @@ -83,50 +87,50 @@ test('`ignores` option without filename', async t => { }); test('JSX support', async t => { - const {results} = await xo.lintText('const app =
Hello, React!
;\n'); - t.true(hasRule(results, 'no-unused-vars')); + const {results, rulesMeta} = await xo.lintText('const app =
Hello, React!
;\n'); + t.true(hasRule(results, 'no-unused-vars', rulesMeta)); }); test('plugin support', async t => { - const {results} = await xo.lintText('var React;\nReact.render();\n', { + const {results, rulesMeta} = await xo.lintText('var React;\nReact.render();\n', { plugins: ['react'], rules: {'react/jsx-no-undef': 'error'}, }); - t.true(hasRule(results, 'react/jsx-no-undef')); + t.true(hasRule(results, 'react/jsx-no-undef', rulesMeta)); }); test('prevent use of extended native objects', async t => { - const {results} = await xo.lintText('[].unicorn();\n'); - t.true(hasRule(results, 'no-use-extend-native/no-use-extend-native')); + const {results, rulesMeta} = await xo.lintText('[].unicorn();\n'); + t.true(hasRule(results, 'no-use-extend-native/no-use-extend-native', rulesMeta)); }); test('extends support', async t => { - const {results} = await xo.lintText('var React;\nReact.render();\n', { + const {results, rulesMeta} = await xo.lintText('var React;\nReact.render();\n', { extends: 'xo-react', }); - t.true(hasRule(results, 'react/jsx-no-undef')); + t.true(hasRule(results, 'react/jsx-no-undef', rulesMeta)); }); test('disable style rules when `prettier` option is enabled', async t => { - const {results: withoutPrettier} = await xo.lintText('(a) => {}\n', {filePath: 'test.js'}); + const {results: withoutPrettier, rulesMeta} = await xo.lintText('(a) => {}\n', {filePath: 'test.js'}); // `arrow-parens` is enabled - t.true(hasRule(withoutPrettier, 'arrow-parens')); + t.true(hasRule(withoutPrettier, 'arrow-parens', rulesMeta)); // `prettier/prettier` is disabled - t.false(hasRule(withoutPrettier, 'prettier/prettier')); + t.false(hasRule(withoutPrettier, 'prettier/prettier', rulesMeta)); const {results: withPrettier} = await xo.lintText('(a) => {}\n', {prettier: true, filePath: 'test.js'}); // `arrow-parens` is disabled by `eslint-config-prettier` - t.false(hasRule(withPrettier, 'arrow-parens')); - // `prettier/prettier` is enabled + t.false(hasRule(withPrettier, 'arrow-parens', rulesMeta)); + // `prettier/prettier` is enabled - this is a special case for rulesMeta - so we remove it from this test t.true(hasRule(withPrettier, 'prettier/prettier')); }); test('extends `react` support with `prettier` option', async t => { - const {results} = await xo.lintText(';\n', {extends: 'xo-react', prettier: true, filePath: 'test.jsx'}); + const {results, rulesMeta} = await xo.lintText(';\n', {extends: 'xo-react', prettier: true, filePath: 'test.jsx'}); // `react/jsx-curly-spacing` is disabled by `eslint-config-prettier` - t.false(hasRule(results, 'react/jsx-curly-spacing')); + t.false(hasRule(results, 'react/jsx-curly-spacing', rulesMeta)); // `prettier/prettier` is enabled - t.true(hasRule(results, 'prettier/prettier')); + t.true(hasRule(results, 'prettier/prettier', rulesMeta)); }); test('regression test for #71', async t => { @@ -210,11 +214,11 @@ test.failing('enable rules based on nodeVersion', async t => { const filePath = path.join(cwd, 'promise-then.js'); const text = await fs.readFile(filePath, 'utf8'); - let {results} = await xo.lintText(text, {nodeVersion: '>=8.0.0'}); - t.true(hasRule(results, 'promise/prefer-await-to-then')); + let {results, rulesMeta} = await xo.lintText(text, {nodeVersion: '>=8.0.0'}); + t.true(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); - ({results} = await xo.lintText(text, {nodeVersion: '>=6.0.0'})); - t.false(hasRule(results, 'promise/prefer-await-to-then')); + ({results, rulesMeta} = await xo.lintText(text, {nodeVersion: '>=6.0.0'})); + t.false(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); }); test.failing('enable rules based on nodeVersion in override', async t => { @@ -222,7 +226,7 @@ test.failing('enable rules based on nodeVersion in override', async t => { const filePath = path.join(cwd, 'promise-then.js'); const text = await fs.readFile(filePath, 'utf8'); - let {results} = await xo.lintText(text, { + let {results, rulesMeta} = await xo.lintText(text, { nodeVersion: '>=8.0.0', filePath: 'promise-then.js', overrides: [ @@ -232,9 +236,9 @@ test.failing('enable rules based on nodeVersion in override', async t => { }, ], }); - t.false(hasRule(results, 'promise/prefer-await-to-then')); + t.false(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); - ({results} = await xo.lintText(text, { + ({results, rulesMeta} = await xo.lintText(text, { nodeVersion: '>=6.0.0', filePath: 'promise-then.js', overrides: [ @@ -244,47 +248,52 @@ test.failing('enable rules based on nodeVersion in override', async t => { }, ], })); - t.true(hasRule(results, 'promise/prefer-await-to-then')); + t.true(hasRule(results, 'promise/prefer-await-to-then', rulesMeta)); }); test('allow unassigned stylesheet imports', async t => { - let {results} = await xo.lintText('import \'stylesheet.css\''); - t.false(hasRule(results, 'import/no-unassigned-import')); + let {results, rulesMeta} = await xo.lintText('import \'stylesheet.css\''); + t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); - ({results} = await xo.lintText('import \'stylesheet.scss\'')); - t.false(hasRule(results, 'import/no-unassigned-import')); + ({results, rulesMeta} = await xo.lintText('import \'stylesheet.scss\'')); + t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); - ({results} = await xo.lintText('import \'stylesheet.sass\'')); - t.false(hasRule(results, 'import/no-unassigned-import')); + ({results, rulesMeta} = await xo.lintText('import \'stylesheet.sass\'')); + t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); - ({results} = await xo.lintText('import \'stylesheet.less\'')); - t.false(hasRule(results, 'import/no-unassigned-import')); + ({results, rulesMeta} = await xo.lintText('import \'stylesheet.less\'')); + t.false(hasRule(results, 'import/no-unassigned-import', rulesMeta)); }); test('find configurations close to linted file', async t => { - let {results} = await xo.lintText('console.log(\'semicolon\');\n', {filePath: 'fixtures/nested-configs/child/semicolon.js'}); - t.true(hasRule(results, 'semi')); + let {results, rulesMeta} = await xo.lintText('console.log(\'semicolon\');\n', {filePath: 'fixtures/nested-configs/child/semicolon.js'}); + t.true(hasRule(results, 'semi', rulesMeta)); - ({results} = await xo.lintText('console.log(\'semicolon\');\n', {filePath: 'fixtures/nested-configs/child-override/child-prettier-override/semicolon.js'})); - t.true(hasRule(results, 'prettier/prettier')); + ({results, rulesMeta} = await xo.lintText('console.log(\'semicolon\');\n', {filePath: 'fixtures/nested-configs/child-override/child-prettier-override/semicolon.js'})); + t.true(hasRule(results, 'prettier/prettier', rulesMeta)); - ({results} = await xo.lintText('console.log(\'no-semicolon\')\n', {filePath: 'fixtures/nested-configs/no-semicolon.js'})); - t.true(hasRule(results, 'semi')); + ({results, rulesMeta} = await xo.lintText('console.log(\'no-semicolon\')\n', {filePath: 'fixtures/nested-configs/no-semicolon.js'})); + t.true(hasRule(results, 'semi', rulesMeta)); - ({results} = await xo.lintText(`console.log([ + ({results, rulesMeta} = await xo.lintText(`console.log([ 2 ]);\n`, {filePath: 'fixtures/nested-configs/child-override/two-spaces.js'})); - t.true(hasRule(results, 'indent')); + t.true(hasRule(results, 'indent', rulesMeta)); +}); + +test('rulesMeta is added to the report by default', async t => { + const report = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n'); + t.is(typeof report.rulesMeta, 'object'); }); test('typescript files: two spaces fails', async t => { const twoSpacesCwd = path.resolve('fixtures', 'typescript'); const twoSpacesfilePath = path.resolve(twoSpacesCwd, 'two-spaces.tsx'); const twoSpacesText = await fs.readFile(twoSpacesfilePath, 'utf8'); - const {results} = await xo.lintText(twoSpacesText, { + const {results, rulesMeta} = await xo.lintText(twoSpacesText, { filePath: twoSpacesfilePath, }); - t.true(hasRule(results, '@typescript-eslint/indent')); + t.true(hasRule(results, '@typescript-eslint/indent', rulesMeta)); }); test('typescript files: two spaces pass', async t => { @@ -302,10 +311,10 @@ test('typescript files: extra semi fail', async t => { const extraSemiCwd = path.resolve('fixtures', 'typescript', 'child'); const extraSemiFilePath = path.resolve(extraSemiCwd, 'extra-semicolon.ts'); const extraSemiText = await fs.readFile(extraSemiFilePath, 'utf8'); - const {results} = await xo.lintText(extraSemiText, { + const {results, rulesMeta} = await xo.lintText(extraSemiText, { filePath: extraSemiFilePath, }); - t.true(hasRule(results, '@typescript-eslint/no-extra-semi')); + t.true(hasRule(results, '@typescript-eslint/no-extra-semi', rulesMeta)); }); test('typescript files: extra semi pass', async t => { @@ -323,10 +332,10 @@ test('typescript files: four space fail', async t => { const fourSpacesCwd = path.resolve('fixtures', 'typescript', 'child', 'sub-child'); const fourSpacesFilePath = path.resolve(fourSpacesCwd, 'four-spaces.ts'); const fourSpacesText = await fs.readFile(fourSpacesFilePath, 'utf8'); - const {results} = await xo.lintText(fourSpacesText, { + const {results, rulesMeta} = await xo.lintText(fourSpacesText, { filePath: fourSpacesFilePath, }); - t.true(hasRule(results, '@typescript-eslint/indent')); + t.true(hasRule(results, '@typescript-eslint/indent', rulesMeta)); }); test('typescript files: four space pass', async t => { @@ -350,8 +359,8 @@ test('deprecated rules', async t => { }); async function configType(t, {dir}) { - const {results} = await xo.lintText('var obj = { a: 1 };\n', {cwd: path.resolve('fixtures', 'config-files', dir), filePath: 'file.js'}); - t.true(hasRule(results, 'no-var')); + const {results, rulesMeta} = await xo.lintText('var obj = { a: 1 };\n', {cwd: path.resolve('fixtures', 'config-files', dir), filePath: 'file.js'}); + t.true(hasRule(results, 'no-var', rulesMeta)); } configType.title = (_, {type}) => `load config from ${type}`.trim();