Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/plugin-eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"type": "module",
"dependencies": {
"ansis": "^3.3.2",
"glob": "^11.0.0",
"@code-pushup/utils": "0.94.0",
"@code-pushup/models": "0.94.0",
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-eslint/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const ESLINT_PLUGIN_SLUG = 'eslint';
export const ESLINT_PLUGIN_TITLE = 'ESLint';
6 changes: 3 additions & 3 deletions packages/plugin-eslint/src/lib/eslint-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
eslintPluginConfigSchema,
eslintPluginOptionsSchema,
} from './config.js';
import { ESLINT_PLUGIN_SLUG } from './constants.js';
import { ESLINT_PLUGIN_SLUG, ESLINT_PLUGIN_TITLE } from './constants.js';
import { listAuditsAndGroups } from './meta/index.js';
import { createRunnerFunction } from './runner/index.js';

Expand Down Expand Up @@ -51,7 +51,7 @@ export async function eslintPlugin(

return {
slug: ESLINT_PLUGIN_SLUG,
title: 'ESLint',
title: ESLINT_PLUGIN_TITLE,
icon: 'eslint',
description: 'Official Code PushUp ESLint plugin',
docsUrl: 'https://www.npmjs.com/package/@code-pushup/eslint-plugin',
Expand All @@ -61,7 +61,7 @@ export async function eslintPlugin(
audits,
groups,

runner: await createRunnerFunction({
runner: createRunnerFunction({
audits,
targets,
...(artifacts ? { artifacts } : {}),
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-eslint/src/lib/meta/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { pluginMetaLogFormatter } from '@code-pushup/utils';
import { ESLINT_PLUGIN_TITLE } from '../constants.js';

export const formatMetaLog = pluginMetaLogFormatter(ESLINT_PLUGIN_TITLE);
38 changes: 33 additions & 5 deletions packages/plugin-eslint/src/lib/meta/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Audit, Group } from '@code-pushup/models';
import { formatAsciiTable, logger, pluralizeToken } from '@code-pushup/utils';
import type { CustomGroup, ESLintTarget } from '../config.js';
import { formatMetaLog } from './format.js';
import {
groupsFromCustomConfig,
groupsFromRuleCategories,
Expand All @@ -10,24 +12,50 @@ import { ruleToAudit } from './transform.js';

export { ruleIdToSlug } from './hash.js';
export { detectConfigVersion, type ConfigFormat } from './versions/index.js';
export { formatMetaLog };

export async function listAuditsAndGroups(
targets: ESLintTarget[],
customGroups?: CustomGroup[] | undefined,
): Promise<{ audits: Audit[]; groups: Group[] }> {
const rules = await listRules(targets);
const audits = rules.map(ruleToAudit);

logger.info(
formatMetaLog(
`Found ${pluralizeToken('rule', rules.length)} in total for ${pluralizeToken('target', targets.length)}, mapped to audits`,
),
);

const resolvedTypeGroups = groupsFromRuleTypes(rules);
const resolvedCategoryGroups = groupsFromRuleCategories(rules);
const resolvedCustomGroups = customGroups
? groupsFromCustomConfig(rules, customGroups)
: [];

const audits = rules.map(ruleToAudit);

const groups = [
...groupsFromRuleTypes(rules),
...groupsFromRuleCategories(rules),
...resolvedTypeGroups,
...resolvedCategoryGroups,
...resolvedCustomGroups,
];

logger.info(
formatMetaLog(
`Created ${pluralizeToken('group', groups.length)} (${resolvedTypeGroups.length} from meta.type, ${resolvedCategoryGroups.length} from meta.docs.category, ${resolvedCustomGroups.length} from custom groups)`,
),
);
logger.debug(
formatMetaLog(
formatAsciiTable(
{
rows: groups.map(group => [
`• ${group.title}`,
pluralizeToken('audit', group.refs.length),
]),
},
{ borderless: true },
),
),
);

return { audits, groups };
}
19 changes: 19 additions & 0 deletions packages/plugin-eslint/src/lib/meta/rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import ansis from 'ansis';
import { logger, pluralize, pluralizeToken, toArray } from '@code-pushup/utils';
import type { ESLintTarget } from '../config.js';
import { formatMetaLog } from './format.js';
import { jsonHash } from './hash.js';
import type { RuleData } from './parse.js';
import { detectConfigVersion, selectRulesLoader } from './versions/index.js';
Expand All @@ -8,10 +11,16 @@ type RulesMap = Record<string, Record<string, RuleData>>;
export async function listRules(targets: ESLintTarget[]): Promise<RuleData[]> {
const version = await detectConfigVersion();
const loadRulesMap = selectRulesLoader(version);
logger.debug(formatMetaLog(`Detected ${version} config format`));

const rulesMap = await targets.reduce(async (acc, target) => {
const map = await acc;
const rules = await loadRulesMap(target);
logger.debug(
formatMetaLog(
`Found ${pluralizeToken('rule', rules.length)} for ${formatTarget(target)}`,
),
);
return rules.reduce(mergeRuleIntoMap, map);
}, Promise.resolve<RulesMap>({}));

Expand All @@ -28,6 +37,16 @@ function mergeRuleIntoMap(map: RulesMap, rule: RuleData): RulesMap {
};
}

function formatTarget(target: ESLintTarget): string {
const patterns = toArray(target.patterns);
return [
`${pluralize('pattern', patterns.length)} ${ansis.bold(patterns.join(' '))}`,
target.eslintrc && `using config ${ansis.bold(target.eslintrc)}`,
]
.filter(Boolean)
.join(' ');
}

export function expandWildcardRules(
wildcard: string,
rules: string[],
Expand Down
21 changes: 18 additions & 3 deletions packages/plugin-eslint/src/lib/nx/find-all-projects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { logger, stringifyError } from '@code-pushup/utils';
import { logger, pluralizeToken, stringifyError } from '@code-pushup/utils';
import type { ESLintTarget } from '../config.js';
import { formatMetaLog } from '../meta/format.js';
import { filterProjectGraph } from './filter-project-graph.js';
import { nxProjectsToConfig } from './projects-to-config.js';

Expand All @@ -14,7 +15,7 @@ async function resolveCachedProjectGraph() {
try {
return readCachedProjectGraph();
} catch (error) {
logger.info(
logger.warn(
`Could not read cached project graph, falling back to async creation.\n${stringifyError(error)}`,
);
return await createProjectGraphAsync({ exitOnError: false });
Expand Down Expand Up @@ -55,7 +56,21 @@ export async function eslintConfigFromAllNxProjects(
projectGraph,
options.exclude,
);
return nxProjectsToConfig(filteredProjectGraph);
const targets = await nxProjectsToConfig(filteredProjectGraph);

logger.info(
formatMetaLog(
[
`Inferred ${pluralizeToken('lint target', targets.length)} for all Nx projects`,
options.exclude?.length &&
`(excluding ${pluralizeToken('project', options.exclude.length)})`,
]
.filter(Boolean)
.join(' '),
),
);

return targets;
}

/**
Expand Down
13 changes: 12 additions & 1 deletion packages/plugin-eslint/src/lib/nx/find-project-with-deps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { logger } from '@nx/devkit';
import { pluralizeToken } from '@code-pushup/utils';
import type { ESLintTarget } from '../config.js';
import { formatMetaLog } from '../meta/format.js';
import { nxProjectsToConfig } from './projects-to-config.js';
import { findAllDependencies } from './traverse-graph.js';

Expand Down Expand Up @@ -35,10 +38,18 @@ export async function eslintConfigFromNxProjectAndDeps(

const dependencies = findAllDependencies(projectName, projectGraph);

return nxProjectsToConfig(
const targets = await nxProjectsToConfig(
projectGraph,
project =>
!!project.name &&
(project.name === projectName || dependencies.has(project.name)),
);

logger.info(
formatMetaLog(
`Inferred ${pluralizeToken('lint target', targets.length)} for Nx project "${projectName}" and its dependencies`,
),
);

return targets;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { logger } from '@code-pushup/utils';
import type { ESLintTarget } from '../config.js';
import { formatMetaLog } from '../meta/format.js';
import { nxProjectsToConfig } from './projects-to-config.js';

/**
Expand Down Expand Up @@ -41,5 +43,9 @@ export async function eslintConfigFromNxProject(
throw new Error(`Couldn't find Nx project named "${projectName}"`);
}

logger.info(
formatMetaLog(`Inferred lint target for Nx project "${projectName}"`),
);

return project;
}
28 changes: 26 additions & 2 deletions packages/plugin-eslint/src/lib/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import type {
PluginArtifactOptions,
RunnerFunction,
} from '@code-pushup/models';
import { asyncSequential, logger } from '@code-pushup/utils';
import {
asyncSequential,
logger,
pluralizeToken,
roundDecimals,
} from '@code-pushup/utils';
import type { ESLintPluginRunnerConfig, ESLintTarget } from '../config.js';
import { lint } from './lint.js';
import { aggregateLintResultsStats } from './stats.js';
import { lintResultsToAudits, mergeLinterOutputs } from './transform.js';
import { loadArtifacts } from './utils.js';

Expand All @@ -23,14 +29,32 @@ export function createRunnerFunction(options: {
};

return async (): Promise<AuditOutputs> => {
logger.info(`ESLint plugin executing ${targets.length} lint targets`);
logger.info(
`ESLint plugin executing ${pluralizeToken('lint target', targets.length)}`,
);

const linterOutputs = artifacts
? await loadArtifacts(artifacts)
: await asyncSequential(targets, lint);

const lintResults = mergeLinterOutputs(linterOutputs);
const failedAudits = lintResultsToAudits(lintResults);

const stats = aggregateLintResultsStats(lintResults.results);
logger.info(
stats.problemsCount === 0
? 'ESLint did not find any problems'
: `ESLint found ${pluralizeToken('problem', stats.problemsCount)} from ${pluralizeToken('rule', stats.failedRulesCount)} across ${pluralizeToken('file', stats.failedFilesCount)}`,
);

const totalCount = config.slugs.length;
const failedCount = failedAudits.length;
const passedCount = totalCount - failedCount;
const percentage = roundDecimals((passedCount / totalCount) * 100, 2);
logger.info(
`${pluralizeToken('audit', passedCount)} passed, ${pluralizeToken('audit', failedCount)} failed (${percentage}% success)`,
);

return config.slugs.map(
(slug): AuditOutput =>
failedAudits.find(audit => audit.slug === slug) ?? {
Expand Down
23 changes: 23 additions & 0 deletions packages/plugin-eslint/src/lib/runner/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ESLint } from 'eslint';

export type LintResultsStats = {
problemsCount: number;
failedFilesCount: number;
failedRulesCount: number;
};

export function aggregateLintResultsStats(
results: ESLint.LintResult[],
): LintResultsStats {
const problemsCount = results.reduce(
(acc, result) => acc + result.messages.length,
0,
);
const failedFilesCount = results.length;
const failedRulesCount = new Set(
results.flatMap(({ messages }) =>
messages.map(({ ruleId }) => ruleId).filter(Boolean),
),
).size;
return { problemsCount, failedFilesCount, failedRulesCount };
}
72 changes: 72 additions & 0 deletions packages/plugin-eslint/src/lib/runner/stats.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ESLint } from 'eslint';
import { type LintResultsStats, aggregateLintResultsStats } from './stats.js';

describe('aggregateLintResultsStats', () => {

Check failure on line 4 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
it('should sum all errors and warning across all files', () => {

Check failure on line 5 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
expect(

Check failure on line 6 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
aggregateLintResultsStats([
{
filePath: 'src/main.js',
messages: [{ severity: 2 }],
},
{
filePath: 'src/lib/index.js',
messages: [{ severity: 2 }, { severity: 1 }],
},
{
filePath: 'src/lib/utils.js',
messages: [
{ severity: 1 },
{ severity: 1 },
{ severity: 2 },
{ severity: 1 },
],
},
] as ESLint.LintResult[]),
).toEqual(
expect.objectContaining<Partial<LintResultsStats>>({

Check failure on line 27 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
problemsCount: 7,
}),
);
});

it('should count files with problems', () => {

Check failure on line 33 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
expect(

Check failure on line 34 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
aggregateLintResultsStats([
{ filePath: 'src/main.js', messages: [{}] },
{ filePath: 'src/lib/index.js', messages: [{}, {}] },
{ filePath: 'src/lib/utils.js', messages: [{}, {}, {}] },
] as ESLint.LintResult[]),
).toEqual(
expect.objectContaining<Partial<LintResultsStats>>({

Check failure on line 41 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
failedFilesCount: 3,
}),
);
});

it('should count unique rules with reported problems', () => {

Check failure on line 47 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
expect(

Check failure on line 48 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
aggregateLintResultsStats([
{ filePath: 'src/lib/main.js', messages: [{}] }, // empty ruleId ignored
{
filePath: 'src/lib/index.js',
messages: [{ ruleId: 'max-lines' }, { ruleId: 'no-unused-vars' }],
},
{
filePath: 'src/lib/utils.js',
messages: [
{ ruleId: 'no-unused-vars' },
{ ruleId: 'eqeqeq' },
{ ruleId: 'no-unused-vars' },
{ ruleId: 'yoda' },
{ ruleId: 'eqeqeq' },
],
},
] as ESLint.LintResult[]),
).toEqual(
expect.objectContaining<Partial<LintResultsStats>>({

Check failure on line 67 in packages/plugin-eslint/src/lib/runner/stats.unit.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
failedRulesCount: 4, // no-unused-vars (3), eqeqeq (2), max-lines (1), yoda (1)
}),
);
});
});
Loading