From 4f4633ac741da9472d6394c500023d71f8aad7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Fri, 19 Dec 2025 10:51:56 +0100 Subject: [PATCH] feat(plugin-jsdocs): log initializer and runner steps --- code-pushup.preset.ts | 18 +++--- .../__snapshots__/collect.e2e.test.ts.snap | 2 +- .../src/lib/runner/lcov/lcov-runner.ts | 23 +------- packages/plugin-jsdocs/src/lib/constants.ts | 6 +- packages/plugin-jsdocs/src/lib/format.ts | 4 ++ .../plugin-jsdocs/src/lib/jsdocs-plugin.ts | 25 +++++---- .../src/lib/jsdocs-plugin.unit.test.ts | 12 ++-- .../src/lib/runner/doc-processor.ts | 24 +++++--- .../plugin-jsdocs/src/lib/runner/utils.ts | 56 ++++++++++++++++++- packages/plugin-jsdocs/src/lib/utils.ts | 31 +++++++++- packages/utils/src/index.ts | 7 ++- packages/utils/src/lib/coverage-tree.ts | 10 ++++ .../utils/src/lib/coverage-tree.unit.test.ts | 20 ++++++- packages/utils/src/lib/formatting.ts | 14 +++++ .../utils/src/lib/formatting.unit.test.ts | 25 +++++++++ 15 files changed, 219 insertions(+), 58 deletions(-) create mode 100644 packages/plugin-jsdocs/src/lib/format.ts diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index e7b472ca3..a16b1759c 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -15,10 +15,6 @@ import eslintPlugin, { } from './packages/plugin-eslint/src/index.js'; import jsPackagesPlugin from './packages/plugin-js-packages/src/index.js'; import jsDocsPlugin from './packages/plugin-jsdocs/src/index.js'; -import { - PLUGIN_SLUG, - groups, -} from './packages/plugin-jsdocs/src/lib/constants.js'; import { lighthouseCategories, lighthouseGroupRef, @@ -185,12 +181,14 @@ export function configureJsDocsPlugin(projectName?: string): CoreConfig { slug: 'docs', title: 'Documentation', description: 'Measures how much of your code is **documented**.', - refs: groups.map(group => ({ - weight: 1, - type: 'group', - plugin: PLUGIN_SLUG, - slug: group.slug, - })), + refs: [ + { + type: 'group', + plugin: 'jsdocs', + slug: 'documentation-coverage', + weight: 1, + }, + ], }, ], }; diff --git a/e2e/plugin-jsdocs-e2e/tests/__snapshots__/collect.e2e.test.ts.snap b/e2e/plugin-jsdocs-e2e/tests/__snapshots__/collect.e2e.test.ts.snap index f4fd55401..c82fd6196 100644 --- a/e2e/plugin-jsdocs-e2e/tests/__snapshots__/collect.e2e.test.ts.snap +++ b/e2e/plugin-jsdocs-e2e/tests/__snapshots__/collect.e2e.test.ts.snap @@ -567,7 +567,7 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc ], "icon": "folder-docs", "slug": "jsdocs", - "title": "JSDoc coverage", + "title": "JSDocs coverage", }, ], } diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts index 3f45141cf..63f13a666 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts @@ -3,9 +3,11 @@ import type { LCOVRecord } from 'parse-lcov'; import type { AuditOutputs, TableColumnObject } from '@code-pushup/models'; import { type FileCoverage, + aggregateCoverageStats, capitalize, exists, formatAsciiTable, + formatCoveragePercentage, getGitRoot, logger, objectFromEntries, @@ -210,7 +212,7 @@ function logLcovRecords(recordsPerReport: Record): void { const stats: Record = objectFromEntries( objectToEntries(groups).map(([type, files = []]) => [ type, - formatCoverageSum(files), + formatCoveragePercentage(aggregateCoverageStats(files)), ]), ); const report = truncatedPaths[idx] ?? reportPath; @@ -222,25 +224,6 @@ function logLcovRecords(recordsPerReport: Record): void { logger.newline(); } -function formatCoverageSum(files: FileCoverage[]): string { - const { covered, total } = files.reduce< - Pick - >( - (acc, file) => ({ - covered: acc.covered + file.covered, - total: acc.total + file.total, - }), - { covered: 0, total: 0 }, - ); - - if (total === 0) { - return 'n/a'; - } - - const percentage = (covered / total) * 100; - return `${percentage.toFixed(1)}%`; -} - function logMergedRecords(counts: { before: number; after: number }): void { if (counts.before === counts.after) { logger.debug( diff --git a/packages/plugin-jsdocs/src/lib/constants.ts b/packages/plugin-jsdocs/src/lib/constants.ts index 558ef38de..f246c48fa 100644 --- a/packages/plugin-jsdocs/src/lib/constants.ts +++ b/packages/plugin-jsdocs/src/lib/constants.ts @@ -2,6 +2,10 @@ import type { Audit, Group } from '@code-pushup/models'; import type { AuditSlug } from './models.js'; export const PLUGIN_SLUG = 'jsdocs'; +export const PLUGIN_TITLE = 'JSDocs coverage'; +export const PLUGIN_DESCRIPTION = 'Official Code PushUp JSDoc coverage plugin.'; +export const PLUGIN_DOCS_URL = + 'https://www.npmjs.com/package/@code-pushup/jsdocs-plugin/'; export const AUDITS_MAP: Record = { 'classes-coverage': { @@ -46,7 +50,7 @@ export const AUDITS_MAP: Record = { }, } as const; -export const groups: Group[] = [ +export const GROUPS: Group[] = [ { slug: 'documentation-coverage', title: 'Documentation coverage', diff --git a/packages/plugin-jsdocs/src/lib/format.ts b/packages/plugin-jsdocs/src/lib/format.ts new file mode 100644 index 000000000..ca7108cc6 --- /dev/null +++ b/packages/plugin-jsdocs/src/lib/format.ts @@ -0,0 +1,4 @@ +import { pluginMetaLogFormatter } from '@code-pushup/utils'; +import { PLUGIN_TITLE } from './constants.js'; + +export const formatMetaLog = pluginMetaLogFormatter(PLUGIN_TITLE); diff --git a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts index 9da282a36..be29c2acb 100644 --- a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts +++ b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts @@ -1,19 +1,19 @@ import { type PluginConfig, validate } from '@code-pushup/models'; import { type JsDocsPluginConfig, jsDocsPluginConfigSchema } from './config.js'; -import { PLUGIN_SLUG, groups } from './constants.js'; +import { + GROUPS, + PLUGIN_DESCRIPTION, + PLUGIN_DOCS_URL, + PLUGIN_SLUG, + PLUGIN_TITLE, +} from './constants.js'; import { createRunnerFunction } from './runner/runner.js'; import { filterAuditsByPluginConfig, filterGroupsByOnlyAudits, + logAuditsAndGroups, } from './utils.js'; -export const PLUGIN_TITLE = 'JSDoc coverage'; - -export const PLUGIN_DESCRIPTION = 'Official Code PushUp JSDoc coverage plugin.'; - -export const PLUGIN_DOCS_URL = - 'https://www.npmjs.com/package/@code-pushup/jsdocs-plugin/'; - /** * Instantiates Code PushUp documentation coverage plugin for core config. * @@ -34,14 +34,19 @@ export function jsDocsPlugin(config: JsDocsPluginConfig): PluginConfig { const jsDocsConfig = validate(jsDocsPluginConfigSchema, config); const scoreTargets = jsDocsConfig.scoreTargets; + const groups = filterGroupsByOnlyAudits(GROUPS, jsDocsConfig); + const audits = filterAuditsByPluginConfig(jsDocsConfig); + + logAuditsAndGroups(audits, groups); + return { slug: PLUGIN_SLUG, title: PLUGIN_TITLE, icon: 'folder-docs', description: PLUGIN_DESCRIPTION, docsUrl: PLUGIN_DOCS_URL, - groups: filterGroupsByOnlyAudits(groups, jsDocsConfig), - audits: filterAuditsByPluginConfig(jsDocsConfig), + groups, + audits, runner: createRunnerFunction(jsDocsConfig), ...(scoreTargets && { scoreTargets }), }; diff --git a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts index 19b6f9b0f..6b768f5ed 100644 --- a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts @@ -1,19 +1,21 @@ import { describe, expect, it, vi } from 'vitest'; import { pluginConfigSchema } from '@code-pushup/models'; -import { PLUGIN_SLUG, groups } from './constants.js'; import { + GROUPS, PLUGIN_DESCRIPTION, PLUGIN_DOCS_URL, + PLUGIN_SLUG, PLUGIN_TITLE, - jsDocsPlugin, -} from './jsdocs-plugin.js'; +} from './constants.js'; +import { jsDocsPlugin } from './jsdocs-plugin.js'; import { createRunnerFunction } from './runner/runner.js'; import { filterAuditsByPluginConfig, filterGroupsByOnlyAudits, } from './utils.js'; -vi.mock('./utils.js', () => ({ +vi.mock('./utils.js', async () => ({ + ...(await vi.importActual('./utils.js')), filterAuditsByPluginConfig: vi.fn().mockReturnValue([ { slug: 'mock-audit', @@ -69,7 +71,7 @@ describe('jsDocsPlugin', () => { const config = { patterns: ['src/**/*.ts'] }; jsDocsPlugin(config); - expect(filterGroupsByOnlyAudits).toHaveBeenCalledWith(groups, config); + expect(filterGroupsByOnlyAudits).toHaveBeenCalledWith(GROUPS, config); }); it('should filter audits', async () => { diff --git a/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts b/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts index c5038a812..bd3e4bb14 100644 --- a/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts +++ b/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts @@ -16,6 +16,8 @@ import type { CoverageType } from './models.js'; import { createInitialCoverageTypesRecord, getCoverageTypeFromKind, + logReport, + logSourceFiles, singularCoverageType, } from './utils.js'; @@ -29,7 +31,7 @@ type Node = { /** * Gets the variables information from the variable statements - * @param variableStatements - The variable statements to process + * @param variableStatements The variable statements to process * @returns The variables information with the right methods to get the information */ export function getVariablesInformation( @@ -54,7 +56,7 @@ export function getVariablesInformation( /** * Processes documentation coverage for TypeScript files in the specified path - * @param config - The configuration object containing patterns to include for documentation analysis + * @param config The configuration object containing patterns to include for documentation analysis * @returns Object containing coverage statistics and undocumented items */ export function processJsDocs( @@ -62,7 +64,15 @@ export function processJsDocs( ): Record { const project = new Project(); project.addSourceFilesAtPaths(config.patterns); - return getDocumentationReport(project.getSourceFiles()); + const sourceFiles = project.getSourceFiles(); + + logSourceFiles(sourceFiles, config); + + const report = getDocumentationReport(sourceFiles); + + logReport(report); + + return report; } export function getAllNodesFromASourceFile(sourceFile: SourceFile) { @@ -80,7 +90,7 @@ export function getAllNodesFromASourceFile(sourceFile: SourceFile) { /** * Gets the documentation coverage report from the source files - * @param sourceFiles - The source files to process + * @param sourceFiles The source files to process * @returns The documentation coverage report */ export function getDocumentationReport( @@ -101,8 +111,8 @@ export function getDocumentationReport( /** * Gets the coverage from all nodes of a file - * @param nodes - The nodes to process - * @param filePath - The file path where the nodes are located + * @param nodes The nodes to process + * @param filePath The file path where the nodes are located * @returns The coverage report for the nodes */ function getCoverageFromAllNodesOfFile(nodes: Node[], filePath: string) { @@ -145,7 +155,7 @@ function getCoverageFromAllNodesOfFile(nodes: Node[], filePath: string) { /** * Gets the nodes from a class - * @param classNodes - The class nodes to process + * @param classNodes The class nodes to process * @returns The nodes from the class */ export function getClassNodes(classNodes: ClassDeclaration[]) { diff --git a/packages/plugin-jsdocs/src/lib/runner/utils.ts b/packages/plugin-jsdocs/src/lib/runner/utils.ts index f9d4c1fa9..7b34948dd 100644 --- a/packages/plugin-jsdocs/src/lib/runner/utils.ts +++ b/packages/plugin-jsdocs/src/lib/runner/utils.ts @@ -1,4 +1,17 @@ -import { SyntaxKind } from 'ts-morph'; +import { type SourceFile, SyntaxKind } from 'ts-morph'; +import { + type FileCoverage, + aggregateCoverageStats, + capitalize, + formatAsciiTable, + formatCoveragePercentage, + logger, + objectToEntries, + pluralize, + pluralizeToken, + toArray, +} from '@code-pushup/utils'; +import type { JsDocsPluginTransformedConfig } from '../config.js'; import { SYNTAX_COVERAGE_MAP } from './constants.js'; import type { CoverageType } from './models.js'; @@ -70,3 +83,44 @@ export function singularCoverageType(type: CoverageType): string { return 'variable'; } } + +export function logSourceFiles( + sourceFiles: SourceFile[], + config: JsDocsPluginTransformedConfig, +): void { + const patterns = toArray(config.patterns); + logger.info( + `Found ${pluralizeToken('source file', sourceFiles.length)} matching ${pluralize('pattern', patterns.length)} ${patterns.join(' ')}`, + ); +} + +export function logReport(report: Record): void { + const typesCount = Object.keys(report).length; + logger.info( + `Collected documentation coverage for ${pluralizeToken('type', typesCount)} of ${pluralize('entity', typesCount)}`, + ); + if (!logger.isVerbose()) { + return; + } + + logger.debug( + formatAsciiTable({ + columns: [ + { key: 'type', label: 'Entity', align: 'left' }, + { key: 'covered', label: 'Hits', align: 'right' }, + { key: 'total', label: 'Found', align: 'right' }, + { key: 'coverage', label: 'Coverage', align: 'right' }, + ], + rows: objectToEntries(report) + .map(([type, files]) => { + const stats = aggregateCoverageStats(files); + return { + ...stats, + type: capitalize(type), + coverage: formatCoveragePercentage(stats), + }; + }) + .toSorted((a, b) => b.total - a.total), + }), + ); +} diff --git a/packages/plugin-jsdocs/src/lib/utils.ts b/packages/plugin-jsdocs/src/lib/utils.ts index ce99d9c86..5c7242939 100644 --- a/packages/plugin-jsdocs/src/lib/utils.ts +++ b/packages/plugin-jsdocs/src/lib/utils.ts @@ -1,6 +1,8 @@ import type { Audit, Group } from '@code-pushup/models'; +import { logger, pluralizeToken } from '@code-pushup/utils'; import type { JsDocsPluginTransformedConfig } from './config.js'; -import { AUDITS_MAP } from './constants.js'; +import { AUDITS_MAP, GROUPS } from './constants.js'; +import { formatMetaLog } from './format.js'; /** * Get audits based on the configuration. @@ -50,3 +52,30 @@ export function filterGroupsByOnlyAudits( })) .filter(group => group.refs.length > 0); } + +export function logAuditsAndGroups(audits: Audit[], groups: Group[]) { + logger.info( + formatMetaLog( + `Created ${pluralizeToken('audit', audits.length)} and ${pluralizeToken('group', groups.length)}`, + ), + ); + const skippedAudits = Object.keys(AUDITS_MAP).filter( + slug => !audits.some(audit => audit.slug === slug), + ); + const skippedGroups = GROUPS.filter( + group => !groups.some(({ slug }) => slug === group.slug), + ); + if (skippedAudits.length > 0) { + logger.info( + formatMetaLog( + [ + `Skipped ${pluralizeToken('audit', skippedAudits.length)}`, + skippedGroups.length > 0 && + pluralizeToken('group', skippedGroups.length), + ] + .filter(Boolean) + .join(' and '), + ), + ); + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0adcf418b..c2db42147 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -9,7 +9,11 @@ export { uppercase, } from './lib/case-conversions.js'; export { formatCommandStatus } from './lib/command.js'; -export { filesCoverageToTree, type FileCoverage } from './lib/coverage-tree.js'; +export { + filesCoverageToTree, + type FileCoverage, + aggregateCoverageStats, +} from './lib/coverage-tree.js'; export { createRunnerFiles } from './lib/create-runner-files.js'; export { dateToUnixTimestamp } from './lib/dates.js'; export { comparePairs, matchArrayItemsByKey, type Diff } from './lib/diff.js'; @@ -49,6 +53,7 @@ export { export { filterItemRefsBy } from './lib/filter.js'; export { formatBytes, + formatCoveragePercentage, formatDuration, indentLines, pluginMetaLogFormatter, diff --git a/packages/utils/src/lib/coverage-tree.ts b/packages/utils/src/lib/coverage-tree.ts index 6f6e70200..7373885d4 100644 --- a/packages/utils/src/lib/coverage-tree.ts +++ b/packages/utils/src/lib/coverage-tree.ts @@ -168,3 +168,13 @@ function sortCoverageTree(root: CoverageTreeNode): CoverageTreeNode { ), }; } + +export function aggregateCoverageStats(files: CoverageStats[]): CoverageStats { + return files.reduce( + (acc, file) => ({ + covered: acc.covered + file.covered, + total: acc.total + file.total, + }), + { covered: 0, total: 0 }, + ); +} diff --git a/packages/utils/src/lib/coverage-tree.unit.test.ts b/packages/utils/src/lib/coverage-tree.unit.test.ts index 0bf7ecbc9..d9729c616 100644 --- a/packages/utils/src/lib/coverage-tree.unit.test.ts +++ b/packages/utils/src/lib/coverage-tree.unit.test.ts @@ -1,6 +1,10 @@ import path from 'node:path'; import type { CoverageTree } from '@code-pushup/models'; -import { type FileCoverage, filesCoverageToTree } from './coverage-tree.js'; +import { + type FileCoverage, + aggregateCoverageStats, + filesCoverageToTree, +} from './coverage-tree.js'; describe('filesCoverageToTree', () => { it('should convert list of files to folder structure', () => { @@ -223,3 +227,17 @@ describe('filesCoverageToTree', () => { ); }); }); + +describe('aggregateCoverageStats', () => { + it('should sum covered and total counts from all files', () => { + expect( + aggregateCoverageStats([ + { covered: 1, total: 5 }, + { covered: 0, total: 3 }, + { covered: 4, total: 4 }, + { covered: 0, total: 0 }, + { covered: 5, total: 8 }, + ]), + ).toEqual({ covered: 10, total: 20 }); + }); +}); diff --git a/packages/utils/src/lib/formatting.ts b/packages/utils/src/lib/formatting.ts index 95a410cf3..d47d0f8b7 100644 --- a/packages/utils/src/lib/formatting.ts +++ b/packages/utils/src/lib/formatting.ts @@ -182,3 +182,17 @@ export function pluginMetaLogFormatter( (line, idx) => `${idx === 0 ? prefix : padding} ${line}`, ); } + +export function formatCoveragePercentage(stats: { + covered: number; + total: number; +}): string { + const { covered, total } = stats; + + if (total === 0) { + return '-'; + } + + const percentage = (covered / total) * 100; + return `${percentage.toFixed(1)}%`; +} diff --git a/packages/utils/src/lib/formatting.unit.test.ts b/packages/utils/src/lib/formatting.unit.test.ts index 66bc826c8..c6459dce8 100644 --- a/packages/utils/src/lib/formatting.unit.test.ts +++ b/packages/utils/src/lib/formatting.unit.test.ts @@ -2,6 +2,7 @@ import ansis from 'ansis'; import { describe, expect, it } from 'vitest'; import { formatBytes, + formatCoveragePercentage, formatDate, formatDuration, indentLines, @@ -296,3 +297,27 @@ describe('pluginMetaLogFormatter', () => { ); }); }); + +describe('formatCoveragePercentage', () => { + it('should render percentage', () => { + expect(formatCoveragePercentage({ covered: 125, total: 1000 })).toBe( + '12.5%', + ); + }); + + it('should render max 1 decimal', () => { + expect(formatCoveragePercentage({ covered: 1, total: 3 })).toBe('33.3%'); + }); + + it('should render at least 1 decimal', () => { + expect(formatCoveragePercentage({ covered: 0, total: 10 })).toBe('0.0%'); + }); + + it('should round decimal up if appropriate', () => { + expect(formatCoveragePercentage({ covered: 2, total: 3 })).toBe('66.7%'); + }); + + it('should not render invalid percentage', () => { + expect(formatCoveragePercentage({ covered: 0, total: 0 })).toBe('-'); + }); +});