From 81552c11c8411ef55332ef658efef04d07acace6 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 23 Jan 2026 20:19:13 -0500 Subject: [PATCH 1/4] feat(models): add URL source type for issues --- packages/models/docs/models-reference.md | 61 +++++++++++- packages/models/src/index.ts | 8 ++ packages/models/src/lib/issue.ts | 24 ++++- packages/models/src/lib/issue.unit.test.ts | 26 +++++ packages/models/src/lib/source.ts | 29 ++++++ packages/models/src/lib/source.unit.test.ts | 102 ++++++++++++++++++++ 6 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 packages/models/src/lib/source.unit.test.ts diff --git a/packages/models/docs/models-reference.md b/packages/models/docs/models-reference.md index a60be813a..189719831 100644 --- a/packages/models/docs/models-reference.md +++ b/packages/models/docs/models-reference.md @@ -291,6 +291,20 @@ _Object containing the following properties:_ _(\*) Required._ +## FileIssue + +Issue with a file source location + +_Object containing the following properties:_ + +| Property | Description | Type | +| :------------------ | :------------------------ | :---------------------------------------- | +| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) | +| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) | +| **`source`** (\*) | Source file location | [SourceFileLocation](#sourcefilelocation) | + +_(\*) Required._ + ## FileName _String which matches the regular expression `/^(?!.*[ \\/:*?"<>|]).+$/` and has a minimum length of 1._ @@ -374,11 +388,11 @@ Issue information _Object containing the following properties:_ -| Property | Description | Type | -| :------------------ | :------------------------ | :---------------------------------------- | -| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) | -| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) | -| `source` | Source file location | [SourceFileLocation](#sourcefilelocation) | +| Property | Description | Type | +| :------------------ | :--------------------------------------------- | :------------------------------ | +| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) | +| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) | +| `source` | Source location of an issue (file path or URL) | [IssueSource](#issuesource) | _(\*) Required._ @@ -392,6 +406,15 @@ _Enum, one of the following possible values:_ - `'warning'` - `'error'` +## IssueSource + +Source location of an issue (file path or URL) + +_Union of the following possible types:_ + +- [SourceFileLocation](#sourcefilelocation) +- [SourceUrlLocation](#sourceurllocation) + ## MaterialIcon Icon from VSCode Material Icons extension @@ -1500,6 +1523,20 @@ _Object containing the following properties:_ _(\*) Required._ +## SourceUrlLocation + +Location of a DOM element in a web page + +_Object containing the following properties:_ + +| Property | Description | Type | +| :------------- | :-------------------------------------------- | :--------------- | +| **`url`** (\*) | URL of the web page where the issue was found | `string` (_url_) | +| `snippet` | HTML snippet of the element | `string` | +| `selector` | CSS selector to locate the element | `string` | + +_(\*) Required._ + ## TableAlignment Cell alignment @@ -1579,6 +1616,20 @@ _Object containing the following properties:_ _(\*) Required._ +## UrlIssue + +Issue with a URL source location + +_Object containing the following properties:_ + +| Property | Description | Type | +| :------------------ | :-------------------------------------- | :-------------------------------------- | +| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) | +| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) | +| **`source`** (\*) | Location of a DOM element in a web page | [SourceUrlLocation](#sourceurllocation) | + +_(\*) Required._ + ## Weight Coefficient for the given score (use weight 0 if only for display) diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index e68f802a1..018fd1f51 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -4,7 +4,11 @@ export { } from './lib/implementation/schemas.js'; export { sourceFileLocationSchema, + sourceUrlLocationSchema, + issueSourceSchema, + type IssueSource, type SourceFileLocation, + type SourceUrlLocation, } from './lib/source.js'; export { @@ -80,10 +84,14 @@ export { validateAsync, } from './lib/implementation/validate.js'; export { + fileIssueSchema, issueSchema, issueSeveritySchema, + urlIssueSchema, + type FileIssue, type Issue, type IssueSeverity, + type UrlIssue, } from './lib/issue.js'; export { formatSchema, diff --git a/packages/models/src/lib/issue.ts b/packages/models/src/lib/issue.ts index be388daab..7888deaa8 100644 --- a/packages/models/src/lib/issue.ts +++ b/packages/models/src/lib/issue.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; import { MAX_ISSUE_MESSAGE_LENGTH } from './implementation/limits.js'; -import { sourceFileLocationSchema } from './source.js'; +import { + issueSourceSchema, + sourceFileLocationSchema, + sourceUrlLocationSchema, +} from './source.js'; export const issueSeveritySchema = z.enum(['info', 'warning', 'error']).meta({ title: 'IssueSeverity', @@ -15,10 +19,26 @@ export const issueSchema = z .max(MAX_ISSUE_MESSAGE_LENGTH) .meta({ description: 'Descriptive error message' }), severity: issueSeveritySchema, - source: sourceFileLocationSchema.optional(), + source: issueSourceSchema.optional(), }) .meta({ title: 'Issue', description: 'Issue information', }); export type Issue = z.infer; + +export const fileIssueSchema = issueSchema + .extend({ source: sourceFileLocationSchema }) + .meta({ + title: 'FileIssue', + description: 'Issue with a file source location', + }); +export type FileIssue = z.infer; + +export const urlIssueSchema = issueSchema + .extend({ source: sourceUrlLocationSchema }) + .meta({ + title: 'UrlIssue', + description: 'Issue with a URL source location', + }); +export type UrlIssue = z.infer; diff --git a/packages/models/src/lib/issue.unit.test.ts b/packages/models/src/lib/issue.unit.test.ts index 4a71f9250..a074d3594 100644 --- a/packages/models/src/lib/issue.unit.test.ts +++ b/packages/models/src/lib/issue.unit.test.ts @@ -24,6 +24,32 @@ describe('issueSchema', () => { ).not.toThrow(); }); + it('should accept a valid issue with source URL information', () => { + expect(() => + issueSchema.parse({ + message: 'Image is missing alt attribute', + severity: 'error', + source: { + url: 'https://example.com/page', + snippet: '', + selector: 'img.logo', + }, + }), + ).not.toThrow(); + }); + + it('should accept issue with URL source without optional fields', () => { + expect(() => + issueSchema.parse({ + message: 'Accessibility issue found', + severity: 'warning', + source: { + url: 'https://example.com', + }, + }), + ).not.toThrow(); + }); + it('should throw for a missing message', () => { expect(() => issueSchema.parse({ diff --git a/packages/models/src/lib/source.ts b/packages/models/src/lib/source.ts index 4500adff9..e0f361bfc 100644 --- a/packages/models/src/lib/source.ts +++ b/packages/models/src/lib/source.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { filePathSchema, filePositionSchema, + urlSchema, } from './implementation/schemas.js'; export const sourceFileLocationSchema = z @@ -17,3 +18,31 @@ export const sourceFileLocationSchema = z }); export type SourceFileLocation = z.infer; + +export const sourceUrlLocationSchema = z + .object({ + url: urlSchema.meta({ + description: 'URL of the web page where the issue was found', + }), + snippet: z.string().optional().meta({ + description: 'HTML snippet of the element', + }), + selector: z.string().optional().meta({ + description: 'CSS selector to locate the element', + }), + }) + .meta({ + title: 'SourceUrlLocation', + description: 'Location of a DOM element in a web page', + }); + +export type SourceUrlLocation = z.infer; + +export const issueSourceSchema = z + .union([sourceFileLocationSchema, sourceUrlLocationSchema]) + .meta({ + title: 'IssueSource', + description: 'Source location of an issue (file path or URL)', + }); + +export type IssueSource = z.infer; diff --git a/packages/models/src/lib/source.unit.test.ts b/packages/models/src/lib/source.unit.test.ts new file mode 100644 index 000000000..07f57d8ff --- /dev/null +++ b/packages/models/src/lib/source.unit.test.ts @@ -0,0 +1,102 @@ +import { + issueSourceSchema, + sourceFileLocationSchema, + sourceUrlLocationSchema, +} from './source.js'; + +describe('sourceFileLocationSchema', () => { + it('should accept valid file location with position', () => { + expect(() => + sourceFileLocationSchema.parse({ + file: 'src/index.ts', + position: { startLine: 10, endLine: 15 }, + }), + ).not.toThrow(); + }); + + it('should accept file location without position', () => { + expect(() => + sourceFileLocationSchema.parse({ file: 'src/utils.ts' }), + ).not.toThrow(); + }); + + it('should reject empty file path', () => { + expect(() => sourceFileLocationSchema.parse({ file: '' })).toThrow( + 'Too small', + ); + }); +}); + +describe('sourceUrlLocationSchema', () => { + it('should accept valid URL location with all fields', () => { + expect(() => + sourceUrlLocationSchema.parse({ + url: 'https://example.com/page', + snippet: '', + selector: 'img.logo', + }), + ).not.toThrow(); + }); + + it('should accept URL location with only required url field', () => { + expect(() => + sourceUrlLocationSchema.parse({ url: 'https://example.com' }), + ).not.toThrow(); + }); + + it('should accept URL location with snippet only', () => { + expect(() => + sourceUrlLocationSchema.parse({ + url: 'https://example.com/dashboard', + snippet: '', + }), + ).not.toThrow(); + }); + + it('should accept URL location with selector only', () => { + expect(() => + sourceUrlLocationSchema.parse({ + url: 'https://example.com/form', + selector: '#submit-btn', + }), + ).not.toThrow(); + }); + + it('should reject invalid URL', () => { + expect(() => + sourceUrlLocationSchema.parse({ url: 'not-a-valid-url' }), + ).toThrow('Invalid URL'); + }); + + it('should reject missing URL', () => { + expect(() => + sourceUrlLocationSchema.parse({ snippet: '
No URL provided
' }), + ).toThrow('Invalid input'); + }); +}); + +describe('issueSourceSchema', () => { + it('should accept file-based source', () => { + expect(() => + issueSourceSchema.parse({ + file: 'src/app.ts', + position: { startLine: 1 }, + }), + ).not.toThrow(); + }); + + it('should accept URL-based source', () => { + expect(() => + issueSourceSchema.parse({ + url: 'https://example.com', + selector: '#main', + }), + ).not.toThrow(); + }); + + it('should reject source with neither file nor url', () => { + expect(() => + issueSourceSchema.parse({ position: { startLine: 1 } }), + ).toThrow('Invalid input'); + }); +}); From ab26349fb6bee5efdece4bba5e92d1c4fde4e7d5 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 23 Jan 2026 20:21:44 -0500 Subject: [PATCH 2/4] feat(utils): add type guards and URL source formatting --- .../docs/file-size-unmodified.audit.md | 8 +- .../package-json/docs/dependencies.audit.md | 12 +-- .../src/package-json/docs/license.audit.md | 12 +-- .../src/package-json/docs/type.audit.md | 12 +-- .../mocks/fixtures/outputs/report-before.md | 6 +- packages/cli/docs/custom-plugins.md | 4 +- .../docs/audits-and-groups.md | 46 +++++------ packages/utils/src/index.ts | 5 ++ .../generate-md-report.unit.test.ts.snap | 28 +++---- .../src/lib/reports/__snapshots__/report.md | 80 +++++++++---------- packages/utils/src/lib/reports/formatting.ts | 22 ++++- .../src/lib/reports/formatting.unit.test.ts | 15 ++++ .../src/lib/reports/generate-md-report.ts | 26 ++++-- .../reports/generate-md-report.unit.test.ts | 46 ++++++++++- packages/utils/src/lib/reports/type-guards.ts | 30 +++++++ packages/utils/src/lib/reports/utils.ts | 24 ++++-- .../src/lib/utils/os-agnostic.ts | 36 ++++++--- .../markdown-table.matcher.unit.test.ts | 2 +- 18 files changed, 280 insertions(+), 134 deletions(-) create mode 100644 packages/utils/src/lib/reports/type-guards.ts diff --git a/examples/plugins/src/file-size/docs/file-size-unmodified.audit.md b/examples/plugins/src/file-size/docs/file-size-unmodified.audit.md index 7ecf94ce8..b681d4131 100644 --- a/examples/plugins/src/file-size/docs/file-size-unmodified.audit.md +++ b/examples/plugins/src/file-size/docs/file-size-unmodified.audit.md @@ -24,8 +24,8 @@ A `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -45,8 +45,8 @@ The file sizes of the given file, the budget as well as the size difference is m Severity Message - Source file - Line(s) + Source + Location 🚨 error diff --git a/examples/plugins/src/package-json/docs/dependencies.audit.md b/examples/plugins/src/package-json/docs/dependencies.audit.md index 14bedab87..5f6757c87 100644 --- a/examples/plugins/src/package-json/docs/dependencies.audit.md +++ b/examples/plugins/src/package-json/docs/dependencies.audit.md @@ -24,8 +24,8 @@ An `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -44,8 +44,8 @@ A `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -65,8 +65,8 @@ The dependencies of the given file, the target version as well as the given vers Severity Message - Source file - Line(s) + Source + Location 🚨 error diff --git a/examples/plugins/src/package-json/docs/license.audit.md b/examples/plugins/src/package-json/docs/license.audit.md index e64d09378..b1e271baa 100644 --- a/examples/plugins/src/package-json/docs/license.audit.md +++ b/examples/plugins/src/package-json/docs/license.audit.md @@ -24,8 +24,8 @@ A `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -44,8 +44,8 @@ A `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -65,8 +65,8 @@ The `license` of the given file, the target `license` as well as the given `lice Severity Message - Source file - Line(s) + Source + Location 🚨 error diff --git a/examples/plugins/src/package-json/docs/type.audit.md b/examples/plugins/src/package-json/docs/type.audit.md index f2d96ba5d..e7af49eef 100644 --- a/examples/plugins/src/package-json/docs/type.audit.md +++ b/examples/plugins/src/package-json/docs/type.audit.md @@ -28,8 +28,8 @@ A `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -48,8 +48,8 @@ A `Issue` with severity `info` is present and names to the given file. Severity Message - Source file - Line(s) + Source + Location ℹ️ info @@ -69,8 +69,8 @@ The `type` of the given file, the target `type` as well as the given `type` are Severity Message - Source file - Line(s) + Source + Location 🚨 error diff --git a/packages/ci/mocks/fixtures/outputs/report-before.md b/packages/ci/mocks/fixtures/outputs/report-before.md index 7ffe57585..ef2e2cace 100644 --- a/packages/ci/mocks/fixtures/outputs/report-before.md +++ b/packages/ci/mocks/fixtures/outputs/report-before.md @@ -9,9 +9,9 @@ #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :------------------------------------ | :------------------------ | :-----: | -| ⚠️ _warning_ | Use .ts file extension instead of .js | [`index.js`](../index.js) | | +| Severity | Message | Source | Location | +| :----------: | :------------------------------------ | :------------------------ | :------: | +| ⚠️ _warning_ | Use .ts file extension instead of .js | [`index.js`](../index.js) | | diff --git a/packages/cli/docs/custom-plugins.md b/packages/cli/docs/custom-plugins.md index cff150db4..b3bf42bbc 100644 --- a/packages/cli/docs/custom-plugins.md +++ b/packages/cli/docs/custom-plugins.md @@ -559,8 +559,8 @@ The `report.md` file should contain a similar content like the following: Severity Message - Source file - Line(s) + Source + Location 🚨 error diff --git a/packages/plugin-typescript/docs/audits-and-groups.md b/packages/plugin-typescript/docs/audits-and-groups.md index 5cb528c22..928eff936 100644 --- a/packages/plugin-typescript/docs/audits-and-groups.md +++ b/packages/plugin-typescript/docs/audits-and-groups.md @@ -33,11 +33,11 @@ Errors that occur during type checking and type inference. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :-----: | -| 🚨 _error_ | TS2307: Cannot find module './non-existent' or its corresponding type declarations. | [`path/to/module-resolution.ts`](../path/to/module-resolution.ts) | 2 | -| 🚨 _error_ | TS2349: This expression is not callable.
Type 'Number' has no call signatures. | [`path/to/strict-function-types.ts`](../path/to/strict-function-types.ts) | 3 | -| 🚨 _error_ | TS2304: Cannot find name 'NonExistentType'. | [`path/to/cannot-find-module.ts`](../path/to/cannot-find-module.ts) | 1 | +| Severity | Message | Source | Location | +| :--------: | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :------: | +| 🚨 _error_ | TS2307: Cannot find module './non-existent' or its corresponding type declarations. | [`path/to/module-resolution.ts`](../path/to/module-resolution.ts) | 2 | +| 🚨 _error_ | TS2349: This expression is not callable.
Type 'Number' has no call signatures. | [`path/to/strict-function-types.ts`](../path/to/strict-function-types.ts) | 3 | +| 🚨 _error_ | TS2304: Cannot find name 'NonExistentType'. | [`path/to/cannot-find-module.ts`](../path/to/cannot-find-module.ts) | 1 | --- @@ -54,9 +54,9 @@ Errors that occur during parsing and lexing of TypeScript source code. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :------------------------------------ | :------------------------------------------------------ | :-----: | -| 🚨 _error_ | TS1136: Property assignment expected. | [`path/to/syntax-error.ts`](../path/to/syntax-error.ts) | 1 | +| Severity | Message | Source | Location | +| :--------: | :------------------------------------ | :------------------------------------------------------ | :------: | +| 🚨 _error_ | TS1136: Property assignment expected. | [`path/to/syntax-error.ts`](../path/to/syntax-error.ts) | 1 | --- @@ -73,9 +73,9 @@ Errors that occur when parsing TypeScript configuration files. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :----------------------------------------- | :-------------------------------------------------- | :-----: | -| 🚨 _error_ | TS5023: Unknown compiler option 'invalid'. | [`path/to/tsconfig.json`](../path/to/tsconfig.json) | 1 | +| Severity | Message | Source | Location | +| :--------: | :----------------------------------------- | :-------------------------------------------------- | :------: | +| 🚨 _error_ | TS5023: Unknown compiler option 'invalid'. | [`path/to/tsconfig.json`](../path/to/tsconfig.json) | 1 | --- @@ -92,9 +92,9 @@ Errors that occur during TypeScript language service operations. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------ | :-----: | -| 🚨 _error_ | TS4112: This member cannot have an 'override' modifier because its containing class 'Standalone' does not extend another class. | [`path/to/incorrect-modifier.ts`](../path/to/incorrect-modifier.ts) | 2 | +| Severity | Message | Source | Location | +| :--------: | :------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------ | :------: | +| 🚨 _error_ | TS4112: This member cannot have an 'override' modifier because its containing class 'Standalone' does not extend another class. | [`path/to/incorrect-modifier.ts`](../path/to/incorrect-modifier.ts) | 2 | --- @@ -111,9 +111,9 @@ Errors that occur during TypeScript internal operations. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :------------------------------- | :---------------------------------------------------------- | :-----: | -| 🚨 _error_ | TS9001: Internal compiler error. | [`path/to/internal-error.ts`](../path/to/internal-error.ts) | 4 | +| Severity | Message | Source | Location | +| :--------: | :------------------------------- | :---------------------------------------------------------- | :------: | +| 🚨 _error_ | TS9001: Internal compiler error. | [`path/to/internal-error.ts`](../path/to/internal-error.ts) | 4 | --- @@ -130,9 +130,9 @@ Errors related to no implicit any compiler option. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :-------------------------------------------------- | :------------------------------------------------------ | :-----: | -| 🚨 _error_ | TS7006: Parameter 'x' implicitly has an 'any' type. | [`path/to/implicit-any.ts`](../path/to/implicit-any.ts) | 5 | +| Severity | Message | Source | Location | +| :--------: | :-------------------------------------------------- | :------------------------------------------------------ | :------: | +| 🚨 _error_ | TS7006: Parameter 'x' implicitly has an 'any' type. | [`path/to/implicit-any.ts`](../path/to/implicit-any.ts) | 5 | --- @@ -149,9 +149,9 @@ Errors that do not match any known TypeScript error code. #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :-------------------------------------- | :-------------------------------------------------------- | :-----: | -| 🚨 _error_ | TS9999: Unknown error code encountered. | [`path/to/unknown-error.ts`](../path/to/unknown-error.ts) | 6 | +| Severity | Message | Source | Location | +| :--------: | :-------------------------------------- | :-------------------------------------------------------- | :------: | +| 🚨 _error_ | TS9999: Unknown error code encountered. | [`path/to/unknown-error.ts`](../path/to/unknown-error.ts) | 6 | --- diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index f019b8055..495bd46ab 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -125,6 +125,11 @@ export { listGroupsFromAllPlugins, } from './lib/reports/flatten-plugins.js'; export { formatIssueSeverities, wrapTags } from './lib/reports/formatting.js'; +export { + isFileIssue, + isFileSource, + isUrlSource, +} from './lib/reports/type-guards.js'; export { generateMdReport } from './lib/reports/generate-md-report.js'; export { generateMdReportsDiff, diff --git a/packages/utils/src/lib/reports/__snapshots__/generate-md-report.unit.test.ts.snap b/packages/utils/src/lib/reports/__snapshots__/generate-md-report.unit.test.ts.snap index a8304ca33..efc87173f 100644 --- a/packages/utils/src/lib/reports/__snapshots__/generate-md-report.unit.test.ts.snap +++ b/packages/utils/src/lib/reports/__snapshots__/generate-md-report.unit.test.ts.snap @@ -26,10 +26,10 @@ exports[`auditDetails > should render complete details section 1`] = ` #### Issues -| Severity | Message | Source file | Line(s) | -| :--------: | :---------------------------------------------- | :------------------ | :-----: | -| 🚨 _error_ | Use design system components instead of classes | \`list.component.ts\` | 400-200 | -| 🚨 _error_ | File size is 20KB too big | \`list.component.ts\` | | +| Severity | Message | Source | Location | +| :--------: | :---------------------------------------------- | :------------------ | :------: | +| 🚨 _error_ | Use design system components instead of classes | \`list.component.ts\` | 400-200 | +| 🚨 _error_ | File size is 20KB too big | \`list.component.ts\` | | " @@ -38,11 +38,11 @@ exports[`auditDetails > should render complete details section 1`] = ` exports[`auditDetailsIssues > should render complete section 1`] = ` "#### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :--------------------------------- | :------------- | :-----: | -| ℹ️ _info_ | File \`index.js\` is 56Kb. | \`index.js\` | | -| ⚠️ _warning_ | Package license is has to be "MIT" | \`package.json\` | 4 | -| 🚨 _error_ | no unused vars | \`index.js\` | 400-200 | +| Severity | Message | Source | Location | +| :----------: | :--------------------------------- | :------------- | :------: | +| ℹ️ _info_ | File \`index.js\` is 56Kb. | \`index.js\` | | +| ⚠️ _warning_ | Package license is has to be "MIT" | \`package.json\` | 4 | +| 🚨 _error_ | no unused vars | \`index.js\` | 400-200 | " `; @@ -130,11 +130,11 @@ Search engines are unable to include your pages in search results if they don't #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :--------------------------------- | :------------- | :-----: | -| ℹ️ _info_ | File \`index.js\` is 56Kb. | \`index.js\` | | -| ⚠️ _warning_ | Package license is has to be "MIT" | \`package.json\` | 4 | -| 🚨 _error_ | no unused vars | \`index.js\` | 400-200 | +| Severity | Message | Source | Location | +| :----------: | :--------------------------------- | :------------- | :------: | +| ℹ️ _info_ | File \`index.js\` is 56Kb. | \`index.js\` | | +| ⚠️ _warning_ | Package license is has to be "MIT" | \`package.json\` | 4 | +| 🚨 _error_ | no unused vars | \`index.js\` | 400-200 | diff --git a/packages/utils/src/lib/reports/__snapshots__/report.md b/packages/utils/src/lib/reports/__snapshots__/report.md index b8722d513..61d5597e3 100644 --- a/packages/utils/src/lib/reports/__snapshots__/report.md +++ b/packages/utils/src/lib/reports/__snapshots__/report.md @@ -73,14 +73,14 @@ Performance metrics [📖 Docs](https://developers.google.com/web/fundamentals/p #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------------------- | :------------------------------ | :-----: | -| ⚠️ _warning_ | 'onCreate' is missing in props validation | `src/components/CreateTodo.jsx` | 15 | -| ⚠️ _warning_ | 'setQuery' is missing in props validation | `src/components/TodoFilter.jsx` | 10 | -| ⚠️ _warning_ | 'setHideComplete' is missing in props validation | `src/components/TodoFilter.jsx` | 18 | -| ⚠️ _warning_ | 'todos' is missing in props validation | `src/components/TodoList.jsx` | 6 | -| ⚠️ _warning_ | 'todos.map' is missing in props validation | `src/components/TodoList.jsx` | 6 | -| ⚠️ _warning_ | 'onEdit' is missing in props validation | `src/components/TodoList.jsx` | 13 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------------------- | :------------------------------ | :------: | +| ⚠️ _warning_ | 'onCreate' is missing in props validation | `src/components/CreateTodo.jsx` | 15 | +| ⚠️ _warning_ | 'setQuery' is missing in props validation | `src/components/TodoFilter.jsx` | 10 | +| ⚠️ _warning_ | 'setHideComplete' is missing in props validation | `src/components/TodoFilter.jsx` | 18 | +| ⚠️ _warning_ | 'todos' is missing in props validation | `src/components/TodoList.jsx` | 6 | +| ⚠️ _warning_ | 'todos.map' is missing in props validation | `src/components/TodoList.jsx` | 6 | +| ⚠️ _warning_ | 'onEdit' is missing in props validation | `src/components/TodoList.jsx` | 13 | @@ -93,11 +93,11 @@ ESLint rule **prop-types**, from _react_ plugin. [📖 Docs](https://github.com/ #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------------------------------------- | :---------------------- | :-----: | -| ⚠️ _warning_ | 'data' is already declared in the upper scope on line 5 column 10. | `src/hooks/useTodos.js` | 11 | -| ⚠️ _warning_ | 'data' is already declared in the upper scope on line 5 column 10. | `src/hooks/useTodos.js` | 29 | -| ⚠️ _warning_ | 'data' is already declared in the upper scope on line 5 column 10. | `src/hooks/useTodos.js` | 41 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------------------------------------- | :---------------------- | :------: | +| ⚠️ _warning_ | 'data' is already declared in the upper scope on line 5 column 10. | `src/hooks/useTodos.js` | 11 | +| ⚠️ _warning_ | 'data' is already declared in the upper scope on line 5 column 10. | `src/hooks/useTodos.js` | 29 | +| ⚠️ _warning_ | 'data' is already declared in the upper scope on line 5 column 10. | `src/hooks/useTodos.js` | 41 | @@ -110,11 +110,11 @@ ESLint rule **no-shadow**. [📖 Docs](https://eslint.org/docs/latest/rules/no-s #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :--------------------------- | :---------------------- | :-----: | -| ⚠️ _warning_ | Expected property shorthand. | `src/hooks/useTodos.js` | 19 | -| ⚠️ _warning_ | Expected property shorthand. | `src/hooks/useTodos.js` | 32 | -| ⚠️ _warning_ | Expected property shorthand. | `src/hooks/useTodos.js` | 33 | +| Severity | Message | Source | Location | +| :----------: | :--------------------------- | :---------------------- | :------: | +| ⚠️ _warning_ | Expected property shorthand. | `src/hooks/useTodos.js` | 19 | +| ⚠️ _warning_ | Expected property shorthand. | `src/hooks/useTodos.js` | 32 | +| ⚠️ _warning_ | Expected property shorthand. | `src/hooks/useTodos.js` | 33 | @@ -127,10 +127,10 @@ ESLint rule **object-shorthand**. [📖 Docs](https://eslint.org/docs/latest/rul #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------------------------------------------------------------------------------------------- | :---------------------- | :-----: | -| ⚠️ _warning_ | React Hook useCallback does nothing when called with only one argument. Did you forget to pass an array of dependencies? | `src/hooks/useTodos.js` | 17 | -| ⚠️ _warning_ | React Hook useCallback does nothing when called with only one argument. Did you forget to pass an array of dependencies? | `src/hooks/useTodos.js` | 40 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------------------------------------------------------------------------------------------- | :---------------------- | :------: | +| ⚠️ _warning_ | React Hook useCallback does nothing when called with only one argument. Did you forget to pass an array of dependencies? | `src/hooks/useTodos.js` | 17 | +| ⚠️ _warning_ | React Hook useCallback does nothing when called with only one argument. Did you forget to pass an array of dependencies? | `src/hooks/useTodos.js` | 40 | @@ -143,9 +143,9 @@ ESLint rule **exhaustive-deps**, from _react-hooks_ plugin. [📖 Docs](https:// #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------------- | :---------------------------- | :-----: | -| ⚠️ _warning_ | Missing "key" prop for element in iterator | `src/components/TodoList.jsx` | 7-28 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------------- | :---------------------------- | :------: | +| ⚠️ _warning_ | Missing "key" prop for element in iterator | `src/components/TodoList.jsx` | 7-28 | @@ -158,9 +158,9 @@ ESLint rule **jsx-key**, from _react_ plugin. [📖 Docs](https://github.com/jsx #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :-------------------------------------------- | :------------ | :-----: | -| ⚠️ _warning_ | 'loading' is assigned a value but never used. | `src/App.jsx` | 8 | +| Severity | Message | Source | Location | +| :----------: | :-------------------------------------------- | :------------ | :------: | +| ⚠️ _warning_ | 'loading' is assigned a value but never used. | `src/App.jsx` | 8 | @@ -173,9 +173,9 @@ ESLint rule **no-unused-vars**. [📖 Docs](https://eslint.org/docs/latest/rules #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :------------------------------------------------------------- | :---------------------- | :-----: | -| ⚠️ _warning_ | Arrow function has too many lines (71). Maximum allowed is 50. | `src/hooks/useTodos.js` | 3-73 | +| Severity | Message | Source | Location | +| :----------: | :------------------------------------------------------------- | :---------------------- | :------: | +| ⚠️ _warning_ | Arrow function has too many lines (71). Maximum allowed is 50. | `src/hooks/useTodos.js` | 3-73 | @@ -188,9 +188,9 @@ ESLint rule **max-lines-per-function**. [📖 Docs](https://eslint.org/docs/late #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------------------- | :-------------- | :-----: | -| ⚠️ _warning_ | 'root' is never reassigned. Use 'const' instead. | `src/index.jsx` | 5 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------------------- | :-------------- | :------: | +| ⚠️ _warning_ | 'root' is never reassigned. Use 'const' instead. | `src/index.jsx` | 5 | @@ -203,9 +203,9 @@ ESLint rule **prefer-const**. [📖 Docs](https://eslint.org/docs/latest/rules/p #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------------------------------------------------------------------------- | :------------------------------ | :-----: | -| ⚠️ _warning_ | Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`. | `src/components/TodoFilter.jsx` | 3-25 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------------------------------------------------------------------------- | :------------------------------ | :------: | +| ⚠️ _warning_ | Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`. | `src/components/TodoFilter.jsx` | 3-25 | @@ -218,9 +218,9 @@ ESLint rule **arrow-body-style**. [📖 Docs](https://eslint.org/docs/latest/rul #### Issues -| Severity | Message | Source file | Line(s) | -| :----------: | :----------------------------------- | :---------------------- | :-----: | -| ⚠️ _warning_ | Expected '===' and instead saw '=='. | `src/hooks/useTodos.js` | 41 | +| Severity | Message | Source | Location | +| :----------: | :----------------------------------- | :---------------------- | :------: | +| ⚠️ _warning_ | Expected '===' and instead saw '=='. | `src/hooks/useTodos.js` | 41 | diff --git a/packages/utils/src/lib/reports/formatting.ts b/packages/utils/src/lib/reports/formatting.ts index 4ecb7d0d0..0fbfac8ae 100644 --- a/packages/utils/src/lib/reports/formatting.ts +++ b/packages/utils/src/lib/reports/formatting.ts @@ -9,11 +9,12 @@ import type { AuditReport, Issue, IssueSeverity, + IssueSource, SourceFileLocation, Table, Tree, } from '@code-pushup/models'; -import { pluralizeToken } from '../formatting.js'; +import { UNICODE_ELLIPSIS, pluralizeToken } from '../formatting.js'; import { formatAsciiTree } from '../text-formats/ascii/tree.js'; import { columnsToStringArray, @@ -27,6 +28,7 @@ import { getGitHubBaseUrl, getGitLabBaseUrl, } from './environment-type.js'; +import { isUrlSource } from './type-guards.js'; import type { MdReportOptions } from './types.js'; import { compareIssueSeverity } from './utils.js'; @@ -113,6 +115,19 @@ export function linkToLocalSourceForIde( return md.link(formatFileLink(file, position, outputDir), md.code(file)); } +/** + * Link to source (handles both file and URL sources) + */ +export function linkToSource( + source: IssueSource, + options?: Pick, +): InlineText { + if (isUrlSource(source)) { + return md.link(source.url, source.url); + } + return linkToLocalSourceForIde(source, options); +} + export function formatSourceLine( position: SourceFileLocation['position'], ): string { @@ -212,3 +227,8 @@ export function wrapTags(text: string | undefined): string { } return text.replace(/<[a-z][a-z\d]*[^>]*>/gi, '`$&`'); } + +export function formatSelectorLocation(selector: string): string { + const lastSegment = selector.split(/\s*>>?\s*/).at(-1) ?? selector; + return selector === lastSegment ? selector : UNICODE_ELLIPSIS + lastSegment; +} diff --git a/packages/utils/src/lib/reports/formatting.unit.test.ts b/packages/utils/src/lib/reports/formatting.unit.test.ts index a2f29de37..7d5339d4f 100644 --- a/packages/utils/src/lib/reports/formatting.unit.test.ts +++ b/packages/utils/src/lib/reports/formatting.unit.test.ts @@ -5,6 +5,7 @@ import { formatGitHubLink, formatGitLabLink, formatIssueSeverities, + formatSelectorLocation, formatSeverityCounts, formatSourceLine, linkToLocalSourceForIde, @@ -429,3 +430,17 @@ describe('wrapTags', () => { expect(wrapTags(input)).toBe(expected); }); }); + +describe('formatSelectorLocation', () => { + it.each([ + ['img.logo', 'img.logo'], + ['main > article > img.logo', '…img.logo'], + ['main >> iframe >> img', '…img'], + [ + 'tr:nth-child(1) > td[align="center"] > a > img[width="24"]', + '…img[width="24"]', + ], + ])('should format %j selector as %j', (input, expected) => { + expect(formatSelectorLocation(input)).toBe(expected); + }); +}); diff --git a/packages/utils/src/lib/reports/generate-md-report.ts b/packages/utils/src/lib/reports/generate-md-report.ts index 423f6018d..0d56a9185 100644 --- a/packages/utils/src/lib/reports/generate-md-report.ts +++ b/packages/utils/src/lib/reports/generate-md-report.ts @@ -9,8 +9,9 @@ import { REPORT_HEADLINE_TEXT, } from './constants.js'; import { + formatSelectorLocation, formatSourceLine, - linkToLocalSourceForIde, + linkToSource, metaDescription, tableSection, treeSection, @@ -20,6 +21,7 @@ import { categoriesDetailsSection, categoriesOverviewSection, } from './generate-md-report-category-section.js'; +import { isFileSource, isUrlSource } from './type-guards.js'; import type { MdReportOptions, ScoredReport } from './types.js'; import { formatReportScore, @@ -82,8 +84,8 @@ export function auditDetailsIssues( [ { heading: 'Severity', alignment: 'center' }, { heading: 'Message', alignment: 'left' }, - { heading: 'Source file', alignment: 'left' }, - { heading: 'Line(s)', alignment: 'center' }, + { heading: 'Source', alignment: 'left' }, + { heading: 'Location', alignment: 'center' }, ], issues.map(({ severity: level, message, source }: Issue) => { const severity = md`${severityMarker(level)} ${md.italic(level)}`; @@ -92,12 +94,20 @@ export function auditDetailsIssues( if (!source) { return [severity, formattedMessage]; } - const file = linkToLocalSourceForIde(source, options); - if (!source.position) { - return [severity, formattedMessage, file]; + + const sourceLink = linkToSource(source, options); + + if (isFileSource(source) && source.position) { + const location = formatSourceLine(source.position); + return [severity, formattedMessage, sourceLink, location]; } - const line = formatSourceLine(source.position); - return [severity, formattedMessage, file, line]; + + if (isUrlSource(source) && source.selector) { + const location = formatSelectorLocation(source.selector); + return [severity, formattedMessage, sourceLink, md.code(location)]; + } + + return [severity, formattedMessage, sourceLink]; }), ); } diff --git a/packages/utils/src/lib/reports/generate-md-report.unit.test.ts b/packages/utils/src/lib/reports/generate-md-report.unit.test.ts index 28caf1c36..f53fb1f03 100644 --- a/packages/utils/src/lib/reports/generate-md-report.unit.test.ts +++ b/packages/utils/src/lib/reports/generate-md-report.unit.test.ts @@ -226,6 +226,48 @@ describe('auditDetailsIssues', () => { ])?.toString(), ).toContainMarkdownTableRow(['⚠️ _warning_', '', '`index.js`', '4-7']); }); + + it('should include URL source with selector as location', () => { + expect( + auditDetailsIssues([ + { + message: 'Element does not have an alt attribute', + severity: 'error', + source: { + url: 'https://example.com/page', + selector: 'img.logo', + snippet: '', + }, + }, + ])!.toString(), + ).toContainMarkdownTableRow([ + '🚨 _error_', + 'Element does not have an alt attribute', + '[https://example.com/page](https://example.com/page)', + '`img.logo`', + ]); + }); + + it('should show only target element for multi-segment selectors', () => { + expect( + auditDetailsIssues([ + { + message: 'Element has insufficient color contrast', + severity: 'warning', + source: { + url: 'https://example.com/page', + selector: + 'main > article > section > div.container > ul > li:nth-child(2) > a > span', + }, + }, + ])!.toString(), + ).toContainMarkdownTableRow([ + '⚠️ _warning_', + 'Element has insufficient color contrast', + '[https://example.com/page](https://example.com/page)', + '`…span`', + ]); + }); }); describe('tableSection', () => { @@ -492,8 +534,8 @@ describe('auditsSection', () => { expect(md).toContainMarkdownTableRow([ 'Severity', 'Message', - 'Source file', - 'Line(s)', + 'Source', + 'Location', ]); expect(md).toContainMarkdownTableRow(['value']); }); diff --git a/packages/utils/src/lib/reports/type-guards.ts b/packages/utils/src/lib/reports/type-guards.ts new file mode 100644 index 000000000..ce4ce5cc0 --- /dev/null +++ b/packages/utils/src/lib/reports/type-guards.ts @@ -0,0 +1,30 @@ +import type { + FileIssue, + Issue, + IssueSource, + SourceFileLocation, + SourceUrlLocation, + UrlIssue, +} from '@code-pushup/models'; + +/** Type guard for file-based source */ +export function isFileSource( + source: IssueSource, +): source is SourceFileLocation { + return 'file' in source; +} + +/** Type guard for URL-based source */ +export function isUrlSource(source: IssueSource): source is SourceUrlLocation { + return 'url' in source; +} + +/** Type guard for issue with file source */ +export function isFileIssue(issue: Issue): issue is FileIssue { + return issue.source != null && isFileSource(issue.source); +} + +/** Type guard for issue with URL source */ +export function isUrlIssue(issue: Issue): issue is UrlIssue { + return issue.source != null && isUrlSource(issue.source); +} diff --git a/packages/utils/src/lib/reports/utils.ts b/packages/utils/src/lib/reports/utils.ts index 65e6a19b3..12c9ad39c 100644 --- a/packages/utils/src/lib/reports/utils.ts +++ b/packages/utils/src/lib/reports/utils.ts @@ -7,8 +7,11 @@ import type { IssueSeverity as CliIssueSeverity, Group, Issue, + IssueSource, + SourceFileLocation, } from '@code-pushup/models'; import { SCORE_COLOR_RANGE } from './constants.js'; +import { isFileSource } from './type-guards.js'; import type { ScoreFilter, ScoredReport, @@ -241,6 +244,10 @@ export function getPluginNameFromSlug( ); } +function getSourceIdentifier(source: IssueSource): string { + return isFileSource(source) ? source.file : source.url; +} + export function compareIssues(a: Issue, b: Issue): number { if (a.severity !== b.severity) { return -compareIssueSeverity(a.severity, b.severity); @@ -251,15 +258,22 @@ export function compareIssues(a: Issue, b: Issue): number { if (a.source && !b.source) { return 1; } - if (a.source?.file !== b.source?.file) { - return a.source?.file.localeCompare(b.source?.file || '') ?? 0; + if (a.source && b.source) { + const aId = getSourceIdentifier(a.source); + const bId = getSourceIdentifier(b.source); + if (aId !== bId) { + return aId.localeCompare(bId); + } + if (isFileSource(a.source) && isFileSource(b.source)) { + return compareSourceFilePosition(a.source.position, b.source.position); + } } - return compareSourceFilePosition(a.source?.position, b.source?.position); + return 0; } function compareSourceFilePosition( - a: NonNullable['position'], - b: NonNullable['position'], + a: SourceFileLocation['position'], + b: SourceFileLocation['position'], ): number { if (!a && b) { return -1; diff --git a/testing/test-fixtures/src/lib/utils/os-agnostic.ts b/testing/test-fixtures/src/lib/utils/os-agnostic.ts index e00d0f175..4cfd63924 100644 --- a/testing/test-fixtures/src/lib/utils/os-agnostic.ts +++ b/testing/test-fixtures/src/lib/utils/os-agnostic.ts @@ -1,6 +1,15 @@ -import type { AuditOutput, AuditReport } from '@code-pushup/models'; +import type { + AuditOutput, + AuditReport, + IssueSource, + SourceFileLocation, +} from '@code-pushup/models'; import { osAgnosticPath } from '@code-pushup/test-utils'; +function isFileSource(source: IssueSource): source is SourceFileLocation { + return 'file' in source; +} + export function osAgnosticAudit( audit: T, transformMessage: (message: string) => string = s => s, @@ -12,18 +21,19 @@ export function osAgnosticAudit( return { ...audit, details: { - issues: issues.map(issue => - issue.source == null - ? issue - : { - ...issue, - source: { - ...issue.source, - file: osAgnosticPath(issue.source.file), - }, - message: transformMessage(issue.message), - }, - ), + issues: issues.map(issue => { + if (issue.source == null || !isFileSource(issue.source)) { + return issue; + } + return { + ...issue, + source: { + ...issue.source, + file: osAgnosticPath(issue.source.file), + }, + message: transformMessage(issue.message), + }; + }), }, }; } diff --git a/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts b/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts index 727ca6289..f79e6ed28 100644 --- a/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts +++ b/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts @@ -46,7 +46,7 @@ describe('markdown-table-matcher', () => { it('should match table rows with an empty cell', () => { const markdown = ` - | Severity | Message | Source file | Line(s) | + | Severity | Message | Source | Location | | :--------: | :------------------------ | :-------------------- | :-----: | | 🚨 _error_ | File size is 20KB too big | \`list.component.ts\` | | `; From 5173f0039e04024ab745a91c391e078741bcad41 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 23 Jan 2026 20:29:12 -0500 Subject: [PATCH 3/4] refactor(core,ci): handle URL source union type --- packages/ci/src/lib/issues.ts | 7 +++--- .../src/lib/implementation/report-to-gql.ts | 3 ++- packages/core/src/lib/normalize.ts | 23 ++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/ci/src/lib/issues.ts b/packages/ci/src/lib/issues.ts index 2768c43e7..45c576a2c 100644 --- a/packages/ci/src/lib/issues.ts +++ b/packages/ci/src/lib/issues.ts @@ -2,11 +2,12 @@ import type { Audit, AuditReport, CategoryRef, - Issue, + FileIssue, PluginMeta, Report, ReportsDiff, } from '@code-pushup/models'; +import { isFileIssue } from '@code-pushup/utils'; import { type ChangedFiles, adjustFileName, @@ -14,7 +15,7 @@ import { isFileChanged, } from './git.js'; -export type SourceFileIssue = Required & IssueContext; +export type SourceFileIssue = FileIssue & IssueContext; type IssueContext = { audit: Pick; @@ -71,7 +72,7 @@ function getAuditIssues( ): SourceFileIssue[] { return ( audit.details?.issues - ?.filter((issue): issue is Required => issue.source?.file != null) + ?.filter(isFileIssue) .map(issue => ({ ...issue, audit, plugin })) ?? [] ); } diff --git a/packages/core/src/lib/implementation/report-to-gql.ts b/packages/core/src/lib/implementation/report-to-gql.ts index f70bc248d..e74b29b03 100644 --- a/packages/core/src/lib/implementation/report-to-gql.ts +++ b/packages/core/src/lib/implementation/report-to-gql.ts @@ -33,6 +33,7 @@ import type { TableAlignment, Tree, } from '@code-pushup/models'; +import { isFileIssue } from '@code-pushup/utils'; export function reportToGQL( report: Report, @@ -106,7 +107,7 @@ export function issueToGQL(issue: Issue): PortalIssue { return { message: issue.message, severity: issueSeverityToGQL(issue.severity), - ...(issue.source?.file && { + ...(isFileIssue(issue) && { sourceType: safeEnum('SourceCode'), sourceFilePath: issue.source.file, sourceStartLine: issue.source.position?.startLine, diff --git a/packages/core/src/lib/normalize.ts b/packages/core/src/lib/normalize.ts index 1b223529d..af690897d 100644 --- a/packages/core/src/lib/normalize.ts +++ b/packages/core/src/lib/normalize.ts @@ -1,18 +1,19 @@ import type { AuditOutputs, Issue } from '@code-pushup/models'; -import { formatGitPath, getGitRoot } from '@code-pushup/utils'; +import { formatGitPath, getGitRoot, isFileIssue } from '@code-pushup/utils'; export function normalizeIssue(issue: Issue, gitRoot: string): Issue { + // Early exit to avoid cloning; only file sources need path normalization + if (!isFileIssue(issue)) { + return issue; + } const { source, ...issueWithoutSource } = issue; - // early exit to avoid issue object cloning. - return source == null - ? issue - : { - ...issueWithoutSource, - source: { - ...source, - file: formatGitPath(source.file, gitRoot), - }, - }; + return { + ...issueWithoutSource, + source: { + ...source, + file: formatGitPath(source.file, gitRoot), + }, + }; } export async function normalizeAuditOutputs( From 2b58226bb27ef5044cbb36acbb301f4b069a24ab Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 23 Jan 2026 20:40:03 -0500 Subject: [PATCH 4/4] feat(plugin-axe): add source field to issues --- .../__snapshots__/collect.e2e.test.ts.snap | 29 ++++++++ e2e/plugin-axe-e2e/tests/collect.e2e.test.ts | 13 +++- packages/plugin-axe/src/lib/runner/run-axe.ts | 7 +- .../plugin-axe/src/lib/runner/transform.ts | 41 +++++------ .../src/lib/runner/transform.unit.test.ts | 72 +++++++++++++------ 5 files changed, 115 insertions(+), 47 deletions(-) diff --git a/e2e/plugin-axe-e2e/tests/__snapshots__/collect.e2e.test.ts.snap b/e2e/plugin-axe-e2e/tests/__snapshots__/collect.e2e.test.ts.snap index 045af4fe9..0c85fe667 100644 --- a/e2e/plugin-axe-e2e/tests/__snapshots__/collect.e2e.test.ts.snap +++ b/e2e/plugin-axe-e2e/tests/__snapshots__/collect.e2e.test.ts.snap @@ -499,6 +499,11 @@ exports[`PLUGIN collect report with axe-plugin NPM package > should run plugin o { "message": "[\`body > button\`] Fix any of the following: Element does not have inner text that is visible to screen readers aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute Element does not have an implicit (wrapped)