Skip to content

Commit

Permalink
feat(utils): support local report links
Browse files Browse the repository at this point in the history
  • Loading branch information
hanna-skryl authored Oct 1, 2024
1 parent 7b6d7da commit f98b10a
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 52 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/lib/implementation/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function persistReport(
case 'md':
return {
format: 'md',
content: generateMdReport(sortedScoredReport),
content: generateMdReport(sortedScoredReport, { outputDir }),
};
}
});
Expand Down
4 changes: 4 additions & 0 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export {
type TableCellValue,
tableCellValueSchema,
} from './lib/implementation/schemas';
export {
type SourceFileLocation,
sourceFileLocationSchema,
} from './lib/source';

export { type Audit, auditSchema } from './lib/audit';
export {
Expand Down
20 changes: 1 addition & 19 deletions packages/models/src/lib/issue.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { z } from 'zod';
import { MAX_ISSUE_MESSAGE_LENGTH } from './implementation/limits';
import { filePathSchema, positiveIntSchema } from './implementation/schemas';

const sourceFileLocationSchema = z.object(
{
file: filePathSchema.describe('Relative path to source file in Git repo'),
position: z
.object(
{
startLine: positiveIntSchema.describe('Start line'),
startColumn: positiveIntSchema.describe('Start column').optional(),
endLine: positiveIntSchema.describe('End line').optional(),
endColumn: positiveIntSchema.describe('End column').optional(),
},
{ description: 'Location in file' },
)
.optional(),
},
{ description: 'Source file location' },
);
import { sourceFileLocationSchema } from './source';

export const issueSeveritySchema = z.enum(['info', 'warning', 'error'], {
description: 'Severity level',
Expand Down
22 changes: 22 additions & 0 deletions packages/models/src/lib/source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';
import { filePathSchema, positiveIntSchema } from './implementation/schemas';

export const sourceFileLocationSchema = z.object(
{
file: filePathSchema.describe('Relative path to source file in Git repo'),
position: z
.object(
{
startLine: positiveIntSchema.describe('Start line'),
startColumn: positiveIntSchema.describe('Start column').optional(),
endLine: positiveIntSchema.describe('End line').optional(),
endColumn: positiveIntSchema.describe('End column').optional(),
},
{ description: 'Location in file' },
)
.optional(),
},
{ description: 'Source file location' },
);

export type SourceFileLocation = z.infer<typeof sourceFileLocationSchema>;
81 changes: 80 additions & 1 deletion packages/utils/src/lib/reports/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import {
MarkdownDocument,
md,
} from 'build-md';
import type { AuditReport, Table } from '@code-pushup/models';
import { posix as pathPosix } from 'node:path';
import type {
AuditReport,
SourceFileLocation,
Table,
} from '@code-pushup/models';
import { HIERARCHY } from '../text-formats';
import {
columnsToStringArray,
getColumnAlignments,
rowToStringArray,
} from '../text-formats/table';
import { getEnvironmentType, getGitHubBaseUrl } from './ide-environment';
import type { MdReportOptions } from './types';

export function tableSection(
tableData: Table,
Expand Down Expand Up @@ -58,3 +65,75 @@ export function metaDescription(
}
return '';
}

/**
* Link to local source for IDE
* @param source
* @param reportLocation
*
* @example
* linkToLocalSourceInIde({ file: 'src/index.ts' }, { outputDir: '.code-pushup' }) // [`src/index.ts`](../src/index.ts)
*/
export function linkToLocalSourceForIde(
source: SourceFileLocation,
options?: Pick<MdReportOptions, 'outputDir'>,
): InlineText {
const { file, position } = source;
const { outputDir } = options ?? {};

// NOT linkable
if (!outputDir) {
return md.code(file);
}

return md.link(formatFileLink(file, position, outputDir), md.code(file));
}

export function formatSourceLine(
position: SourceFileLocation['position'],
): string {
if (!position) {
return '';
}
const { startLine, endLine } = position;
return endLine && startLine !== endLine
? `${startLine}-${endLine}`
: `${startLine}`;
}

export function formatGitHubLink(
file: string,
position: SourceFileLocation['position'],
): string {
const baseUrl = getGitHubBaseUrl();
if (!position) {
return `${baseUrl}/${file}`;
}
const { startLine, endLine, startColumn, endColumn } = position;
const start = startColumn ? `L${startLine}C${startColumn}` : `L${startLine}`;
const end = endLine
? endColumn
? `L${endLine}C${endColumn}`
: `L${endLine}`
: '';
const lineRange = end && start !== end ? `${start}-${end}` : start;
return `${baseUrl}/${file}#${lineRange}`;
}

export function formatFileLink(
file: string,
position: SourceFileLocation['position'],
outputDir: string,
): string {
const relativePath = pathPosix.relative(outputDir, file);
const env = getEnvironmentType();

switch (env) {
case 'vscode':
return position ? `${relativePath}#L${position.startLine}` : relativePath;
case 'github':
return formatGitHubLink(file, position);
default:
return relativePath;
}
}
178 changes: 177 additions & 1 deletion packages/utils/src/lib/reports/formatting.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { describe, expect, it } from 'vitest';
import { metaDescription, tableSection } from './formatting';
import { toUnixPath } from '../transform';
import {
formatFileLink,
formatGitHubLink,
formatSourceLine,
linkToLocalSourceForIde,
metaDescription,
tableSection,
} from './formatting';

describe('tableSection', () => {
it('should accept a title', () => {
Expand Down Expand Up @@ -112,3 +120,171 @@ describe('metaDescription', () => {
);
});
});

describe('formatSourceLine', () => {
it.each([
[{ startLine: 2 }, '2'],
[{ startLine: 2, endLine: undefined }, '2'],
[{ startLine: 2, endLine: 2 }, '2'],
[{ startLine: 2, endLine: 3 }, '2-3'],
])('should format position %o as "%s"', (position, expected) => {
expect(formatSourceLine(position)).toBe(expected);
});

it('should return an empty string when the position is missing', () => {
expect(formatSourceLine(undefined)).toBe('');
});
});

describe('linkToLocalSourceForIde', () => {
it('should not format the file path as a link when outputDir is undefined (VS Code)', () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
vi.stubEnv('GITHUB_ACTIONS', 'false');

expect(
linkToLocalSourceForIde({
file: toUnixPath('packages/utils/src/index.ts'),
}).toString(),
).toBe('`packages/utils/src/index.ts`');
});

it('should format the file path as a link when outputDir is defined (VS Code)', () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
vi.stubEnv('GITHUB_ACTIONS', 'false');
const filePath = toUnixPath('packages/utils/src/index.ts');
const outputDir = toUnixPath('.code-pushup');

expect(
linkToLocalSourceForIde({ file: filePath }, { outputDir }).toString(),
).toBe('[`packages/utils/src/index.ts`](../packages/utils/src/index.ts)');
});

it('should return a link to a specific line when startLine is provided (VS Code)', () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
vi.stubEnv('GITHUB_ACTIONS', 'false');
const filePath = toUnixPath('packages/utils/src/index.ts');
const outputDir = toUnixPath('.code-pushup');

expect(
linkToLocalSourceForIde(
{ file: filePath, position: { startLine: 2 } },
{ outputDir },
).toString(),
).toBe(
'[`packages/utils/src/index.ts`](../packages/utils/src/index.ts#L2)',
);
});
});

describe('formatGitHubLink', () => {
beforeEach(() => {
vi.stubEnv('TERM_PROGRAM', '');
vi.stubEnv('GITHUB_ACTIONS', 'true');
vi.stubEnv('GITHUB_SERVER_URL', 'https://github.com');
vi.stubEnv('GITHUB_REPOSITORY', 'user/repo');
vi.stubEnv('GITHUB_SHA', '1234567890abcdef');
});

it.each([
[
{ startLine: 2 },
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2',
],
[
{ startLine: 2, endLine: 5 },
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2-L5',
],
[
{ startLine: 2, startColumn: 1 },
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2C1',
],
[
{ startLine: 2, endLine: 2, startColumn: 1, endColumn: 5 },
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2C1-L2C5',
],
[
{ startLine: 2, endLine: 5, startColumn: 1, endColumn: 6 },
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2C1-L5C6',
],
[
{ startLine: 2, endLine: 2, startColumn: 1, endColumn: 1 },
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2C1',
],
])(
'should generate a GitHub repository link for the file with position %o',
(position, expected) => {
expect(formatGitHubLink(toUnixPath('src/index.ts'), position)).toBe(
expected,
);
},
);

it('should generate a GitHub repository link for the file when the position is undefined', () => {
expect(formatGitHubLink(toUnixPath('src/index.ts'), undefined)).toBe(
'https://github.com/user/repo/blob/1234567890abcdef/src/index.ts',
);
});
});

describe('formatFileLink', () => {
it('should return a GitHub repository link when running in GitHub Actions', () => {
vi.stubEnv('TERM_PROGRAM', '');
vi.stubEnv('GITHUB_ACTIONS', 'true');
vi.stubEnv('GITHUB_SERVER_URL', 'https://github.com');
vi.stubEnv('GITHUB_REPOSITORY', 'user/repo');
vi.stubEnv('GITHUB_SHA', '1234567890abcdef');

expect(
formatFileLink(
toUnixPath('src/index.ts'),
{ startLine: 2 },
toUnixPath('.code-pushup'),
),
).toBe(
`https://github.com/user/repo/blob/1234567890abcdef/src/index.ts#L2`,
);
});

it.each([
[{ startLine: 2 }, '../src/index.ts#L2'],
[{ startLine: 2, endLine: 5 }, '../src/index.ts#L2'],
[{ startLine: 2, startColumn: 1 }, '../src/index.ts#L2'],
])(
'should transform the file path by including position %o when running in VS Code',
(position, expected) => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
vi.stubEnv('GITHUB_ACTIONS', 'false');
expect(
formatFileLink(
toUnixPath('src/index.ts'),
position,
toUnixPath('.code-pushup'),
),
).toBe(expected);
},
);

it('should return a relative file path when the position is undefined (VS Code)', () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
vi.stubEnv('GITHUB_ACTIONS', 'false');
expect(
formatFileLink(
toUnixPath('src/index.ts'),
undefined,
toUnixPath('.code-pushup'),
),
).toBe('../src/index.ts');
});

it('should return a relative file path when the environment is neither VS Code nor GitHub', () => {
vi.stubEnv('TERM_PROGRAM', '');
vi.stubEnv('GITHUB_ACTIONS', 'false');
expect(
formatFileLink(
toUnixPath('src/index.ts'),
{ startLine: 2, startColumn: 1 },
toUnixPath('.code-pushup'),
),
).toBe('../src/index.ts');
});
});
Loading

0 comments on commit f98b10a

Please sign in to comment.