Skip to content

Commit 7494cde

Browse files
committed
Implement error reporting
1 parent 3e6e6b0 commit 7494cde

File tree

5 files changed

+281
-80
lines changed

5 files changed

+281
-80
lines changed

eslint-plugin/src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ const plugin: IPlugin = {
3131
// from the NPM package name, and then appending this string.
3232
"syntax": {
3333
meta: {
34-
messages: tsdocMessageIds,
34+
messages: {
35+
"error-loading-config-file": "Error loading TSDoc config file:\n{{details}}",
36+
...tsdocMessageIds
37+
},
3538
type: "problem",
3639
docs: {
3740
description: "Validates that TypeScript documentation comments conform to the TSDoc standard",
@@ -48,6 +51,19 @@ const plugin: IPlugin = {
4851
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadForFolder(sourceFilePath);
4952

5053
if (!tsdocConfigFile.fileNotFound) {
54+
if (tsdocConfigFile.hasErrors) {
55+
context.report({
56+
loc: {
57+
line: 1,
58+
column: 1
59+
},
60+
messageId: "error-loading-config-file",
61+
data: {
62+
details: tsdocConfigFile.getErrorSummary()
63+
}
64+
});
65+
}
66+
5167
tsdocConfigFile.configureParser(tsdocConfiguration);
5268
} else {
5369
// If we weren't able to find a tsdoc-config.json file, then by default we will use a lax configuration

tsdoc-config/src/TSDocConfigFile.ts

Lines changed: 190 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {
22
TSDocTagDefinition,
33
TSDocTagSyntaxKind,
4-
TSDocConfiguration
4+
TSDocConfiguration,
5+
ParserMessageLog,
6+
TSDocMessageId,
7+
ParserMessage,
8+
TextRange,
9+
IParserMessageParameters
510
} from '@microsoft/tsdoc';
611
import * as fs from 'fs';
712
import * as resolve from 'resolve';
@@ -42,35 +47,126 @@ interface IConfigJson {
4247
*/
4348
export class TSDocConfigFile {
4449
public static readonly FILENAME: string = 'tsdocconfig.json';
45-
public static readonly CURRENT_SCHEMA_URL: string = 'https://developer.microsoft.com/json-schemas/tsdoc/v1/tsdocconfig.schema.json';
50+
public static readonly CURRENT_SCHEMA_URL: string
51+
= 'https://developer.microsoft.com/json-schemas/tsdoc/v1/tsdocconfig.schema.json';
4652

47-
private readonly _extendsFiles: TSDocConfigFile[] = [];
53+
/**
54+
* A queryable log that reports warnings and error messages that occurred during parsing.
55+
*/
56+
public readonly log: ParserMessageLog;
57+
58+
private readonly _extendsFiles: TSDocConfigFile[];
59+
private _filePath: string;
60+
private _fileNotFound: boolean;
61+
private _hasErrors: boolean;
62+
private _tsdocSchema: string;
63+
private readonly _extendsPaths: string[];
64+
private readonly _tagDefinitions: TSDocTagDefinition[];
65+
66+
private constructor() {
67+
this.log = new ParserMessageLog();
68+
69+
this._extendsFiles = [];
70+
this._filePath = '';
71+
this._fileNotFound = true;
72+
this._hasErrors = false;
73+
this._tsdocSchema = '';
74+
this._extendsPaths = [];
75+
this._tagDefinitions= [];
76+
}
77+
78+
/**
79+
* Other config files that this file extends from.
80+
*/
81+
public get extendsFiles(): ReadonlyArray<TSDocConfigFile> {
82+
return this._extendsFiles;
83+
}
4884

4985
/**
5086
* The full path of the file that was attempted to load.
5187
*/
52-
public readonly filePath: string;
88+
public get filePath(): string {
89+
return this._filePath;
90+
}
5391

54-
public readonly fileNotFound: boolean = false;
92+
/**
93+
* If true, then the TSDocConfigFile object contains an empty state, because the `tsdocconfig.json` file could
94+
* not be found by the loader.
95+
*/
96+
public get fileNotFound(): boolean {
97+
return this._fileNotFound;
98+
}
99+
100+
/**
101+
* If true, then at least one error was encountered while loading this file or one of its "extends" files.
102+
*
103+
* @remarks
104+
* You can use {@link TSDocConfigFile.getErrorSummary} to report these errors.
105+
*
106+
* The individual messages can be retrieved from the {@link TSDocConfigFile.log} property of each `TSDocConfigFile`
107+
* object (including the {@link TSDocConfigFile.extendsFiles} tree).
108+
*/
109+
public get hasErrors(): boolean {
110+
return this._hasErrors;
111+
}
55112

56113
/**
57114
* The `$schema` field from the `tsdocconfig.json` file.
58115
*/
59-
public readonly tsdocSchema: string;
116+
public get tsdocSchema(): string {
117+
return this._tsdocSchema;
118+
}
60119

61120
/**
62121
* The `extends` field from the `tsdocconfig.json` file. For the parsed file contents,
63122
* use the `extendsFiles` property instead.
64123
*/
65-
public readonly extendsPaths: ReadonlyArray<string>;
124+
public get extendsPaths(): ReadonlyArray<string> {
125+
return this._extendsPaths;
126+
}
127+
128+
public get tagDefinitions(): ReadonlyArray<TSDocTagDefinition> {
129+
return this._tagDefinitions;
130+
}
66131

67-
public readonly tagDefinitions: ReadonlyArray<TSDocTagDefinition>;
132+
private _reportError(parserMessageParameters: IParserMessageParameters): void {
133+
this.log.addMessage(new ParserMessage(parserMessageParameters));
134+
this._hasErrors = true;
135+
}
136+
137+
private _loadJsonFile(): void {
138+
const configJsonContent: string = fs.readFileSync(this._filePath).toString();
68139

69-
private constructor(filePath: string, configJson: IConfigJson) {
70-
this.filePath = filePath;
71-
this.tsdocSchema = configJson.$schema;
72-
this.extendsPaths = configJson.extends || [];
73-
const tagDefinitions: TSDocTagDefinition[] = [];
140+
this._fileNotFound = false;
141+
142+
const configJson: IConfigJson = JSON.parse(configJsonContent);
143+
144+
if (configJson.$schema !== TSDocConfigFile.CURRENT_SCHEMA_URL) {
145+
this._reportError({
146+
messageId: TSDocMessageId.ConfigFileUnsupportedSchema,
147+
messageText: `Unsupported JSON "$schema" value; expecting "${TSDocConfigFile.CURRENT_SCHEMA_URL}"`,
148+
textRange: TextRange.empty
149+
});
150+
return;
151+
}
152+
153+
const success: boolean = tsdocSchemaValidator(configJson) as boolean;
154+
155+
if (!success) {
156+
const description: string = ajv.errorsText(tsdocSchemaValidator.errors);
157+
158+
this._reportError({
159+
messageId: TSDocMessageId.ConfigFileSchemaError,
160+
messageText: 'Error loading config file: ' + description,
161+
textRange: TextRange.empty
162+
});
163+
return;
164+
}
165+
166+
this._tsdocSchema = configJson.$schema;
167+
if (configJson.extends) {
168+
this._extendsPaths.push(...configJson.extends);
169+
}
74170

75171
for (const jsonTagDefinition of configJson.tagDefinitions || []) {
76172
let syntaxKind: TSDocTagSyntaxKind;
@@ -82,52 +178,73 @@ export class TSDocConfigFile {
82178
// The JSON schema should have caught this error
83179
throw new Error('Unexpected tag kind');
84180
}
85-
tagDefinitions.push(new TSDocTagDefinition({
181+
this._tagDefinitions.push(new TSDocTagDefinition({
86182
tagName: jsonTagDefinition.tagName,
87183
syntaxKind: syntaxKind,
88184
allowMultiple: jsonTagDefinition.allowMultiple
89185
}));
90186
}
91-
92-
this.tagDefinitions = tagDefinitions;
93187
}
94188

95-
/**
96-
* Other config files that this file extends from.
97-
*/
98-
public get extendsFiles(): ReadonlyArray<TSDocConfigFile> {
99-
return this._extendsFiles;
100-
}
189+
private _loadWithExtends(configFilePath: string, referencingConfigFile: TSDocConfigFile | undefined,
190+
alreadyVisitedPaths: Set<string>): void {
101191

102-
/**
103-
* Loads the contents of a single JSON input file.
104-
*
105-
* @remarks
106-
*
107-
* This method does not process the `extends` field of `tsdocconfig.json`.
108-
* For full functionality, including discovery of the file path, use the {@link TSDocConfigFileSet}
109-
* API instead.
110-
*/
111-
private static _loadSingleFile(jsonFilePath: string): TSDocConfigFile {
112-
const fullJsonFilePath: string = path.resolve(jsonFilePath);
113-
114-
const configJsonContent: string = fs.readFileSync(fullJsonFilePath).toString();
192+
if (!configFilePath) {
193+
this._reportError({
194+
messageId: TSDocMessageId.ConfigFileNotFound,
195+
messageText: 'File not found',
196+
textRange: TextRange.empty
197+
});
198+
return;
199+
}
115200

116-
const configJson: IConfigJson = JSON.parse(configJsonContent);
117-
const success: boolean = tsdocSchemaValidator(configJson) as boolean;
201+
this._filePath = path.resolve(configFilePath);
118202

119-
if (!success) {
120-
const description: string = ajv.errorsText(tsdocSchemaValidator.errors);
121-
throw new Error('Error parsing config file: ' + description
122-
+ '\nError in file: ' + jsonFilePath);
203+
if (!fs.existsSync(this._filePath)) {
204+
this._reportError({
205+
messageId: TSDocMessageId.ConfigFileNotFound,
206+
messageText: 'File not found',
207+
textRange: TextRange.empty
208+
});
209+
return;
123210
}
124211

125-
if (configJson.$schema !== TSDocConfigFile.CURRENT_SCHEMA_URL) {
126-
throw new Error('Expecting JSON "$schema" field to be ' + TSDocConfigFile.CURRENT_SCHEMA_URL
127-
+ '\nError in file: ' + jsonFilePath);
212+
const hashKey: string = fs.realpathSync(this._filePath);
213+
if (referencingConfigFile && alreadyVisitedPaths.has(hashKey)) {
214+
this._reportError({
215+
messageId: TSDocMessageId.ConfigFileCyclicExtends,
216+
messageText: `Circular reference encountered for "extends" field of "${referencingConfigFile.filePath}"`,
217+
textRange: TextRange.empty
218+
});
219+
return;
128220
}
221+
alreadyVisitedPaths.add(hashKey);
222+
223+
this._loadJsonFile();
224+
225+
const configFileFolder: string = path.dirname(this.filePath);
129226

130-
return new TSDocConfigFile(fullJsonFilePath, configJson);
227+
for (const extendsField of this.extendsPaths) {
228+
const resolvedExtendsPath: string = resolve.sync(extendsField, { basedir: configFileFolder });
229+
230+
const baseConfigFile: TSDocConfigFile = new TSDocConfigFile();
231+
232+
baseConfigFile._loadWithExtends(resolvedExtendsPath, this, alreadyVisitedPaths);
233+
234+
if (baseConfigFile.fileNotFound) {
235+
this._reportError({
236+
messageId: TSDocMessageId.ConfigFileUnresolvedExtends,
237+
messageText: `Unable to resolve "extends" reference to "${extendsField}"`,
238+
textRange: TextRange.empty
239+
});
240+
}
241+
242+
this._extendsFiles.push(baseConfigFile);
243+
244+
if (baseConfigFile.hasErrors) {
245+
this._hasErrors = true;
246+
}
247+
}
131248
}
132249

133250
private static _findConfigPathForFolder(folderPath: string): string {
@@ -152,45 +269,45 @@ export class TSDocConfigFile {
152269
return '';
153270
}
154271

155-
private static _loadWithExtends(configFilePath: string, alreadyVisitedPaths: Set<string>): TSDocConfigFile {
156-
const hashKey: string = fs.realpathSync(configFilePath);
157-
if (alreadyVisitedPaths.has(hashKey)) {
158-
throw new Error('Circular reference encountered for "extends" field of ' + configFilePath);
159-
}
160-
alreadyVisitedPaths.add(hashKey);
161-
162-
const configFile: TSDocConfigFile = TSDocConfigFile._loadSingleFile(configFilePath);
163-
164-
const configFileFolder: string = path.dirname(configFile.filePath);
165-
166-
for (const extendsField of configFile.extendsPaths) {
167-
const resolvedExtendsPath: string = resolve.sync(extendsField, { basedir: configFileFolder });
168-
if (!fs.existsSync(resolvedExtendsPath)) {
169-
throw new Error('Unable to resolve "extends" field of ' + configFilePath);
170-
}
171-
172-
const baseConfigFile: TSDocConfigFile = TSDocConfigFile._loadWithExtends(resolvedExtendsPath, alreadyVisitedPaths);
173-
configFile.addExtendsFile(baseConfigFile);
174-
}
175-
176-
return configFile;
177-
}
178-
179272
/**
180273
* For the given folder, discover the relevant tsdocconfig.json files (if any), and load them.
181274
* @param folderPath - the path to a folder where the search should start
182275
*/
183276
public static loadForFolder(folderPath: string): TSDocConfigFile {
277+
const configFile: TSDocConfigFile = new TSDocConfigFile();
184278
const rootConfigPath: string = TSDocConfigFile._findConfigPathForFolder(folderPath);
279+
185280
const alreadyVisitedPaths: Set<string> = new Set<string>();
186-
return TSDocConfigFile._loadWithExtends(rootConfigPath, alreadyVisitedPaths);
281+
configFile._loadWithExtends(rootConfigPath, undefined, alreadyVisitedPaths);
282+
283+
return configFile;
187284
}
188285

189286
/**
190-
* Adds an item to `TSDocConfigFile.extendsFiles`.
287+
* Returns a report of any errors that occurred while attempting to load this file or any files
288+
* referenced via the "extends" field.
289+
*
290+
* @remarks
291+
* Use {@link TSDocConfigFile.hasErrors} to determine whether any errors occurred.
191292
*/
192-
public addExtendsFile(otherFile: TSDocConfigFile): void {
193-
this._extendsFiles.push(otherFile);
293+
public getErrorSummary(): string {
294+
if (!this._hasErrors) {
295+
return 'No errors.';
296+
}
297+
298+
let result: string = `Errors encountered for ${this.filePath}:\n`;
299+
300+
for (const message of this.log.messages) {
301+
result += ` ${message.text}\n`;
302+
}
303+
304+
for (const extendsFile of this.extendsFiles) {
305+
if (extendsFile.hasErrors) {
306+
result += extendsFile.getErrorSummary();
307+
}
308+
}
309+
310+
return result;
194311
}
195312

196313
/**

0 commit comments

Comments
 (0)