1
1
import {
2
2
TSDocTagDefinition ,
3
3
TSDocTagSyntaxKind ,
4
- TSDocConfiguration
4
+ TSDocConfiguration ,
5
+ ParserMessageLog ,
6
+ TSDocMessageId ,
7
+ ParserMessage ,
8
+ TextRange ,
9
+ IParserMessageParameters
5
10
} from '@microsoft/tsdoc' ;
6
11
import * as fs from 'fs' ;
7
12
import * as resolve from 'resolve' ;
@@ -42,35 +47,126 @@ interface IConfigJson {
42
47
*/
43
48
export class TSDocConfigFile {
44
49
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' ;
46
52
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
+ }
48
84
49
85
/**
50
86
* The full path of the file that was attempted to load.
51
87
*/
52
- public readonly filePath : string ;
88
+ public get filePath ( ) : string {
89
+ return this . _filePath ;
90
+ }
53
91
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
+ }
55
112
56
113
/**
57
114
* The `$schema` field from the `tsdocconfig.json` file.
58
115
*/
59
- public readonly tsdocSchema : string ;
116
+ public get tsdocSchema ( ) : string {
117
+ return this . _tsdocSchema ;
118
+ }
60
119
61
120
/**
62
121
* The `extends` field from the `tsdocconfig.json` file. For the parsed file contents,
63
122
* use the `extendsFiles` property instead.
64
123
*/
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
+ }
66
131
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 ( ) ;
68
139
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
+ }
74
170
75
171
for ( const jsonTagDefinition of configJson . tagDefinitions || [ ] ) {
76
172
let syntaxKind : TSDocTagSyntaxKind ;
@@ -82,52 +178,73 @@ export class TSDocConfigFile {
82
178
// The JSON schema should have caught this error
83
179
throw new Error ( 'Unexpected tag kind' ) ;
84
180
}
85
- tagDefinitions . push ( new TSDocTagDefinition ( {
181
+ this . _tagDefinitions . push ( new TSDocTagDefinition ( {
86
182
tagName : jsonTagDefinition . tagName ,
87
183
syntaxKind : syntaxKind ,
88
184
allowMultiple : jsonTagDefinition . allowMultiple
89
185
} ) ) ;
90
186
}
91
-
92
- this . tagDefinitions = tagDefinitions ;
93
187
}
94
188
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 {
101
191
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
+ }
115
200
116
- const configJson : IConfigJson = JSON . parse ( configJsonContent ) ;
117
- const success : boolean = tsdocSchemaValidator ( configJson ) as boolean ;
201
+ this . _filePath = path . resolve ( configFilePath ) ;
118
202
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 ;
123
210
}
124
211
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 ;
128
220
}
221
+ alreadyVisitedPaths . add ( hashKey ) ;
222
+
223
+ this . _loadJsonFile ( ) ;
224
+
225
+ const configFileFolder : string = path . dirname ( this . filePath ) ;
129
226
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
+ }
131
248
}
132
249
133
250
private static _findConfigPathForFolder ( folderPath : string ) : string {
@@ -152,45 +269,45 @@ export class TSDocConfigFile {
152
269
return '' ;
153
270
}
154
271
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
-
179
272
/**
180
273
* For the given folder, discover the relevant tsdocconfig.json files (if any), and load them.
181
274
* @param folderPath - the path to a folder where the search should start
182
275
*/
183
276
public static loadForFolder ( folderPath : string ) : TSDocConfigFile {
277
+ const configFile : TSDocConfigFile = new TSDocConfigFile ( ) ;
184
278
const rootConfigPath : string = TSDocConfigFile . _findConfigPathForFolder ( folderPath ) ;
279
+
185
280
const alreadyVisitedPaths : Set < string > = new Set < string > ( ) ;
186
- return TSDocConfigFile . _loadWithExtends ( rootConfigPath , alreadyVisitedPaths ) ;
281
+ configFile . _loadWithExtends ( rootConfigPath , undefined , alreadyVisitedPaths ) ;
282
+
283
+ return configFile ;
187
284
}
188
285
189
286
/**
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.
191
292
*/
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 ;
194
311
}
195
312
196
313
/**
0 commit comments