Skip to content

Commit acb8b52

Browse files
authored
infra: improve error messages for parameter properties (#3082)
1 parent f128d77 commit acb8b52

File tree

6 files changed

+99
-51
lines changed

6 files changed

+99
-51
lines changed

scripts/apidocs/processing/error.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import { FakerError } from '../../../src/errors/faker-error';
2+
import { CI_PREFLIGHT } from '../../env';
23
import type { SourceableNode } from './source';
34
import { getSourcePath } from './source';
45

56
export class FakerApiDocsProcessingError extends FakerError {
67
constructor(options: {
78
type: string;
89
name: string;
9-
source: string | SourceableNode;
10+
source: SourceableNode;
1011
cause: unknown;
1112
}) {
1213
const { type, name, source, cause } = options;
13-
const sourceText =
14-
typeof source === 'string' ? source : getSourcePathText(source);
14+
15+
const mainText = `Failed to process ${type} '${name}'`;
1516
const causeText = cause instanceof Error ? cause.message : '';
16-
super(`Failed to process ${type} ${name} at ${sourceText} : ${causeText}`, {
17+
const { filePath, line, column } = getSourcePath(source);
18+
const sourceText = `${filePath}:${line}:${column}`;
19+
20+
if (CI_PREFLIGHT) {
21+
const sourceArgs = `file=${filePath},line=${line},col=${column}`;
22+
console.log(`::error ${sourceArgs}::${mainText}: ${causeText}`);
23+
}
24+
25+
super(`${mainText} at ${sourceText} : ${causeText}`, {
1726
cause,
1827
});
1928
}
@@ -22,7 +31,7 @@ export class FakerApiDocsProcessingError extends FakerError {
2231
export function newProcessingError(options: {
2332
type: string;
2433
name: string;
25-
source: string | SourceableNode;
34+
source: SourceableNode;
2635
cause: unknown;
2736
}): FakerApiDocsProcessingError {
2837
const { cause } = options;
@@ -33,8 +42,3 @@ export function newProcessingError(options: {
3342

3443
return new FakerApiDocsProcessingError(options);
3544
}
36-
37-
function getSourcePathText(source: SourceableNode): string {
38-
const { filePath, line, column } = getSourcePath(source);
39-
return `${filePath}:${line}:${column}`;
40-
}

scripts/apidocs/processing/jsdocs.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
export type JSDocableLikeNode = Pick<JSDocableNode, 'getJsDocs'>;
1111

1212
export function getJsDocs(node: JSDocableLikeNode): JSDoc {
13-
return exactlyOne(node.getJsDocs(), 'jsdocs');
13+
return exactlyOne(
14+
node.getJsDocs(),
15+
'jsdocs',
16+
'Please ensure that each method signature has JSDocs, and that all properties of option/object parameters are documented with both @param tags and inline JSDocs.'
17+
);
1418
}
1519

1620
export function getDeprecated(jsdocs: JSDoc): string | undefined {

scripts/apidocs/processing/parameter.ts

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
PropertySignature,
3+
Symbol,
34
Type,
45
TypeParameterDeclaration,
56
} from 'ts-morph';
@@ -174,30 +175,43 @@ function processComplexParameter(
174175
return type
175176
.getApparentProperties()
176177
.flatMap((parameter) => {
177-
const declaration = exactlyOne(
178-
parameter.getDeclarations(),
179-
'property declaration'
180-
) as PropertySignature;
181-
const propertyType = declaration.getType();
182-
const jsdocs = getJsDocs(declaration);
183-
const deprecated = getDeprecated(jsdocs);
184-
185-
return [
186-
{
187-
name: `${name}.${parameter.getName()}${getNameSuffix(propertyType)}`,
188-
type: getTypeText(propertyType, {
189-
abbreviate: false,
190-
stripUndefined: true,
191-
}),
192-
default: getDefault(jsdocs),
193-
description:
194-
getDescription(jsdocs) +
195-
(deprecated ? `\n\n**DEPRECATED:** ${deprecated}` : ''),
196-
},
197-
];
178+
try {
179+
return processComplexParameterProperty(name, parameter);
180+
} catch (error) {
181+
throw newProcessingError({
182+
type: 'property',
183+
name: `${name}.${parameter.getName()}`,
184+
source: parameter.getDeclarations()[0],
185+
cause: error,
186+
});
187+
}
198188
})
199189
.sort((a, b) => a.name.localeCompare(b.name));
200190
}
201191

202192
return [];
203193
}
194+
195+
function processComplexParameterProperty(name: string, parameter: Symbol) {
196+
const declaration = exactlyOne(
197+
parameter.getDeclarations(),
198+
'property declaration'
199+
) as PropertySignature;
200+
const propertyType = declaration.getType();
201+
const jsdocs = getJsDocs(declaration);
202+
const deprecated = getDeprecated(jsdocs);
203+
204+
return [
205+
{
206+
name: `${name}.${parameter.getName()}${getNameSuffix(propertyType)}`,
207+
type: getTypeText(propertyType, {
208+
abbreviate: false,
209+
stripUndefined: true,
210+
}),
211+
default: getDefault(jsdocs),
212+
description:
213+
getDescription(jsdocs) +
214+
(deprecated ? `\n\n**DEPRECATED:** ${deprecated}` : ''),
215+
},
216+
];
217+
}
Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
export function exactlyOne<T>(input: ReadonlyArray<T>, property: string): T {
1+
export function exactlyOne<T>(
2+
input: ReadonlyArray<T>,
3+
property: string,
4+
extraDescription: string = ''
5+
): T {
26
if (input.length !== 1) {
37
throw new Error(
4-
`Expected exactly one element for ${property}, got ${input.length}`
8+
`Expected exactly one ${property} element, got ${input.length}. ${extraDescription}`
59
);
610
}
711

@@ -10,11 +14,12 @@ export function exactlyOne<T>(input: ReadonlyArray<T>, property: string): T {
1014

1115
export function optionalOne<T>(
1216
input: ReadonlyArray<T>,
13-
property: string
17+
property: string,
18+
extraDescription: string = ''
1419
): T | undefined {
1520
if (input.length > 1) {
1621
throw new Error(
17-
`Expected one optional element for ${property}, got ${input.length}`
22+
`Expected one optional ${property} element, got ${input.length}. ${extraDescription}`
1823
);
1924
}
2025

@@ -23,47 +28,66 @@ export function optionalOne<T>(
2328

2429
export function required<T>(
2530
input: T | undefined,
26-
property: string
31+
property: string,
32+
extraDescription: string = ''
2733
): NonNullable<T> {
2834
if (input == null) {
29-
throw new Error(`Expected a value for ${property}, got undefined`);
35+
throw new Error(
36+
`Expected a value for ${property}, got undefined. ${extraDescription}`
37+
);
3038
}
3139

3240
return input;
3341
}
3442

3543
export function allRequired<T>(
3644
input: ReadonlyArray<T | undefined>,
37-
property: string
45+
property: string,
46+
extraDescription: string = ''
3847
): Array<NonNullable<T>> {
39-
return input.map((v, i) => required(v, `${property}[${i}]`));
48+
return input.map((v, i) =>
49+
required(v, `${property}[${i}]`, extraDescription)
50+
);
4051
}
4152

4253
export function atLeastOne<T>(
4354
input: ReadonlyArray<T>,
44-
property: string
55+
property: string,
56+
extraDescription: string = ''
4557
): ReadonlyArray<T> {
4658
if (input.length === 0) {
47-
throw new Error(`Expected at least one element for ${property}`);
59+
throw new Error(
60+
`Expected at least one ${property} element. ${extraDescription}`
61+
);
4862
}
4963

5064
return input;
5165
}
5266

5367
export function atLeastOneAndAllRequired<T>(
5468
input: ReadonlyArray<T | undefined>,
55-
property: string
69+
property: string,
70+
extraDescription: string = ''
5671
): ReadonlyArray<NonNullable<T>> {
57-
return atLeastOne(allRequired(input, property), property);
72+
return atLeastOne(
73+
allRequired(input, property, extraDescription),
74+
property,
75+
extraDescription
76+
);
5877
}
5978

60-
export function valueForKey<T>(input: Record<string, T>, key: string): T {
61-
return required(input[key], key);
79+
export function valueForKey<T>(
80+
input: Record<string, T>,
81+
key: string,
82+
extraDescription: string = ''
83+
): T {
84+
return required(input[key], key, extraDescription);
6285
}
6386

6487
export function valuesForKeys<T>(
6588
input: Record<string, T>,
66-
keys: string[]
89+
keys: string[],
90+
extraDescription: string = ''
6791
): T[] {
68-
return keys.map((key) => valueForKey(input, key));
92+
return keys.map((key) => valueForKey(input, key, extraDescription));
6993
}

scripts/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { env } from 'node:process';
2+
3+
export const CI_PREFLIGHT = env.CI_PREFLIGHT === 'true';

vitest.config.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineConfig } from 'vitest/config';
2+
import { CI_PREFLIGHT } from './scripts/env';
23

34
const VITEST_SEQUENCE_SEED = Date.now();
45

@@ -14,9 +15,7 @@ export default defineConfig({
1415
reporter: ['clover', 'cobertura', 'lcov', 'text'],
1516
include: ['src'],
1617
},
17-
reporters: process.env.CI_PREFLIGHT
18-
? ['basic', 'github-actions']
19-
: ['basic'],
18+
reporters: CI_PREFLIGHT ? ['basic', 'github-actions'] : ['basic'],
2019
sequence: {
2120
seed: VITEST_SEQUENCE_SEED,
2221
shuffle: true,

0 commit comments

Comments
 (0)