@cspell/cspell-junit-reporter 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 cspell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # `@cspell/cspell-junit-reporter`
2
+
3
+ > CSpell reporter with JUnit XML output
4
+
5
+ Emits a [JUnit](https://github.com/testmoapp/junitxml) compatible XML report of a cspell run, suitable for
6
+ consumption by CI systems (GitHub Actions, GitLab, Jenkins, CircleCI, etc.) that understand the JUnit test
7
+ result format.
8
+
9
+ ## Installation
10
+
11
+ Install it as a development package in the repository that will use it.
12
+
13
+ ```sh
14
+ npm install -SD @cspell/cspell-junit-reporter
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Using Command Line
20
+
21
+ ```sh
22
+ cspell . --reporter @cspell/cspell-junit-reporter
23
+ ```
24
+
25
+ ### Using CSpell Configuration
26
+
27
+ Add this to `cspell.yaml`:
28
+
29
+ ```yaml
30
+ reporters: [['@cspell/cspell-junit-reporter', { outFile: 'cspell-junit.xml' }]]
31
+ ```
32
+
33
+ or `cspell.json`
34
+
35
+ ```json
36
+ {
37
+ "reporters": [["@cspell/cspell-junit-reporter", { "outFile": "cspell-junit.xml" }]]
38
+ }
39
+ ```
40
+
41
+ ## Output format
42
+
43
+ `@cspell/cspell-junit-reporter` maps a cspell run onto JUnit XML as follows:
44
+
45
+ - one `<testsuites>` root element for the whole run
46
+ - one `<testsuite>` per file checked, named after the file
47
+ - one `<testcase>` per spelling issue found in a file, containing a `<failure>` with the misspelled word,
48
+ location, and message
49
+ - a file with no issues gets a single passing `<testcase>` so the suite is never empty
50
+ - a skipped file gets a single `<testcase>` containing a `<skipped>` element
51
+ - processing errors that are not tied to a specific spelling issue (for example, a bad config file) are
52
+ reported in a dedicated `cspell-errors` testsuite using `<error>` instead of `<failure>`
53
+
54
+ All file paths, words, and messages are escaped so the output is valid XML even when they contain
55
+ `& < > " '` characters.
56
+
57
+ Example output for a single file with one issue:
58
+
59
+ ```xml
60
+ <?xml version="1.0" encoding="UTF-8"?>
61
+ <testsuites name="cspell" tests="1" failures="1" errors="0" time="0.001">
62
+ <testsuite name="src/example.ts" tests="1" failures="1" errors="0" time="0.001">
63
+ <testcase name="teh (3:5)" classname="src/example.ts">
64
+ <failure type="spelling" message="Unknown word: &quot;teh&quot;">Unknown word: "teh"</failure>
65
+ </testcase>
66
+ </testsuite>
67
+ </testsuites>
68
+ ```
69
+
70
+ ## Settings
71
+
72
+ Possible settings:
73
+
74
+ - `outFile` (default: stdout) - path for the XML file to emit
75
+ - `suiteName` (default: `cspell`) - name used for the root `<testsuites>` element
76
+
77
+ <details>
78
+ <summary>Reporter Settings</summary>
79
+
80
+ <!--- @@inject: src/CSpellJUnitReporterSettings.ts --->
81
+
82
+ ```ts
83
+ /**
84
+ * cspell-junit-reporter settings type definition
85
+ */
86
+ export type CSpellJUnitReporterSettings = {
87
+ /**
88
+ * Path to the output JUnit XML file.
89
+ *
90
+ * Relative paths are relative to the current working directory.
91
+ *
92
+ * Special values:
93
+ * - `stdout` - write the XML to `stdout`.
94
+ * - `stderr` - write the XML to `stderr`.
95
+ *
96
+ * @default stdout
97
+ */
98
+ outFile?: string;
99
+ /**
100
+ * Name to use for the root `<testsuites>` element.
101
+ *
102
+ * @default cspell
103
+ */
104
+ suiteName?: string;
105
+ };
106
+ ```
107
+
108
+ <!--- @@inject-end: src/CSpellJUnitReporterSettings.ts --->
109
+
110
+ </details>
@@ -0,0 +1,24 @@
1
+ /**
2
+ * cspell-junit-reporter settings type definition
3
+ */
4
+ export type CSpellJUnitReporterSettings = {
5
+ /**
6
+ * Path to the output JUnit XML file.
7
+ *
8
+ * Relative paths are relative to the current working directory.
9
+ *
10
+ * Special values:
11
+ * - `stdout` - write the XML to `stdout`.
12
+ * - `stderr` - write the XML to `stderr`.
13
+ *
14
+ * @default stdout
15
+ */
16
+ outFile?: string;
17
+ /**
18
+ * Name to use for the root `<testsuites>` element.
19
+ *
20
+ * @default cspell
21
+ */
22
+ suiteName?: string;
23
+ };
24
+ //# sourceMappingURL=CSpellJUnitReporterSettings.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=CSpellJUnitReporterSettings.js.map
@@ -0,0 +1,9 @@
1
+ import type { CSpellReporter, ReporterConfiguration } from '@cspell/cspell-types';
2
+ import type { CSpellJUnitReporterSettings } from './CSpellJUnitReporterSettings.js';
3
+ type ReporterConsole = Pick<Console, 'log' | 'warn' | 'error'>;
4
+ export interface CSpellJUnitReporterConfiguration extends ReporterConfiguration {
5
+ console?: ReporterConsole;
6
+ }
7
+ export declare function getReporter(settings: unknown | CSpellJUnitReporterSettings, cliOptions?: CSpellJUnitReporterConfiguration): Required<CSpellReporter>;
8
+ export {};
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { buildJUnitXml } from './utils/buildJUnitXml.js';
4
+ import { validateSettings } from './utils/validateSettings.js';
5
+ function mkdirp(p) {
6
+ return fs.mkdir(p, { recursive: true });
7
+ }
8
+ const STDOUT = 'stdout';
9
+ const STDERR = 'stderr';
10
+ export function getReporter(settings, cliOptions) {
11
+ const useSettings = normalizeSettings(settings);
12
+ const console = cliOptions?.console ?? globalThis.console;
13
+ const files = new Map();
14
+ const errors = [];
15
+ const fileOrder = [];
16
+ function getOrCreateFile(filename) {
17
+ const existing = files.get(filename);
18
+ if (existing)
19
+ return existing;
20
+ const created = { filename, issues: [] };
21
+ files.set(filename, created);
22
+ fileOrder.push(filename);
23
+ return created;
24
+ }
25
+ return {
26
+ issue: (issue) => {
27
+ const filename = issue.uri || '(unknown)';
28
+ getOrCreateFile(filename).issues.push(issue);
29
+ },
30
+ info: () => undefined,
31
+ debug: () => undefined,
32
+ error: (message, error) => {
33
+ errors.push({ message, error });
34
+ },
35
+ progress: (item) => {
36
+ if (item.type !== 'ProgressFileComplete')
37
+ return;
38
+ const file = getOrCreateFile(item.filename);
39
+ file.elapsedTimeMs = item.elapsedTimeMs;
40
+ file.processed = item.processed;
41
+ file.skippedReason = item.skippedReason;
42
+ },
43
+ result: async () => {
44
+ const orderedFiles = fileOrder.map((filename) => files.get(filename)).filter((f) => !!f);
45
+ const xml = buildJUnitXml(orderedFiles, errors, { suiteName: useSettings.suiteName || 'cspell' });
46
+ const outFile = useSettings.outFile || STDOUT;
47
+ if (outFile === STDOUT) {
48
+ console.log(xml);
49
+ return;
50
+ }
51
+ if (outFile === STDERR) {
52
+ console.error(xml);
53
+ return;
54
+ }
55
+ const outFilePath = path.join(cliOptions?.root ?? process.cwd(), outFile);
56
+ await mkdirp(path.dirname(outFilePath));
57
+ return fs.writeFile(outFilePath, xml);
58
+ },
59
+ features: undefined,
60
+ };
61
+ }
62
+ function normalizeSettings(settings) {
63
+ if (settings === undefined)
64
+ return { outFile: STDOUT, suiteName: 'cspell' };
65
+ validateSettings(settings);
66
+ return settings;
67
+ }
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,47 @@
1
+ import type { ErrorLike, Issue } from '@cspell/cspell-types';
2
+ /**
3
+ * A single file that was checked by cspell.
4
+ *
5
+ * Maps to one `<testsuite>` element. Each spelling {@link Issue} found in the
6
+ * file maps to one `<testcase>` containing a `<failure>`. A file with no
7
+ * issues is reported as a single passing `<testcase>` so the suite is never
8
+ * empty (this mirrors the convention used by other JUnit reporters, e.g. the
9
+ * ESLint JUnit formatter).
10
+ */
11
+ export interface FileReport {
12
+ /** File name/uri as reported by cspell. */
13
+ filename: string;
14
+ /** Spelling issues found in the file. */
15
+ issues: Issue[];
16
+ /** Time, in milliseconds, spent processing the file (if known). */
17
+ elapsedTimeMs?: number | undefined;
18
+ /** `false` if the file was skipped (not processed). */
19
+ processed?: boolean | undefined;
20
+ /** Reason the file was skipped, if applicable. */
21
+ skippedReason?: string | undefined;
22
+ }
23
+ /**
24
+ * A processing error reported by cspell that is not tied to a specific
25
+ * spelling issue (e.g. a bad config file or an unreadable file).
26
+ */
27
+ export interface ErrorReport {
28
+ message: string;
29
+ error: ErrorLike;
30
+ }
31
+ export interface JUnitXmlOptions {
32
+ /** Name for the root `<testsuites>` element. */
33
+ suiteName: string;
34
+ }
35
+ /**
36
+ * Builds a JUnit compatible XML document from the results of a cspell run.
37
+ *
38
+ * Mapping:
39
+ * - one `<testsuites>` root for the whole run
40
+ * - one `<testsuite>` per file checked
41
+ * - one `<testcase>` per spelling issue found in a file, containing a `<failure>`
42
+ * - a single passing `<testcase>` for files with no issues
43
+ * - processing errors (not tied to a spelling issue) are reported in a
44
+ * dedicated `cspell-errors` testsuite using `<error>` instead of `<failure>`
45
+ */
46
+ export declare function buildJUnitXml(files: FileReport[], errors: ErrorReport[], options: JUnitXmlOptions): string;
47
+ //# sourceMappingURL=buildJUnitXml.d.ts.map
@@ -0,0 +1,99 @@
1
+ import { IssueType } from '@cspell/cspell-types';
2
+ import { escapeXmlAttribute, escapeXmlText } from './escapeXml.js';
3
+ function formatTime(ms) {
4
+ return ((ms ?? 0) / 1000).toFixed(3);
5
+ }
6
+ function issueName(issue) {
7
+ const location = `${issue.row}:${issue.col}`;
8
+ return `${issue.text} (${location})`;
9
+ }
10
+ function issueMessage(issue) {
11
+ if (issue.message)
12
+ return issue.message;
13
+ return `Unknown word: "${issue.text}"`;
14
+ }
15
+ function buildTestCaseForIssue(filename, issue) {
16
+ const name = escapeXmlAttribute(issueName(issue));
17
+ const classname = escapeXmlAttribute(filename);
18
+ const failureType = escapeXmlAttribute(issue.issueType === IssueType.directive ? 'directive' : 'spelling');
19
+ const message = escapeXmlAttribute(issueMessage(issue));
20
+ const body = escapeXmlText(issueMessage(issue));
21
+ return (` <testcase name="${name}" classname="${classname}">\n` +
22
+ ` <failure type="${failureType}" message="${message}">${body}</failure>\n` +
23
+ ` </testcase>\n`);
24
+ }
25
+ function buildPassingTestCase(filename) {
26
+ const classname = escapeXmlAttribute(filename);
27
+ return ` <testcase name="no issues found" classname="${classname}"/>\n`;
28
+ }
29
+ function buildSkippedTestCase(filename, reason) {
30
+ const classname = escapeXmlAttribute(filename);
31
+ const message = reason ? ` message="${escapeXmlAttribute(reason)}"` : '';
32
+ return ` <testcase name="skipped" classname="${classname}">\n <skipped${message}/>\n </testcase>\n`;
33
+ }
34
+ function fileSuiteCounts(file) {
35
+ if (file.processed === false)
36
+ return { tests: 1, failures: 0 };
37
+ const issueCount = file.issues.length;
38
+ return { tests: Math.max(issueCount, 1), failures: issueCount };
39
+ }
40
+ function buildTestSuiteForFile(file) {
41
+ const name = escapeXmlAttribute(file.filename);
42
+ const time = formatTime(file.elapsedTimeMs);
43
+ let testCases;
44
+ if (file.processed === false) {
45
+ testCases = buildSkippedTestCase(file.filename, file.skippedReason);
46
+ }
47
+ else if (file.issues.length) {
48
+ testCases = file.issues.map((issue) => buildTestCaseForIssue(file.filename, issue)).join('');
49
+ }
50
+ else {
51
+ testCases = buildPassingTestCase(file.filename);
52
+ }
53
+ const { tests, failures } = fileSuiteCounts(file);
54
+ return (` <testsuite name="${name}" tests="${tests}" failures="${failures}" errors="0" time="${time}">\n` +
55
+ testCases +
56
+ ` </testsuite>\n`);
57
+ }
58
+ function buildTestSuiteForErrors(errors) {
59
+ const testCases = errors
60
+ .map(({ message, error }) => {
61
+ const name = escapeXmlAttribute(message);
62
+ const errorMessage = escapeXmlAttribute(error.message);
63
+ const body = escapeXmlText(error.message);
64
+ return (` <testcase name="${name}" classname="cspell">\n` +
65
+ ` <error type="error" message="${errorMessage}">${body}</error>\n` +
66
+ ` </testcase>\n`);
67
+ })
68
+ .join('');
69
+ return (` <testsuite name="cspell-errors" tests="${errors.length}" failures="0" errors="${errors.length}" time="0.000">\n` +
70
+ testCases +
71
+ ` </testsuite>\n`);
72
+ }
73
+ /**
74
+ * Builds a JUnit compatible XML document from the results of a cspell run.
75
+ *
76
+ * Mapping:
77
+ * - one `<testsuites>` root for the whole run
78
+ * - one `<testsuite>` per file checked
79
+ * - one `<testcase>` per spelling issue found in a file, containing a `<failure>`
80
+ * - a single passing `<testcase>` for files with no issues
81
+ * - processing errors (not tied to a spelling issue) are reported in a
82
+ * dedicated `cspell-errors` testsuite using `<error>` instead of `<failure>`
83
+ */
84
+ export function buildJUnitXml(files, errors, options) {
85
+ const suiteName = escapeXmlAttribute(options.suiteName);
86
+ const fileSuites = files.map(buildTestSuiteForFile);
87
+ const errorSuite = errors.length ? [buildTestSuiteForErrors(errors)] : [];
88
+ const suites = [...fileSuites, ...errorSuite];
89
+ const fileCounts = files.map(fileSuiteCounts);
90
+ const totalTests = fileCounts.reduce((sum, counts) => sum + counts.tests, errors.length);
91
+ const totalFailures = fileCounts.reduce((sum, counts) => sum + counts.failures, 0);
92
+ const totalErrors = errors.length;
93
+ const totalTime = files.reduce((sum, file) => sum + (file.elapsedTimeMs ?? 0), 0) / 1000;
94
+ return ('<?xml version="1.0" encoding="UTF-8"?>\n' +
95
+ `<testsuites name="${suiteName}" tests="${totalTests}" failures="${totalFailures}" errors="${totalErrors}" time="${totalTime.toFixed(3)}">\n` +
96
+ suites.join('') +
97
+ '</testsuites>\n');
98
+ }
99
+ //# sourceMappingURL=buildJUnitXml.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Escapes text so it is safe to embed inside an XML element's text content.
3
+ */
4
+ export declare function escapeXmlText(value: string): string;
5
+ /**
6
+ * Escapes text so it is safe to embed inside a double-quoted XML attribute value.
7
+ */
8
+ export declare function escapeXmlAttribute(value: string): string;
9
+ //# sourceMappingURL=escapeXml.d.ts.map
@@ -0,0 +1,27 @@
1
+ // Control characters that should not appear literally in XML 1.0 output
2
+ // (tab \t, newline \n, and carriage return \r are allowed).
3
+ // eslint-disable-next-line no-control-regex
4
+ const invalidXmlChars = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F]/g;
5
+ /**
6
+ * Escapes text so it is safe to embed inside an XML element's text content.
7
+ */
8
+ export function escapeXmlText(value) {
9
+ return value
10
+ .replaceAll(invalidXmlChars, '')
11
+ .replaceAll('&', '&amp;')
12
+ .replaceAll('<', '&lt;')
13
+ .replaceAll('>', '&gt;');
14
+ }
15
+ /**
16
+ * Escapes text so it is safe to embed inside a double-quoted XML attribute value.
17
+ */
18
+ export function escapeXmlAttribute(value) {
19
+ return value
20
+ .replaceAll(invalidXmlChars, '')
21
+ .replaceAll('&', '&amp;')
22
+ .replaceAll('<', '&lt;')
23
+ .replaceAll('>', '&gt;')
24
+ .replaceAll('"', '&quot;')
25
+ .replaceAll("'", '&apos;'); // cspell:ignore apos
26
+ }
27
+ //# sourceMappingURL=escapeXml.js.map
@@ -0,0 +1,6 @@
1
+ import type { CSpellJUnitReporterSettings } from '../CSpellJUnitReporterSettings.js';
2
+ /**
3
+ * Throws an error if passed cspell-junit-reporter settings are invalid
4
+ */
5
+ export declare function validateSettings(settings: unknown): asserts settings is CSpellJUnitReporterSettings;
6
+ //# sourceMappingURL=validateSettings.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { AssertionError } from 'node:assert';
2
+ /**
3
+ * Throws an error if passed cspell-junit-reporter settings are invalid
4
+ */
5
+ export function validateSettings(settings) {
6
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
7
+ throw new AssertionError({
8
+ message: 'cspell-junit-reporter settings must be an object',
9
+ actual: typeof settings,
10
+ expected: 'object',
11
+ });
12
+ }
13
+ const { outFile = 'stdout', suiteName = 'cspell' } = settings;
14
+ if (typeof outFile !== 'string') {
15
+ throw new AssertionError({
16
+ message: 'cspell-junit-reporter settings.outFile must be a string',
17
+ actual: typeof outFile,
18
+ expected: 'string',
19
+ });
20
+ }
21
+ if (typeof suiteName !== 'string') {
22
+ throw new AssertionError({
23
+ message: 'cspell-junit-reporter settings.suiteName must be a string',
24
+ actual: typeof suiteName,
25
+ expected: 'string',
26
+ });
27
+ }
28
+ }
29
+ //# sourceMappingURL=validateSettings.js.map
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@cspell/cspell-junit-reporter",
3
+ "version": "10.1.0",
4
+ "description": "JUnit XML reporter for CSpell",
5
+ "keywords": [
6
+ "cspell",
7
+ "spelling",
8
+ "reporter",
9
+ "junit",
10
+ "junit-xml",
11
+ "ci"
12
+ ],
13
+ "author": "Jason Dent",
14
+ "license": "MIT",
15
+ "bugs": {
16
+ "url": "https://github.com/streetsidesoftware/cspell/labels/cspell-junit-reporter"
17
+ },
18
+ "homepage": "https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell-junit-reporter#readme",
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "types": "dist/esm/index.d.ts",
22
+ "module": "dist/esm/index.js",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/esm/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "!**/*.tsbuildInfo",
31
+ "!**/__mocks__",
32
+ "!**/test/**",
33
+ "!**/*.test.*",
34
+ "!**/*.spec.*",
35
+ "!**/*.map"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "provenance": true
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/streetsidesoftware/cspell.git",
44
+ "directory": "packages/cspell-junit-reporter"
45
+ },
46
+ "scripts": {
47
+ "clean": "shx rm -rf dist temp coverage \"*.tsbuildInfo\"",
48
+ "build": "tsc -b . -f",
49
+ "build:esm": "tsc -p tsconfig.esm.json",
50
+ "clean-build": "pnpm run clean && pnpm run build",
51
+ "coverage": "vitest run --coverage",
52
+ "test:watch": "vitest",
53
+ "test": "vitest run && pnpm test:run",
54
+ "test:run": "node ../../bin.mjs . --config cSpell.example.mjs",
55
+ "watch": "tsc -b . -w -f"
56
+ },
57
+ "dependencies": {
58
+ "@cspell/cspell-types": "10.1.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=22.18.0"
62
+ },
63
+ "gitHead": "401518c5e37c06fb406aedadb3fc7a1c27eca8e6"
64
+ }