diff --git a/dist/index.js b/dist/index.js index d2c4f59..f521b5f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -9505,6 +9505,71 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 9029: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.FileAnnotations = void 0; +/** + * Collect annotations for a single file + */ +class FileAnnotations { + constructor(filepath, filePrefix) { + this.annotations = {}; + // Strip path prefix off filepath for annotation + if (filePrefix.length) { + if (filepath.startsWith(filePrefix)) { + filepath = filepath.substring(filePrefix.length); + } + else { + console.warn(`The coverage working directory '${filePrefix}' does not exist on coverage file entry: ${filepath}.`); + } + } + this.filepath = filepath; + } + /** + * Get existing partial annotation for a line + */ + getAnnotation(line) { + let annotation = this.annotations[line]; + if (!annotation) { + annotation = { + line, + message: [], + }; + this.annotations[line] = annotation; + } + return annotation; + } + /** + * Define an annotation type for a line + */ + addAnnotation(type, line, column) { + const annotation = this.getAnnotation(line); + annotation.message.push(`${type} starting at column ${column}`); + } + /** + * Get all full annotations + */ + getAnnotations() { + return Object.values(this.annotations).map(({ line, message }) => ({ + title: "Line missing test coverage", + start_line: line, + end_line: line, + start_column: 0, + path: this.filepath, + annotation_level: "warning", + message: message.join("\n").trim(), + })); + } +} +exports.FileAnnotations = FileAnnotations; + + /***/ }), /***/ 6144: @@ -9677,36 +9742,18 @@ main(); /***/ }), /***/ 5541: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseCoverage = void 0; +const FileAnnotation_1 = __nccwpck_require__(9029); /** * Read the test coverage JSON and stream positions that are missing coverage. */ function parseCoverage(coverage, files, filePrefix = "") { - const annotations = []; - const addAnnotation = (annotation) => { - // Remove null value - if (annotation.end_column === null) { - delete annotation.end_column; - } - if (annotation.start_column === null) { - delete annotation.start_column; - } - // Remove column values if start/end lines are not the same - if (annotation.start_line !== annotation.end_line) { - if (typeof annotation.end_column !== "undefined") { - delete annotation.end_column; - } - if (typeof annotation.start_column !== "undefined") { - delete annotation.start_column; - } - } - annotations.push(annotation); - }; + let annotations = []; for (let filepath of files) { // Get coverage for file if (typeof coverage[filepath] === "undefined") { @@ -9715,35 +9762,19 @@ function parseCoverage(coverage, files, filePrefix = "") { } const fileCoverage = coverage[filepath]; console.warn(`Checking coverage for ${filepath}`); - // Strip path prefix off filepath for annotation - let annotationPath = filepath; - if (filePrefix.length) { - if (annotationPath.startsWith(filePrefix)) { - annotationPath = annotationPath.substring(filePrefix.length); - } - else { - console.warn(`The coverage working directory '${filePrefix}' does not exist on coverage file entry: ${annotationPath}.`); - } - } - // Base annotation object - const base = { - path: annotationPath, - annotation_level: "warning", - }; + const fileAnnotations = new FileAnnotation_1.FileAnnotations(filepath, filePrefix); // Statements for (const [id, count] of Object.entries(fileCoverage.s)) { if (count === 0) { const statement = fileCoverage.statementMap[id]; - const message = "This statement lacks test coverage"; - addAnnotation(Object.assign(Object.assign({}, base), { message, start_line: statement.start.line, start_column: statement.start.column, end_line: statement.end.line, end_column: statement.end.column })); + fileAnnotations.addAnnotation("statement", statement.start.line, statement.start.column); } } // Functions for (const [id, count] of Object.entries(fileCoverage.f)) { if (count === 0) { const func = fileCoverage.fnMap[id]; - const message = "This function lacks test coverage"; - addAnnotation(Object.assign(Object.assign({}, base), { message, start_line: func.decl.start.line, start_column: func.decl.start.column, end_line: func.loc.end.line, end_column: func.loc.end.column || 0 })); + fileAnnotations.addAnnotation("function", func.decl.start.line, func.decl.start.column); } } // Branches @@ -9752,11 +9783,11 @@ function parseCoverage(coverage, files, filePrefix = "") { const count = counts[i]; if (count === 0) { const branch = fileCoverage.branchMap[id]; - const message = "This branch lacks test coverage"; - addAnnotation(Object.assign(Object.assign({}, base), { message, start_line: branch.locations[i].start.line, start_column: branch.locations[i].start.column, end_line: branch.locations[i].end.line, end_column: branch.locations[i].end.column || 0 })); + fileAnnotations.addAnnotation("branch", branch.locations[i].start.line, branch.locations[i].start.column); } } } + annotations = [...annotations, ...fileAnnotations.getAnnotations()]; } return annotations; } diff --git a/package.json b/package.json index a3ec136..eeb3e5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "test-coverage-annotations", - "version": "0.0.1", + "version": "1.0.2", "description": "A github action that add file annotations to areas of code lacking test coverage", "main": "dist/index.ts", "scripts": { diff --git a/src/FileAnnotation.ts b/src/FileAnnotation.ts new file mode 100644 index 0000000..916737f --- /dev/null +++ b/src/FileAnnotation.ts @@ -0,0 +1,72 @@ +import type { Annotation } from "./types"; + +type AnnotationType = "statement" | "function" | "branch"; + +type PartialAnnotation = { + line: number; + message: string[]; +}; + +/** + * Collect annotations for a single file + */ +export class FileAnnotations { + filepath: string; + annotations: Record; + + constructor(filepath: string, filePrefix: string) { + this.annotations = {}; + + // Strip path prefix off filepath for annotation + if (filePrefix.length) { + if (filepath.startsWith(filePrefix)) { + filepath = filepath.substring(filePrefix.length); + } else { + console.warn( + `The coverage working directory '${filePrefix}' does not exist on coverage file entry: ${filepath}.` + ); + } + } + this.filepath = filepath; + } + + /** + * Get existing partial annotation for a line + */ + getAnnotation(line: number): PartialAnnotation { + let annotation = this.annotations[line]; + if (!annotation) { + annotation = { + line, + message: [], + }; + this.annotations[line] = annotation; + } + return annotation; + } + + /** + * Define an annotation type for a line + */ + addAnnotation(type: AnnotationType, line: number, column: number) { + const annotation = this.getAnnotation(line); + annotation.message.push(`${type} starting at column ${column}`); + } + + /** + * Get all full annotations + */ + getAnnotations() { + return Object.values(this.annotations).map( + ({ line, message }: PartialAnnotation): Annotation => ({ + title: "Line missing test coverage", + start_line: line, + end_line: line, + start_column: 0, + path: this.filepath, + annotation_level: "warning", + message: message.join("\n").trim(), + }) + ); + } +} diff --git a/src/parseCoverage.test.ts b/src/parseCoverage.test.ts index a2ac6b8..8887861 100644 --- a/src/parseCoverage.test.ts +++ b/src/parseCoverage.test.ts @@ -18,52 +18,70 @@ describe("parseCoverage", () => { { path: "/test/src/file1.ts", annotation_level: "warning", - message: "This statement lacks test coverage", + message: "statement starting at column 0", start_line: 2, - start_column: 0, end_line: 2, - end_column: 30, + start_column: 0, + title: "Line missing test coverage", }, { path: "/test/src/file1.ts", annotation_level: "warning", - message: "This function lacks test coverage", + message: "function starting at column 0", start_line: 3, + end_line: 3, start_column: 0, - end_line: 4, + title: "Line missing test coverage", }, { path: "/test/src/file1.ts", annotation_level: "warning", - message: "This branch lacks test coverage", + message: "branch starting at column 10", start_line: 6, - start_column: 10, end_line: 6, - end_column: 20, + start_column: 0, + title: "Line missing test coverage", }, { - path: "/test/src/file2.ts", + path: "/test/src/file1.ts", annotation_level: "warning", - message: "This statement lacks test coverage", + message: [ + "statement starting at column 0", + "function starting at column 2", + "branch starting at column 10", + "branch starting at column 30", + ].join("\n"), + start_line: 8, + end_line: 8, + start_column: 0, + title: "Line missing test coverage", + }, + { + title: "Line missing test coverage", start_line: 1, + end_line: 1, start_column: 0, - end_line: 2, + path: "/test/src/file2.ts", + annotation_level: "warning", + message: "statement starting at column 0", }, { path: "/test/src/file2.ts", annotation_level: "warning", - message: "This function lacks test coverage", + message: "function starting at column 2", start_line: 4, - start_column: 2, - end_line: 5, + end_line: 4, + start_column: 0, + title: "Line missing test coverage", }, { path: "/test/src/file2.ts", annotation_level: "warning", - message: "This branch lacks test coverage", + message: "branch starting at column 30", start_line: 5, - start_column: 30, - end_line: 6, + end_line: 5, + start_column: 0, + title: "Line missing test coverage", }, ]); }); diff --git a/src/parseCoverage.ts b/src/parseCoverage.ts index a6fc44a..20378df 100644 --- a/src/parseCoverage.ts +++ b/src/parseCoverage.ts @@ -1,3 +1,4 @@ +import { FileAnnotations } from "./FileAnnotation"; import type { Coverage, Annotation } from "./types"; /** @@ -8,27 +9,7 @@ export function parseCoverage( files: string[], filePrefix: string = "" ): Annotation[] { - const annotations: Annotation[] = []; - - const addAnnotation = (annotation: Annotation) => { - // Remove null value - if (annotation.end_column === null) { - delete annotation.end_column; - } - if (annotation.start_column === null) { - delete annotation.start_column; - } - // Remove column values if start/end lines are not the same - if (annotation.start_line !== annotation.end_line) { - if (typeof annotation.end_column !== "undefined") { - delete annotation.end_column; - } - if (typeof annotation.start_column !== "undefined") { - delete annotation.start_column; - } - } - annotations.push(annotation); - }; + let annotations: Annotation[] = []; for (let filepath of files) { // Get coverage for file @@ -39,37 +20,17 @@ export function parseCoverage( const fileCoverage = coverage[filepath]; console.warn(`Checking coverage for ${filepath}`); - // Strip path prefix off filepath for annotation - let annotationPath = filepath; - if (filePrefix.length) { - if (annotationPath.startsWith(filePrefix)) { - annotationPath = annotationPath.substring(filePrefix.length); - } else { - console.warn( - `The coverage working directory '${filePrefix}' does not exist on coverage file entry: ${annotationPath}.` - ); - } - } - - // Base annotation object - const base: Pick = { - path: annotationPath, - annotation_level: "warning", - }; + const fileAnnotations = new FileAnnotations(filepath, filePrefix); // Statements for (const [id, count] of Object.entries(fileCoverage.s)) { if (count === 0) { const statement = fileCoverage.statementMap[id]; - const message = "This statement lacks test coverage"; - addAnnotation({ - ...base, - message, - start_line: statement.start.line, - start_column: statement.start.column, - end_line: statement.end.line, - end_column: statement.end.column, - }); + fileAnnotations.addAnnotation( + "statement", + statement.start.line, + statement.start.column + ); } } @@ -77,15 +38,11 @@ export function parseCoverage( for (const [id, count] of Object.entries(fileCoverage.f)) { if (count === 0) { const func = fileCoverage.fnMap[id]; - const message = "This function lacks test coverage"; - addAnnotation({ - ...base, - message, - start_line: func.decl.start.line, - start_column: func.decl.start.column, - end_line: func.loc.end.line, - end_column: func.loc.end.column || 0, - }); + fileAnnotations.addAnnotation( + "function", + func.decl.start.line, + func.decl.start.column + ); } } @@ -95,18 +52,16 @@ export function parseCoverage( const count = counts[i]; if (count === 0) { const branch = fileCoverage.branchMap[id]; - const message = "This branch lacks test coverage"; - addAnnotation({ - ...base, - message, - start_line: branch.locations[i].start.line, - start_column: branch.locations[i].start.column, - end_line: branch.locations[i].end.line, - end_column: branch.locations[i].end.column || 0, - }); + fileAnnotations.addAnnotation( + "branch", + branch.locations[i].start.line, + branch.locations[i].start.column + ); } } } + + annotations = [...annotations, ...fileAnnotations.getAnnotations()]; } return annotations; diff --git a/src/types.ts b/src/types.ts index 3e09844..0e82011 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,10 +7,11 @@ export type Inputs = { export type Annotation = { path: string; + title?: string; message: string; annotation_level: string; start_line: number; - end_line: number; + end_line?: number; start_column?: number; end_column?: number; }; diff --git a/test-coverage.json b/test-coverage.json index e7b17ce..7ef4d61 100644 --- a/test-coverage.json +++ b/test-coverage.json @@ -1,6 +1,6 @@ { "/test/src/file1.ts": { - "path": "/test/src/file2.ts", + "path": "/test/src/file1.ts", "statementMap": { "0": { "start": { @@ -21,6 +21,16 @@ "line": 2, "column": 30 } + }, + "2": { + "start": { + "line": 8, + "column": 0 + }, + "end": { + "line": 8, + "column": 30 + } } }, "fnMap": { @@ -69,6 +79,29 @@ "column": 3 } } + }, + "2": { + "name": "(anonymous_1)", + "decl": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 8 + } + }, + "loc": { + "start": { + "line": 8, + "column": 22 + }, + "end": { + "line": 8, + "column": 3 + } + } } }, "branchMap": { @@ -141,19 +174,57 @@ } } ] + }, + "2": { + "loc": { + "start": { + "line": 8, + "column": 10 + }, + "end": { + "line": 8, + "column": 20 + } + }, + "type": "default-arg", + "locations": [ + { + "start": { + "line": 8, + "column": 10 + }, + "end": { + "line": 8, + "column": 20 + } + }, + { + "start": { + "line": 8, + "column": 30 + }, + "end": { + "line": 8, + "column": 40 + } + } + ] } }, "s": { "0": 1, - "1": 0 + "1": 0, + "2": 0 }, "f": { "0": 0, - "1": 1 + "1": 1, + "2": 0 }, "b": { "0": [1, 1], - "1": [0, 1] + "1": [0, 1], + "2": [0, 0] } }, "/test/src/file2.ts": {