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 b405e7ca6..f4fd55401 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 @@ -9,7 +9,56 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of enums", "details": { - "issues": [], + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Documented enums", + "type": "coverage", + }, + ], }, "displayValue": "0 undocumented enums", "score": 1, @@ -20,7 +69,56 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of interfaces", "details": { - "issues": [], + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Documented interfaces", + "type": "coverage", + }, + ], }, "displayValue": "0 undocumented interfaces", "score": 1, @@ -31,7 +129,56 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of types", "details": { - "issues": [], + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Documented types", + "type": "coverage", + }, + ], }, "displayValue": "0 undocumented types", "score": 1, @@ -42,26 +189,68 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of functions", "details": { - "issues": [ - { - "message": "Missing functions documentation for notRealisticFunction", - "severity": "warning", - "source": { - "file": "tmp/e2e/plugin-jsdocs-e2e/__test__/angular/src/app.component.spec.ts", - "position": { - "startLine": 1, - }, - }, - }, + "trees": [ { - "message": "Missing functions documentation for mapEventToCustomEvent", - "severity": "warning", - "source": { - "file": "tmp/e2e/plugin-jsdocs-e2e/__test__/angular/src/map-event.function.ts", - "position": { - "startLine": 3, + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 0, + "missing": [ + { + "endLine": 3, + "kind": "function", + "name": "notRealisticFunction", + "startLine": 1, + }, + ], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 0.5, + "missing": [ + { + "endLine": 5, + "kind": "function", + "name": "mapEventToCustomEvent", + "startLine": 3, + }, + ], + }, + }, + ], + "name": "src", + "values": { + "coverage": 0.3333333333333333, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 0.3333333333333333, }, }, + "title": "Documented functions", + "type": "coverage", }, ], }, @@ -74,16 +263,61 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of variables", "details": { - "issues": [ + "trees": [ { - "message": "Missing variables documentation for someVariable", - "severity": "warning", - "source": { - "file": "tmp/e2e/plugin-jsdocs-e2e/__test__/angular/src/map-event.function.ts", - "position": { - "startLine": 1, + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 0, + "missing": [ + { + "endLine": 1, + "kind": "variable", + "name": "someVariable", + "startLine": 1, + }, + ], + }, + }, + ], + "name": "src", + "values": { + "coverage": 0, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 0, }, }, + "title": "Documented variables", + "type": "coverage", }, ], }, @@ -96,7 +330,56 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of classes", "details": { - "issues": [], + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Documented classes", + "type": "coverage", + }, + ], }, "displayValue": "0 undocumented classes", "score": 1, @@ -107,16 +390,61 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of methods", "details": { - "issues": [ + "trees": [ { - "message": "Missing methods documentation for sendEvent", - "severity": "warning", - "source": { - "file": "tmp/e2e/plugin-jsdocs-e2e/__test__/angular/src/app.component.ts", - "position": { - "startLine": 15, + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 0.5, + "missing": [ + { + "endLine": 17, + "kind": "method", + "name": "sendEvent", + "startLine": 15, + }, + ], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 0.5, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 0.5, }, }, + "title": "Documented methods", + "type": "coverage", }, ], }, @@ -129,16 +457,61 @@ exports[`PLUGIN collect report with jsdocs-plugin NPM package > should run JSDoc { "description": "Documentation coverage of properties", "details": { - "issues": [ + "trees": [ { - "message": "Missing properties documentation for title", - "severity": "warning", - "source": { - "file": "tmp/e2e/plugin-jsdocs-e2e/__test__/angular/src/app.component.ts", - "position": { - "startLine": 5, + "root": { + "children": [ + { + "children": [ + { + "name": "app.component.spec.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + { + "name": "app.component.ts", + "values": { + "coverage": 0, + "missing": [ + { + "endLine": 5, + "kind": "property", + "name": "title", + "startLine": 5, + }, + ], + }, + }, + { + "name": "map-event.function.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 0, + }, + }, + { + "name": "code-pushup.config.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": ".", + "values": { + "coverage": 0, }, }, + "title": "Documented properties", + "type": "coverage", }, ], }, diff --git a/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts b/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts index c177b04c3..dc94cdfbe 100644 --- a/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts @@ -1,11 +1,13 @@ import { cp } from 'node:fs/promises'; import path from 'node:path'; +import { simpleGit } from 'simple-git'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { type Report, reportSchema } from '@code-pushup/models'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, + initGitRepo, omitVariableReportData, teardownTestFolder, } from '@code-pushup/test-utils'; @@ -34,6 +36,8 @@ describe('PLUGIN collect report with jsdocs-plugin NPM package', () => { beforeAll(async () => { await cp(fixturesAngularDir, angularDir, { recursive: true }); await cp(fixturesReactDir, reactDir, { recursive: true }); + await initGitRepo(simpleGit, { baseDir: angularDir }); + await initGitRepo(simpleGit, { baseDir: reactDir }); }); afterAll(async () => { diff --git a/packages/plugin-jsdocs/mocks/fixtures/filled-documentation/functions-coverage.ts b/packages/plugin-jsdocs/mocks/fixtures/filled-documentation/functions-coverage.ts new file mode 100644 index 000000000..5a17c925e --- /dev/null +++ b/packages/plugin-jsdocs/mocks/fixtures/filled-documentation/functions-coverage.ts @@ -0,0 +1,7 @@ +/** + * An example function which returns a mock value. + * @returns A mock value + */ +export function exampleFunction() { + return 'exampleFunction'; +} diff --git a/packages/plugin-jsdocs/mocks/node.mock.ts b/packages/plugin-jsdocs/mocks/node.mock.ts index 691c7ab26..81e9dbc74 100644 --- a/packages/plugin-jsdocs/mocks/node.mock.ts +++ b/packages/plugin-jsdocs/mocks/node.mock.ts @@ -12,6 +12,7 @@ export function nodeMock(options: { getJsDocs: () => (options.isCommented ? ['Comment'] : []), getName: () => 'test', getStartLineNumber: () => options.line, + getEndLineNumber: () => options.line, getDeclarations: () => [], // Only for classes getMethods: () => [], diff --git a/packages/plugin-jsdocs/project.json b/packages/plugin-jsdocs/project.json index d8534553e..545006933 100644 --- a/packages/plugin-jsdocs/project.json +++ b/packages/plugin-jsdocs/project.json @@ -12,7 +12,6 @@ "outputPath": "dist/packages/plugin-jsdocs", "main": "packages/plugin-jsdocs/src/index.ts", "tsConfig": "packages/plugin-jsdocs/tsconfig.lib.json", - "additionalEntryPoints": ["packages/plugin-jsdocs/src/bin.ts"], "assets": ["packages/plugin-jsdocs/*.md"] } }, diff --git a/packages/plugin-jsdocs/src/lib/runner/__snapshots__/doc-processor.unit.test.ts.snap b/packages/plugin-jsdocs/src/lib/runner/__snapshots__/doc-processor.unit.test.ts.snap index 3a9c6a965..87b739e95 100644 --- a/packages/plugin-jsdocs/src/lib/runner/__snapshots__/doc-processor.unit.test.ts.snap +++ b/packages/plugin-jsdocs/src/lib/runner/__snapshots__/doc-processor.unit.test.ts.snap @@ -2,91 +2,115 @@ exports[`getDocumentationReport > should produce a full report 1`] = ` { - "classes": { - "coverage": 33.33, - "issues": [ - { - "file": "test.ts", - "line": 4, - "name": "test", - "type": "classes", - }, - { - "file": "test.ts", - "line": 5, - "name": "test", - "type": "classes", - }, - ], - "nodesCount": 3, - }, - "enums": { - "coverage": 33.33, - "issues": [ - { - "file": "test.ts", - "line": 8, - "name": "test", - "type": "enums", - }, - { - "file": "test.ts", - "line": 9, - "name": "test", - "type": "enums", - }, - ], - "nodesCount": 3, - }, - "functions": { - "coverage": 100, - "issues": [], - "nodesCount": 3, - }, - "interfaces": { - "coverage": 66.67, - "issues": [ - { - "file": "test.ts", - "line": 15, - "name": "test", - "type": "interfaces", - }, - ], - "nodesCount": 3, - }, - "methods": { - "coverage": 100, - "issues": [], - "nodesCount": 0, - }, - "properties": { - "coverage": 100, - "issues": [], - "nodesCount": 0, - }, - "types": { - "coverage": 50, - "issues": [ - { - "file": "test.ts", - "line": 10, - "name": "test", - "type": "types", - }, - { - "file": "test.ts", - "line": 11, - "name": "test", - "type": "types", - }, - ], - "nodesCount": 4, - }, - "variables": { - "coverage": 100, - "issues": [], - "nodesCount": 0, - }, + "classes": [ + { + "covered": 1, + "missing": [ + { + "endLine": 4, + "kind": "class", + "name": "test", + "startLine": 4, + }, + { + "endLine": 5, + "kind": "class", + "name": "test", + "startLine": 5, + }, + ], + "path": "test.ts", + "total": 3, + }, + ], + "enums": [ + { + "covered": 1, + "missing": [ + { + "endLine": 8, + "kind": "enum", + "name": "test", + "startLine": 8, + }, + { + "endLine": 9, + "kind": "enum", + "name": "test", + "startLine": 9, + }, + ], + "path": "test.ts", + "total": 3, + }, + ], + "functions": [ + { + "covered": 3, + "missing": [], + "path": "test.ts", + "total": 3, + }, + ], + "interfaces": [ + { + "covered": 2, + "missing": [ + { + "endLine": 15, + "kind": "interface", + "name": "test", + "startLine": 15, + }, + ], + "path": "test.ts", + "total": 3, + }, + ], + "methods": [ + { + "covered": 0, + "missing": [], + "path": "test.ts", + "total": 0, + }, + ], + "properties": [ + { + "covered": 0, + "missing": [], + "path": "test.ts", + "total": 0, + }, + ], + "types": [ + { + "covered": 2, + "missing": [ + { + "endLine": 10, + "kind": "type", + "name": "test", + "startLine": 10, + }, + { + "endLine": 11, + "kind": "type", + "name": "test", + "startLine": 11, + }, + ], + "path": "test.ts", + "total": 4, + }, + ], + "variables": [ + { + "covered": 0, + "missing": [], + "path": "test.ts", + "total": 0, + }, + ], } `; diff --git a/packages/plugin-jsdocs/src/lib/runner/doc-processor.integration.test.ts b/packages/plugin-jsdocs/src/lib/runner/doc-processor.integration.test.ts index bb023a6da..3947896f6 100644 --- a/packages/plugin-jsdocs/src/lib/runner/doc-processor.integration.test.ts +++ b/packages/plugin-jsdocs/src/lib/runner/doc-processor.integration.test.ts @@ -1,11 +1,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import type { FileCoverage } from '@code-pushup/utils'; import { processJsDocs } from './doc-processor.js'; -import type { DocumentationData } from './models.js'; - -type DocumentationDataCovered = DocumentationData & { - coverage: number; -}; describe('processJsDocs', () => { const fixturesDir = path.join( @@ -19,18 +15,21 @@ describe('processJsDocs', () => { 'missing-documentation/classes-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.classes).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('classes-coverage.ts'), - type: 'classes', - name: 'ExampleClass', - line: 1, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.classes).toStrictEqual([ + { + path: expect.pathToEndWith('classes-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'class', + name: 'ExampleClass', + startLine: 1, + endLine: 1, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented class', () => { @@ -39,11 +38,14 @@ describe('processJsDocs', () => { 'filled-documentation/classes-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.classes).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.classes).toStrictEqual([ + { + path: expect.pathToEndWith('classes-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); it('should detect undocumented method', () => { @@ -52,18 +54,21 @@ describe('processJsDocs', () => { 'missing-documentation/methods-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.methods).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('methods-coverage.ts'), - type: 'methods', - name: 'exampleMethod', - line: 5, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.methods).toStrictEqual([ + { + path: expect.pathToEndWith('methods-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'method', + name: 'exampleMethod', + startLine: 5, + endLine: 7, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented method', () => { @@ -72,11 +77,14 @@ describe('processJsDocs', () => { 'filled-documentation/methods-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.methods).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.methods).toStrictEqual([ + { + path: expect.pathToEndWith('methods-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); it('should detect undocumented interface', () => { @@ -85,18 +93,21 @@ describe('processJsDocs', () => { 'missing-documentation/interfaces-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.interfaces).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('interfaces-coverage.ts'), - type: 'interfaces', - name: 'ExampleInterface', - line: 1, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.interfaces).toStrictEqual([ + { + path: expect.pathToEndWith('interfaces-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'interface', + name: 'ExampleInterface', + startLine: 1, + endLine: 1, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented interface', () => { @@ -105,11 +116,14 @@ describe('processJsDocs', () => { 'filled-documentation/interfaces-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.interfaces).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.interfaces).toStrictEqual([ + { + path: expect.pathToEndWith('interfaces-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); it('should detect undocumented variable', () => { @@ -118,18 +132,21 @@ describe('processJsDocs', () => { 'missing-documentation/variables-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.variables).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('variables-coverage.ts'), - type: 'variables', - name: 'exampleVariable', - line: 1, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.variables).toStrictEqual([ + { + path: expect.pathToEndWith('variables-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'variable', + name: 'exampleVariable', + startLine: 1, + endLine: 1, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented variable', () => { @@ -138,11 +155,14 @@ describe('processJsDocs', () => { 'filled-documentation/variables-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.variables).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.variables).toStrictEqual([ + { + path: expect.pathToEndWith('variables-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); it('should detect undocumented property', () => { @@ -151,18 +171,21 @@ describe('processJsDocs', () => { 'missing-documentation/properties-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.properties).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('properties-coverage.ts'), - type: 'properties', - name: 'exampleProperty', - line: 5, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.properties).toStrictEqual([ + { + path: expect.pathToEndWith('properties-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'property', + name: 'exampleProperty', + startLine: 5, + endLine: 5, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented property', () => { @@ -171,11 +194,14 @@ describe('processJsDocs', () => { 'filled-documentation/properties-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.properties).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.properties).toStrictEqual([ + { + path: expect.pathToEndWith('properties-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); it('should detect undocumented type', () => { @@ -184,18 +210,21 @@ describe('processJsDocs', () => { 'missing-documentation/types-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.types).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('types-coverage.ts'), - type: 'types', - name: 'ExampleType', - line: 1, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.types).toStrictEqual([ + { + path: expect.pathToEndWith('types-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'type', + name: 'ExampleType', + startLine: 1, + endLine: 1, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented type', () => { @@ -204,11 +233,14 @@ describe('processJsDocs', () => { 'filled-documentation/types-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.types).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.types).toStrictEqual([ + { + path: expect.pathToEndWith('types-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); it('should detect undocumented enum', () => { @@ -217,18 +249,21 @@ describe('processJsDocs', () => { 'missing-documentation/enums-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.enums).toStrictEqual({ - coverage: 0, - nodesCount: 1, - issues: [ - { - file: expect.pathToEndWith('enums-coverage.ts'), - type: 'enums', - name: 'ExampleEnum', - line: 1, - }, - ], - } satisfies DocumentationDataCovered); + expect(results.enums).toStrictEqual([ + { + path: expect.pathToEndWith('enums-coverage.ts'), + covered: 0, + total: 1, + missing: [ + { + kind: 'enum', + name: 'ExampleEnum', + startLine: 1, + endLine: 1, + }, + ], + }, + ] satisfies FileCoverage[]); }); it('should detect documented enum', () => { @@ -237,10 +272,13 @@ describe('processJsDocs', () => { 'filled-documentation/enums-coverage.ts', ); const results = processJsDocs({ patterns: [sourcePath] }); - expect(results.enums).toStrictEqual({ - coverage: 100, - nodesCount: 1, - issues: [], - } satisfies DocumentationDataCovered); + expect(results.enums).toStrictEqual([ + { + path: expect.pathToEndWith('enums-coverage.ts'), + covered: 1, + total: 1, + missing: [], + }, + ] satisfies FileCoverage[]); }); }); diff --git a/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts b/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts index ba26d14e0..c5038a812 100644 --- a/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts +++ b/packages/plugin-jsdocs/src/lib/runner/doc-processor.ts @@ -6,22 +6,24 @@ import { SyntaxKind, VariableStatement, } from 'ts-morph'; -import { objectFromEntries, objectToEntries } from '@code-pushup/utils'; +import { + type FileCoverage, + objectFromEntries, + objectToEntries, +} from '@code-pushup/utils'; import type { JsDocsPluginTransformedConfig } from '../config.js'; -import type { - DocumentationCoverageReport, - DocumentationReport, -} from './models.js'; +import type { CoverageType } from './models.js'; import { - calculateCoverage, - createEmptyCoverageData, + createInitialCoverageTypesRecord, getCoverageTypeFromKind, + singularCoverageType, } from './utils.js'; type Node = { getKind: () => SyntaxKind; getName: () => string | undefined; getStartLineNumber: () => number; + getEndLineNumber: () => number; getJsDocs: () => JSDoc[]; }; @@ -32,13 +34,14 @@ type Node = { */ export function getVariablesInformation( variableStatements: VariableStatement[], -) { +): Node[] { return variableStatements.flatMap(variable => { // Get parent-level information const parentInfo = { getKind: () => variable.getKind(), getJsDocs: () => variable.getJsDocs(), getStartLineNumber: () => variable.getStartLineNumber(), + getEndLineNumber: () => variable.getEndLineNumber(), }; // Map each declaration to combine parent info with declaration-specific info @@ -56,7 +59,7 @@ export function getVariablesInformation( */ export function processJsDocs( config: JsDocsPluginTransformedConfig, -): DocumentationCoverageReport { +): Record { const project = new Project(); project.addSourceFilesAtPaths(config.patterns); return getDocumentationReport(project.getSourceFiles()); @@ -82,26 +85,18 @@ export function getAllNodesFromASourceFile(sourceFile: SourceFile) { */ export function getDocumentationReport( sourceFiles: SourceFile[], -): DocumentationCoverageReport { - const unprocessedCoverageReport = sourceFiles.reduce( - (coverageReportOfAllFiles, sourceFile) => { - const filePath = sourceFile.getFilePath(); - const allNodesFromFile = getAllNodesFromASourceFile(sourceFile); - - const coverageReportOfCurrentFile = getCoverageFromAllNodesOfFile( - allNodesFromFile, - filePath, - ); - - return mergeDocumentationReports( - coverageReportOfAllFiles, - coverageReportOfCurrentFile, - ); - }, - createEmptyCoverageData(), - ); - - return calculateCoverage(unprocessedCoverageReport); +): Record { + return sourceFiles.reduce((acc, sourceFile) => { + const filePath = sourceFile.getFilePath(); + const nodes = getAllNodesFromASourceFile(sourceFile); + const coverageTypes = getCoverageFromAllNodesOfFile(nodes, filePath); + return objectFromEntries( + objectToEntries(coverageTypes).map(([type, file]) => [ + type, + [...acc[type], file], + ]), + ); + }, createInitialCoverageTypesRecord([])); } /** @@ -111,54 +106,39 @@ export function getDocumentationReport( * @returns The coverage report for the nodes */ function getCoverageFromAllNodesOfFile(nodes: Node[], filePath: string) { - return nodes.reduce((acc: DocumentationReport, node: Node) => { - const nodeType = getCoverageTypeFromKind(node.getKind()); - const currentTypeReport = acc[nodeType]; - const updatedIssues = - node.getJsDocs().length === 0 - ? [ - ...currentTypeReport.issues, - { - file: filePath, - type: nodeType, - name: node.getName() || '', - line: node.getStartLineNumber(), - }, - ] - : currentTypeReport.issues; + return nodes.reduce( + (acc: Record, node: Node) => { + const nodeType = getCoverageTypeFromKind(node.getKind()); + const isCovered = node.getJsDocs().length > 0; - return { - ...acc, - [nodeType]: { - nodesCount: currentTypeReport.nodesCount + 1, - issues: updatedIssues, - }, - }; - }, createEmptyCoverageData()); -} - -/** - * Merges two documentation results - * @param accumulatedReport - The first empty documentation result - * @param currentFileReport - The second documentation result - * @returns The merged documentation result - */ -export function mergeDocumentationReports( - accumulatedReport: DocumentationReport, - currentFileReport: Partial, -): DocumentationReport { - return objectFromEntries( - objectToEntries(accumulatedReport).map(([key, value]) => { - const node = value; - const type = key; - return [ - type, - { - nodesCount: - node.nodesCount + (currentFileReport[type]?.nodesCount ?? 0), - issues: [...node.issues, ...(currentFileReport[type]?.issues ?? [])], + return { + ...acc, + [nodeType]: { + ...acc[nodeType], + total: acc[nodeType].total + 1, + ...(isCovered + ? { + covered: acc[nodeType].covered + 1, + } + : { + missing: [ + ...acc[nodeType].missing, + { + kind: singularCoverageType(nodeType), + name: node.getName(), + startLine: node.getStartLineNumber(), + endLine: node.getEndLineNumber(), + }, + ], + }), }, - ]; + }; + }, + createInitialCoverageTypesRecord({ + path: filePath, + covered: 0, + total: 0, + missing: [], }), ); } diff --git a/packages/plugin-jsdocs/src/lib/runner/doc-processor.unit.test.ts b/packages/plugin-jsdocs/src/lib/runner/doc-processor.unit.test.ts index 4200216ad..3fb57a1ae 100644 --- a/packages/plugin-jsdocs/src/lib/runner/doc-processor.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/runner/doc-processor.unit.test.ts @@ -1,4 +1,5 @@ import type { ClassDeclaration, VariableStatement } from 'ts-morph'; +import type { CoverageTreeMissingLOC } from '@code-pushup/models'; import { nodeMock } from '../../../mocks/node.mock.js'; import { sourceFileMock } from '../../../mocks/source-files.mock.js'; import { @@ -6,9 +7,7 @@ import { getClassNodes, getDocumentationReport, getVariablesInformation, - mergeDocumentationReports, } from './doc-processor.js'; -import type { DocumentationReport } from './models.js'; describe('getDocumentationReport', () => { it('should produce a full report', () => { @@ -38,147 +37,36 @@ describe('getDocumentationReport', () => { sourceFileMock('test.ts', { functions: { 1: true, 2: true, 3: false } }), ]); - expect(results.functions.nodesCount).toBe(3); + expect(results.functions).toHaveLength(1); + expect(results.functions[0]!.total).toBe(3); }); - it('should collect uncommented nodes issues', () => { + it('should collect uncommented nodes', () => { const results = getDocumentationReport([ sourceFileMock('test.ts', { functions: { 1: true, 2: false, 3: false } }), ]); - expect(results.functions.issues).toHaveLength(2); + expect(results.functions).toHaveLength(1); + expect(results.functions[0]!.missing).toHaveLength(2); }); - it('should collect valid issues', () => { + it('should collect valid missing nodes', () => { const results = getDocumentationReport([ sourceFileMock('test.ts', { functions: { 1: false } }), ]); - expect(results.functions.issues).toStrictEqual([ + expect(results.functions).toHaveLength(1); + expect(results.functions[0]!.missing).toStrictEqual< + CoverageTreeMissingLOC[] + >([ { - line: 1, - file: 'test.ts', - type: 'functions', + startLine: 1, + endLine: 1, + kind: 'function', name: 'test', }, ]); }); - - it('should calculate coverage correctly', () => { - const results = getDocumentationReport([ - sourceFileMock('test.ts', { functions: { 1: true, 2: false } }), - ]); - - expect(results.functions.coverage).toBe(50); - }); -}); - -describe('mergeDocumentationReports', () => { - const emptyResult: DocumentationReport = { - enums: { nodesCount: 0, issues: [] }, - interfaces: { nodesCount: 0, issues: [] }, - types: { nodesCount: 0, issues: [] }, - functions: { nodesCount: 0, issues: [] }, - variables: { nodesCount: 0, issues: [] }, - classes: { nodesCount: 0, issues: [] }, - methods: { nodesCount: 0, issues: [] }, - properties: { nodesCount: 0, issues: [] }, - }; - - it.each([ - 'enums', - 'interfaces', - 'types', - 'functions', - 'variables', - 'classes', - 'methods', - 'properties', - ])('should merge results on top-level property: %s', type => { - const secondResult = { - [type]: { - nodesCount: 1, - issues: [{ file: 'test2.ts', line: 1, name: 'test2', type }], - }, - }; - - const results = mergeDocumentationReports( - emptyResult, - secondResult as Partial, - ); - expect(results).toStrictEqual( - expect.objectContaining({ - [type]: { - nodesCount: 1, - issues: [{ file: 'test2.ts', line: 1, name: 'test2', type }], - }, - }), - ); - }); - - it('should merge empty results', () => { - const results = mergeDocumentationReports(emptyResult, emptyResult); - expect(results).toStrictEqual(emptyResult); - }); - - it('should merge second level property nodesCount', () => { - const results = mergeDocumentationReports( - { - ...emptyResult, - enums: { nodesCount: 1, issues: [] }, - }, - { - enums: { nodesCount: 1, issues: [] }, - }, - ); - expect(results.enums.nodesCount).toBe(2); - }); - - it('should merge second level property issues', () => { - const results = mergeDocumentationReports( - { - ...emptyResult, - enums: { - nodesCount: 0, - issues: [ - { - file: 'file.enum-first.ts', - line: 6, - name: 'file.enum-first', - type: 'enums', - }, - ], - }, - }, - { - enums: { - nodesCount: 0, - issues: [ - { - file: 'file.enum-second.ts', - line: 5, - name: 'file.enum-second', - type: 'enums', - }, - ], - }, - }, - ); - expect(results.enums.issues).toStrictEqual([ - { - file: 'file.enum-first.ts', - line: 6, - name: 'file.enum-first', - type: 'enums', - }, - { - file: 'file.enum-second.ts', - line: 5, - name: 'file.enum-second', - type: 'enums', - }, - ]); - }); }); describe('getClassNodes', () => { @@ -210,6 +98,7 @@ describe('getVariablesInformation', () => { getKind: () => 'const', getJsDocs: () => ['some docs'], getStartLineNumber: () => 42, + getEndLineNumber: () => 44, getDeclarations: () => [mockDeclaration], }; @@ -222,6 +111,7 @@ describe('getVariablesInformation', () => { getKind: expect.any(Function), getJsDocs: expect.any(Function), getStartLineNumber: expect.any(Function), + getEndLineNumber: expect.any(Function), getName: expect.any(Function), }); // It must be defined @@ -229,6 +119,7 @@ describe('getVariablesInformation', () => { expect(result[0]!.getKind()).toBe('const'); expect(result[0]!.getJsDocs()).toEqual(['some docs']); expect(result[0]!.getStartLineNumber()).toBe(42); + expect(result[0]!.getEndLineNumber()).toBe(44); }); it('should handle multiple declarations in a single variable statement', () => { @@ -241,6 +132,7 @@ describe('getVariablesInformation', () => { getKind: () => 'let', getJsDocs: () => [], getStartLineNumber: () => 10, + getEndLineNumber: () => 10, getDeclarations: () => mockDeclarations, }; @@ -266,6 +158,7 @@ describe('getVariablesInformation', () => { getKind: () => 'const', getJsDocs: () => [], getStartLineNumber: () => 1, + getEndLineNumber: () => 1, getDeclarations: () => [], }; diff --git a/packages/plugin-jsdocs/src/lib/runner/models.ts b/packages/plugin-jsdocs/src/lib/runner/models.ts index 6da638ece..a48effe80 100644 --- a/packages/plugin-jsdocs/src/lib/runner/models.ts +++ b/packages/plugin-jsdocs/src/lib/runner/models.ts @@ -8,29 +8,3 @@ export type CoverageType = | 'variables' | 'properties' | 'types'; - -/** The undocumented node is the node that is not documented and has the information for the report. */ -export type UndocumentedNode = { - file: string; - type: CoverageType; - name: string; - line: number; - class?: string; -}; - -/** The documentation data has the issues and the total nodes count from a specific CoverageType. */ -export type DocumentationData = { - issues: UndocumentedNode[]; - nodesCount: number; -}; - -/** The documentation report has all the documentation data for each coverage type. */ -export type DocumentationReport = Record; - -/** The processed documentation result has the documentation data for each coverage type and with coverage stats. */ -export type DocumentationCoverageReport = Record< - CoverageType, - DocumentationData & { - coverage: number; - } ->; diff --git a/packages/plugin-jsdocs/src/lib/runner/runner.integration.test.ts b/packages/plugin-jsdocs/src/lib/runner/runner.integration.test.ts index 38cc4b954..8afdecd95 100644 --- a/packages/plugin-jsdocs/src/lib/runner/runner.integration.test.ts +++ b/packages/plugin-jsdocs/src/lib/runner/runner.integration.test.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import type { AuditOutput, CoverageTree } from '@code-pushup/models'; import { AUDITS_MAP } from '../constants.js'; import { createRunnerFunction } from './runner.js'; @@ -19,7 +20,7 @@ describe('createRunnerFunction', () => { ]); it.each(AUDIT_SLUGS)( - 'should generate issues for %s coverage if undocumented', + 'should calculate %s coverage when undocumented', async coverageType => { const filePath = path.join( fixturesDir, @@ -33,35 +34,28 @@ describe('createRunnerFunction', () => { expect( results.find(({ slug }) => slug === `${coverageType}-coverage`), - ).toStrictEqual( - expect.objectContaining({ - slug: `${coverageType}-coverage`, - score: 0, - value: 1, - displayValue: `1 undocumented ${coverageType}`, - details: { - issues: [ - expect.objectContaining({ - message: expect.stringContaining( - `Missing ${coverageType} documentation for`, - ), - severity: 'warning', - source: { - file: expect.stringContaining(path.basename(filePath)), - position: { - startLine: expect.any(Number), - }, - }, - }), - ], - }, - }), - ); + ).toStrictEqual({ + slug: `${coverageType}-coverage`, + score: 0, + value: 1, + displayValue: `1 undocumented ${coverageType}`, + details: { + trees: [ + expect.objectContaining>({ + root: { + name: '.', + values: { coverage: 0 }, + children: [expect.any(Object)], + }, + }), + ], + }, + }); }, ); it.each(AUDIT_SLUGS)( - 'should not generate issues for %s coverage if documented', + 'should calculate %s coverage when documented', async coverageType => { const filePath = path.join( fixturesDir, @@ -75,17 +69,23 @@ describe('createRunnerFunction', () => { expect( results.find(({ slug }) => slug === `${coverageType}-coverage`), - ).toStrictEqual( - expect.objectContaining({ - slug: `${coverageType}-coverage`, - score: 1, - value: 0, - displayValue: `0 undocumented ${coverageType}`, - details: { - issues: [], - }, - }), - ); + ).toStrictEqual({ + slug: `${coverageType}-coverage`, + score: 1, + value: 0, + displayValue: `0 undocumented ${coverageType}`, + details: { + trees: [ + expect.objectContaining>({ + root: { + name: '.', + values: { coverage: 1 }, + children: [expect.any(Object)], + }, + }), + ], + }, + }); }, ); diff --git a/packages/plugin-jsdocs/src/lib/runner/runner.ts b/packages/plugin-jsdocs/src/lib/runner/runner.ts index 811e751be..c2be2e73c 100644 --- a/packages/plugin-jsdocs/src/lib/runner/runner.ts +++ b/packages/plugin-jsdocs/src/lib/runner/runner.ts @@ -1,15 +1,27 @@ import type { AuditOutputs, RunnerFunction } from '@code-pushup/models'; +import { + type FileCoverage, + filesCoverageToTree, + getGitRoot, + objectToEntries, + toNumberPrecision, +} from '@code-pushup/utils'; import type { JsDocsPluginTransformedConfig } from '../config.js'; import { processJsDocs } from './doc-processor.js'; -import type { CoverageType, DocumentationCoverageReport } from './models.js'; +import type { CoverageType } from './models.js'; import { coverageTypeToAuditSlug } from './utils.js'; export function createRunnerFunction( config: JsDocsPluginTransformedConfig, ): RunnerFunction { - return (): AuditOutputs => { + return async (): Promise => { const coverageResult = processJsDocs(config); - return trasformCoverageReportToAuditOutputs(coverageResult, config); + const gitRoot = await getGitRoot(); + return trasformCoverageReportToAuditOutputs( + coverageResult, + config, + gitRoot, + ); }; } @@ -17,15 +29,17 @@ export function createRunnerFunction( * Transforms the coverage report into audit outputs. * @param coverageResult - The coverage result containing undocumented items and coverage statistics * @param options - Configuration options specifying which audits to include and exclude + * @param gitRoot - Root directory in repo for relative file paths * @returns Audit outputs with coverage scores and details about undocumented items */ export function trasformCoverageReportToAuditOutputs( - coverageResult: DocumentationCoverageReport, + coverageResult: Record, options: Pick, + gitRoot: string, ): AuditOutputs { - return Object.entries(coverageResult) + return objectToEntries(coverageResult) .filter(([type]) => { - const auditSlug = coverageTypeToAuditSlug(type as CoverageType); + const auditSlug = coverageTypeToAuditSlug(type); if (options.onlyAudits?.length) { return options.onlyAudits.includes(auditSlug); } @@ -34,20 +48,22 @@ export function trasformCoverageReportToAuditOutputs( } return true; }) - .map(([type, item]) => { - const { coverage, issues } = item; + .map(([type, files]) => { + const tree = filesCoverageToTree(files, gitRoot, `Documented ${type}`); + const coverage = tree.root.values.coverage; + const missingCount = files.reduce( + (acc, file) => acc + file.missing.length, + 0, + ); + const MAX_DECIMAL_PLACES = 4; return { slug: `${type}-coverage`, - value: issues.length, - score: coverage / 100, - displayValue: `${issues.length} undocumented ${type}`, + value: missingCount, + score: toNumberPrecision(coverage, MAX_DECIMAL_PLACES), + displayValue: `${missingCount} undocumented ${type}`, details: { - issues: item.issues.map(({ file, line, name }) => ({ - message: `Missing ${type} documentation for ${name}`, - source: { file, position: { startLine: line } }, - severity: 'warning', - })), + trees: [tree], }, }; }); diff --git a/packages/plugin-jsdocs/src/lib/runner/runner.unit.test.ts b/packages/plugin-jsdocs/src/lib/runner/runner.unit.test.ts index dee74fdc9..863624aad 100644 --- a/packages/plugin-jsdocs/src/lib/runner/runner.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/runner/runner.unit.test.ts @@ -1,36 +1,33 @@ -import type { DocumentationCoverageReport } from './models.js'; +import type { FileCoverage } from '@code-pushup/utils'; +import type { CoverageType } from './models.js'; import { trasformCoverageReportToAuditOutputs } from './runner.js'; describe('trasformCoverageReportToAudits', () => { const mockCoverageResult = { - functions: { - coverage: 75, - nodesCount: 4, - issues: [ - { - file: 'test.ts', - line: 10, - name: 'testFunction', - type: 'functions', - }, - ], - }, - classes: { - coverage: 100, - nodesCount: 2, - issues: [ - { - file: 'test.ts', - line: 10, - name: 'testClass', - type: 'classes', - }, - ], - }, - } as DocumentationCoverageReport; + functions: [ + { + path: 'test.ts', + covered: 3, + total: 4, + missing: [{ startLine: 10, kind: 'function', name: 'testFunction' }], + }, + ], + classes: [ + { + path: 'test.ts', + covered: 2, + total: 2, + missing: [{ startLine: 10, kind: 'class', name: 'testClass' }], + }, + ], + } as Record; it('should return all audits from the coverage result when no filters are provided', () => { - const result = trasformCoverageReportToAuditOutputs(mockCoverageResult, {}); + const result = trasformCoverageReportToAuditOutputs( + mockCoverageResult, + {}, + process.cwd(), + ); expect(result.map(item => item.slug)).toStrictEqual([ 'functions-coverage', 'classes-coverage', @@ -38,38 +35,46 @@ describe('trasformCoverageReportToAudits', () => { }); it('should filter audits when onlyAudits is provided', () => { - const result = trasformCoverageReportToAuditOutputs(mockCoverageResult, { - onlyAudits: ['functions-coverage'], - }); + const result = trasformCoverageReportToAuditOutputs( + mockCoverageResult, + { + onlyAudits: ['functions-coverage'], + }, + process.cwd(), + ); expect(result).toHaveLength(1); expect(result.map(item => item.slug)).toStrictEqual(['functions-coverage']); }); it('should filter audits when skipAudits is provided', () => { - const result = trasformCoverageReportToAuditOutputs(mockCoverageResult, { - skipAudits: ['functions-coverage'], - }); + const result = trasformCoverageReportToAuditOutputs( + mockCoverageResult, + { + skipAudits: ['functions-coverage'], + }, + process.cwd(), + ); expect(result).toHaveLength(1); expect(result.map(item => item.slug)).toStrictEqual(['classes-coverage']); }); it('should handle properly empty coverage result', () => { const result = trasformCoverageReportToAuditOutputs( - {} as unknown as DocumentationCoverageReport, + {} as Record, {}, + process.cwd(), ); expect(result).toEqual([]); }); - it('should handle coverage result with multiple issues and add them to the details.issue of the report', () => { - const expectedIssues = 2; - const result = trasformCoverageReportToAuditOutputs(mockCoverageResult, {}); + it('should calculate coverage for multiple node types', () => { + const result = trasformCoverageReportToAuditOutputs( + mockCoverageResult, + {}, + process.cwd(), + ); expect(result).toHaveLength(2); - expect( - result.reduce( - (acc, item) => acc + (item.details?.issues?.length ?? 0), - 0, - ), - ).toBe(expectedIssues); + expect(result[0]!.score).toBe(0.75); + expect(result[1]!.score).toBe(1); }); }); diff --git a/packages/plugin-jsdocs/src/lib/runner/utils.ts b/packages/plugin-jsdocs/src/lib/runner/utils.ts index 7729ab407..f9d4c1fa9 100644 --- a/packages/plugin-jsdocs/src/lib/runner/utils.ts +++ b/packages/plugin-jsdocs/src/lib/runner/utils.ts @@ -1,25 +1,24 @@ import { SyntaxKind } from 'ts-morph'; import { SYNTAX_COVERAGE_MAP } from './constants.js'; -import type { - CoverageType, - DocumentationCoverageReport, - DocumentationReport, -} from './models.js'; +import type { CoverageType } from './models.js'; /** * Creates an empty unprocessed coverage report. + * @param initialValue - Initial value for each coverage type * @returns The empty unprocessed coverage report. */ -export function createEmptyCoverageData(): DocumentationReport { +export function createInitialCoverageTypesRecord( + initialValue: T, +): Record { return { - enums: { nodesCount: 0, issues: [] }, - interfaces: { nodesCount: 0, issues: [] }, - types: { nodesCount: 0, issues: [] }, - functions: { nodesCount: 0, issues: [] }, - variables: { nodesCount: 0, issues: [] }, - classes: { nodesCount: 0, issues: [] }, - methods: { nodesCount: 0, issues: [] }, - properties: { nodesCount: 0, issues: [] }, + enums: initialValue, + interfaces: initialValue, + types: initialValue, + functions: initialValue, + variables: initialValue, + classes: initialValue, + methods: initialValue, + properties: initialValue, }; } @@ -32,34 +31,6 @@ export function coverageTypeToAuditSlug(type: CoverageType) { return `${type}-coverage`; } -/** - * Calculates the coverage percentage for each coverage type. - * @param result - The unprocessed coverage result. - * @returns The processed coverage result. - */ -export function calculateCoverage(result: DocumentationReport) { - return Object.fromEntries( - Object.entries(result).map(([key, value]) => { - const type = key as CoverageType; - return [ - type, - { - coverage: - value.nodesCount === 0 - ? 100 - : Number( - ((1 - value.issues.length / value.nodesCount) * 100).toFixed( - 2, - ), - ), - issues: value.issues, - nodesCount: value.nodesCount, - }, - ]; - }), - ) as DocumentationCoverageReport; -} - /** * Maps the SyntaxKind from the library ts-morph to the coverage type. * @param kind - The SyntaxKind from the library ts-morph. @@ -73,3 +44,29 @@ export function getCoverageTypeFromKind(kind: SyntaxKind): CoverageType { } return type; } + +/** + * Convert plural coverage type to singular form + * @param type Coverage type (plural) + * @returns Singular form of coverage type + */ +export function singularCoverageType(type: CoverageType): string { + switch (type) { + case 'classes': + return 'class'; + case 'enums': + return 'enum'; + case 'functions': + return 'function'; + case 'interfaces': + return 'interface'; + case 'methods': + return 'method'; + case 'properties': + return 'property'; + case 'types': + return 'type'; + case 'variables': + return 'variable'; + } +} diff --git a/packages/plugin-jsdocs/src/lib/runner/utils.unit.test.ts b/packages/plugin-jsdocs/src/lib/runner/utils.unit.test.ts index fcf8e2f33..d6a688b9c 100644 --- a/packages/plugin-jsdocs/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/runner/utils.unit.test.ts @@ -1,67 +1,26 @@ import { SyntaxKind } from 'ts-morph'; -import type { DocumentationReport } from './models.js'; import { - calculateCoverage, - createEmptyCoverageData, + createInitialCoverageTypesRecord, getCoverageTypeFromKind, } from './utils.js'; -describe('createEmptyCoverageData', () => { +describe('createInitialCoverageTypesRecord', () => { it('should create an empty report with all categories initialized', () => { - const result = createEmptyCoverageData(); + const result = createInitialCoverageTypesRecord([]); expect(result).toStrictEqual({ - enums: { nodesCount: 0, issues: [] }, - interfaces: { nodesCount: 0, issues: [] }, - types: { nodesCount: 0, issues: [] }, - functions: { nodesCount: 0, issues: [] }, - variables: { nodesCount: 0, issues: [] }, - classes: { nodesCount: 0, issues: [] }, - methods: { nodesCount: 0, issues: [] }, - properties: { nodesCount: 0, issues: [] }, + enums: [], + interfaces: [], + types: [], + functions: [], + variables: [], + classes: [], + methods: [], + properties: [], }); }); }); -describe('calculateCoverage', () => { - it('should calculate 100% coverage when there are no nodes', () => { - const input = createEmptyCoverageData(); - const result = calculateCoverage(input); - - Object.values(result).forEach(category => { - expect(category.coverage).toBe(100); - expect(category.nodesCount).toBe(0); - expect(category.issues).toEqual([]); - }); - }); - - it('should calculate correct coverage percentage with issues', () => { - const input: DocumentationReport = { - ...createEmptyCoverageData(), - functions: { - nodesCount: 4, - issues: [ - { type: 'functions', line: 1, file: 'test.ts', name: 'fn1' }, - { type: 'functions', line: 2, file: 'test.ts', name: 'fn2' }, - ], - }, - classes: { - nodesCount: 4, - issues: [ - { type: 'classes', line: 1, file: 'test.ts', name: 'Class1' }, - { type: 'classes', line: 2, file: 'test.ts', name: 'Class2' }, - { type: 'classes', line: 3, file: 'test.ts', name: 'Class3' }, - ], - }, - }; - - const result = calculateCoverage(input); - - expect(result.functions.coverage).toBe(50); - expect(result.classes.coverage).toBe(25); - }); -}); - describe('getCoverageTypeFromKind', () => { it.each([ [SyntaxKind.ClassDeclaration, 'classes'], diff --git a/packages/plugin-jsdocs/tsconfig.test.json b/packages/plugin-jsdocs/tsconfig.test.json index 9f29d6bb0..211f44ad2 100644 --- a/packages/plugin-jsdocs/tsconfig.test.json +++ b/packages/plugin-jsdocs/tsconfig.test.json @@ -8,6 +8,7 @@ "vite.config.unit.ts", "vite.config.integration.ts", "mocks/**/*.ts", - "src/**/*.test.ts" + "src/**/*.test.ts", + "../../testing/test-setup/src/vitest.d.ts" ] }