Skip to content

Commit 6b3c824

Browse files
authored
Merge pull request microsoft#56 from Microsoft/pgonzal/fix-errors
Improve confusing error messages from api-extractor
2 parents f26da6f + b2b8681 commit 6b3c824

10 files changed

Lines changed: 137 additions & 103 deletions

File tree

api-extractor/src/DebugRun.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import ApiJsonGenerator from './generators/ApiJsonGenerator';
99

1010
const analyzer: Analyzer = new Analyzer(
1111
(message: string, fileName: string, lineNumber: number): void => {
12-
console.log(`TypeScript error: ${message}` + os.EOL
12+
console.log(`ErrorHandler: ${message}` + os.EOL
1313
+ ` ${fileName}#${lineNumber}`);
1414
}
1515
);
@@ -25,9 +25,9 @@ analyzer.analyze({
2525
moduleResolution: ts.ModuleResolutionKind.NodeJs,
2626
experimentalDecorators: true,
2727
jsx: ts.JsxEmit.React,
28-
rootDir: ''
28+
rootDir: 'D:/GitRepos/sp-client/spfx-core/sp-codepart-base'
2929
},
30-
entryPointFile: '',
30+
entryPointFile: 'D:/GitRepos/sp-client/spfx-core/sp-codepart-base/src/index.ts',
3131
otherFiles: []
3232
});
3333

@@ -36,3 +36,5 @@ apiFileGenerator.writeApiFile('./lib/DebugRun.api.ts', analyzer);
3636

3737
const apiJsonGenerator: ApiJsonGenerator = new ApiJsonGenerator();
3838
apiJsonGenerator.writeJsonFile('./lib/DebugRun.json', analyzer);
39+
40+
console.log('DebugRun completed.');

api-extractor/src/DocElementParser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ITextElement, IDocElement, IHrefLinkElement, ICodeLinkElement, ISeeDocElement } from './IDocElement';
22
import { IApiDefinitionReference } from './IApiDefinitionReference';
33
import ApiDocumentation from './definitions/ApiDocumentation';
4-
import Token from './Token';
4+
import Token, { TokenType } from './Token';
55
import Tokenizer from './Tokenizer';
66

77
export default class DocElementParser {
@@ -47,7 +47,7 @@ export default class DocElementParser {
4747
break;
4848
}
4949

50-
if (token.type === 'Tag') {
50+
if (token.type === TokenType.Tag) {
5151
switch (token.tag) {
5252
case '@see':
5353
tokenizer.getToken();
@@ -60,7 +60,7 @@ export default class DocElementParser {
6060
parsing = false; // end of summary tokens
6161
break;
6262
}
63-
} else if (token.type === 'Inline') {
63+
} else if (token.type === TokenType.Inline) {
6464
switch (token.tag) {
6565
case '@link' :
6666
const linkDocElement: ICodeLinkElement | IHrefLinkElement = this.parseLinkTag(token, reportError);
@@ -73,7 +73,7 @@ export default class DocElementParser {
7373
parsing = false;
7474
break;
7575
}
76-
} else if (token.type === 'Text') {
76+
} else if (token.type === TokenType.Text) {
7777
docElements.push({kind: 'textDocElement', value: token.text} as ITextElement);
7878
tokenizer.getToken();
7979
} else {

api-extractor/src/Token.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Allowed Token types.
33
*/
4-
export enum TokenTypes {
4+
export enum TokenType {
55
/**
66
* A Token that contains only text.
77
*/
@@ -14,10 +14,10 @@ export enum TokenTypes {
1414
Tag,
1515

1616
/**
17-
* This is a specific kind of Tag that is important to
17+
* This is a specific kind of Tag that is important to
1818
* distinguish because it contains additional parameters.
19-
*
20-
* Example:
19+
*
20+
* Example:
2121
* \{@link http://microosft.com | microsoft home \}
2222
* \{@inheritdoc @ microsoft/sp-core-library:Guid.newGuid \}
2323
*/
@@ -30,10 +30,9 @@ export enum TokenTypes {
3030
export default class Token {
3131

3232
/**
33-
* The type of the token.
34-
* Possible options: Text, Tag, Inline.
33+
* The type of the token.
3534
*/
36-
private _type: string;
35+
private _type: TokenType;
3736

3837
/**
3938
* This is not used for Text.
@@ -46,7 +45,7 @@ export default class Token {
4645
*/
4746
private _text: string;
4847

49-
constructor(type: string, tag?: string, text?: string) {
48+
constructor(type: TokenType, tag?: string, text?: string) {
5049
this._type = type;
5150
this._tag = tag ? tag : '';
5251
this._text = text ? this._unescape(text) : '';
@@ -56,13 +55,13 @@ export default class Token {
5655
/**
5756
* Determines if the type is not what we expect.
5857
*/
59-
public requireType(type: string): void {
58+
public requireType(type: TokenType): void {
6059
if (this._type !== type) {
61-
throw new Error('Token of type \"${this._type}\" is not of required type \"${type}\"');
60+
throw new Error(`Encountered a token of type \"${this._type}\" when expecting \"${type}\"`);
6261
}
6362
}
6463

65-
public get type(): string {
64+
public get type(): TokenType {
6665
return this._type;
6766
}
6867

api-extractor/src/Tokenizer.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Token from './Token';
1+
import Token, { TokenType } from './Token';
22
import TypeScriptHelpers from './TypeScriptHelpers';
33

44
/**
@@ -29,7 +29,7 @@ export default class Tokenizer {
2929
* can be processed more strictly.
3030
* Example: "This is a JsDoc description with a {@link URL} and more text. \@summary example \@public"
3131
* => [
32-
* {tokenType: 'text', parameter: 'This is a JsDoc description with a'},
32+
* {tokenType: 'text', parameter: 'This is a JsDoc description with a'},
3333
* {tokenType: '@link', parameter: 'URL'},
3434
* {tokenType: '\@summary', parameter: ''},
3535
* {tokenType: 'text', parameter: 'example'},
@@ -41,7 +41,7 @@ export default class Tokenizer {
4141
return;
4242
}
4343
const docEntries: string[] = TypeScriptHelpers.splitStringWithRegEx(docs, Tokenizer._jsdocTagsRegex);
44-
const sanitizedTokens: string[] = this._sanitizeDocEntries(docEntries); // remove white space and empty entries
44+
const sanitizedTokens: string[] = this._sanitizeDocEntries(docEntries); // remove white space and empty entries
4545

4646
// process each sanitized doc string to a Token object
4747
const tokens: Token[] = [];
@@ -50,11 +50,11 @@ export default class Tokenizer {
5050
let token: Token;
5151
value = sanitizedTokens[i];
5252
if (value.charAt(0) === '@') {
53-
token = new Token('Tag', value);
53+
token = new Token(TokenType.Tag, value);
5454
} else if (value.charAt(0) === '{' && value.charAt(value.length - 1) === '}') {
5555
token = this._tokenizeInline(value); // Can return undefined if invalid inline tag
5656
} else {
57-
token = new Token('Text', '', value);
57+
token = new Token(TokenType.Text, '', value);
5858
}
5959

6060
if (token) {
@@ -66,7 +66,7 @@ export default class Tokenizer {
6666
}
6767

6868
/**
69-
* Parse an inline tag and returns the Token for it if itis a valid inline tag.
69+
* Parse an inline tag and returns the Token for it if itis a valid inline tag.
7070
* Example '{@link https://bing.com | Bing}' => '{type: 'Inline', tag: '@link', text: 'https://bing.com | Bing'}'
7171
*/
7272
protected _tokenizeInline(docEntry: string): Token {
@@ -98,11 +98,11 @@ export default class Tokenizer {
9898
}
9999

100100
tokenChunks.shift(); // Gets rid of '@link'
101-
const token: Token = new Token('Inline', '@link', tokenChunks.join(' '));
101+
const token: Token = new Token(TokenType.Inline, '@link', tokenChunks.join(' '));
102102
return token;
103103
} else if (tokenChunks[0] === '@inheritdoc') {
104104
tokenChunks.shift(); // Gets rid of '@inheritdoc'
105-
const token: Token = new Token('Inline', '@inheritdoc', tokenChunks.join(' '));
105+
const token: Token = new Token(TokenType.Inline, '@inheritdoc', tokenChunks.join(' '));
106106
return token;
107107
}
108108

@@ -119,7 +119,7 @@ export default class Tokenizer {
119119
}
120120

121121
/**
122-
* Trims whitespaces on either end of the entry (which is just a string within the doc comments),
122+
* Trims whitespaces on either end of the entry (which is just a string within the doc comments),
123123
* replaces \r and \n's with single whitespace, and removes empty entries.
124124
*
125125
* @param docEntries - Array of doc strings to be santitized
@@ -138,4 +138,4 @@ export default class Tokenizer {
138138

139139
return result;
140140
}
141-
}
141+
}

api-extractor/src/definitions/ApiDocumentation.ts

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IDocElement, IParam, IHrefLinkElement, ICodeLinkElement, ITextElement }
88
import { IDocItem, IDocFunction } from '../IDocItem';
99
import DocItemLoader from '../DocItemLoader';
1010
import { IApiDefinitionReference } from '../IApiDefinitionReference';
11-
import Token from '../Token';
11+
import Token, { TokenType } from '../Token';
1212
import Tokenizer from '../Tokenizer';
1313

1414
/**
@@ -64,15 +64,13 @@ export default class ApiDocumentation {
6464
// For guidance about using these tags, please see this document:
6565
// https://onedrive.visualstudio.com/DefaultCollection/SPPPlat/_git/sp-client
6666
// ?path=/common/docs/ApiPrinciplesAndProcess.md
67-
private static _allowedJsdocTags: string[] = [
67+
private static _allowedRegularJsdocTags: string[] = [
6868
// (alphabetical order)
6969
'@alpha',
7070
'@beta',
7171
'@betadocumentation',
72-
'@inheritdoc',
7372
'@internal',
7473
'@internalremarks',
75-
'@link',
7674
'@param',
7775
'@preapproved',
7876
'@public',
@@ -84,6 +82,12 @@ export default class ApiDocumentation {
8482
'@remarks'
8583
];
8684

85+
private static _allowedInlineJsdocTags: string[] = [
86+
// (alphabetical order)
87+
'@inheritdoc',
88+
'@link'
89+
];
90+
8791
/**
8892
* Match JsDoc block tags and inline tags
8993
* Example "@a @b@c d@e @f {whatever} {@link a} { @something } \@g" => ["@a", "@f", "{@link a}", "{ @something }"]
@@ -282,14 +286,12 @@ export default class ApiDocumentation {
282286
// if this documentation inherits docs from a deprecated API item, then
283287
// this documentation must either have a deprecated message or it must
284288
// not use the @inheritdoc and copy+paste the documentation
285-
this.reportError(`Use of @inheritdoc API item reference that is deprecated. ` +
286-
'Either include @deprecated JSDoc with message on this item or remove the @inheritdoc tag ' +
287-
'and copy+paste the documentation.');
289+
this.reportError(`A deprecation message must be included after the @deprecated tag.`);
288290
}
289291
break;
290292
}
291293

292-
if (token.type === 'Tag') {
294+
if (token.type === TokenType.Tag) {
293295
switch (token.tag) {
294296
case '@remarks':
295297
tokenizer.getToken();
@@ -355,15 +357,9 @@ export default class ApiDocumentation {
355357
break;
356358
default:
357359
tokenizer.getToken();
358-
if (ApiDocumentation._allowedJsdocTags.indexOf(token.tag) < 0) {
359-
this.reportError(`The JSDoc tag \"${token.tag}\" is not allowed`);
360-
break;
361-
} else {
362-
this.reportError(`Error formatting token tag: ${token.tag}`);
363-
break;
364-
}
360+
this._reportBadJSDocTag(token);
365361
}
366-
} else if (token.type === 'Inline') {
362+
} else if (token.type === TokenType.Inline) {
367363
switch (token.tag) {
368364
case '@inheritdoc':
369365
tokenizer.getToken();
@@ -383,15 +379,21 @@ export default class ApiDocumentation {
383379
break;
384380
default:
385381
tokenizer.getToken();
386-
this.reportError(`Unidentifiable inline token ${token.tag}`);
382+
this._reportBadJSDocTag(token);
387383
break;
388384
}
389-
} else if (token.type === 'Text') {
385+
} else if (token.type === TokenType.Text) {
390386
tokenizer.getToken();
391-
this.reportError('Unexpected text. Text must either be the first sentences of the JSDoc, or if too long for ' +
392-
'the first 2-3 sentences the text must be preceded by a @internalremarks tag.');
387+
// Shorten "This is too long text" to "This is..."
388+
const MAX_LENGTH: number = 40;
389+
let problemText: string = token.text.trim();
390+
if (problemText.length > MAX_LENGTH) {
391+
problemText = problemText.substr(0, MAX_LENGTH - 3).trim() + '...';
392+
}
393+
this.reportError(`Unexpected text in JSDoc comment: "${problemText}"`);
393394
} else {
394395
tokenizer.getToken();
396+
// This would be a program bug
395397
this.reportError(`Unexpected token: ${token.type} ${token.tag} ${token.text}`);
396398
}
397399
}
@@ -474,37 +476,59 @@ export default class ApiDocumentation {
474476
}
475477

476478
protected _parseParam(tokenizer: Tokenizer): IParam {
477-
const paramDescriptionToken: Token = tokenizer.getToken();
478-
if (!paramDescriptionToken) {
479-
this.reportError('@param tag missing required description');
480-
return;
481-
}
482-
const hyphenIndex: number = paramDescriptionToken ? paramDescriptionToken.text.indexOf('-') : -1;
483-
if (hyphenIndex < 0) {
484-
this.reportError('No hyphens found in the @param line. ' +
485-
'There should be a hyphen between the parameter name and its description.');
486-
return;
487-
} else {
488-
const name: string = paramDescriptionToken.text.slice(0, hyphenIndex).trim();
489-
const comment: string = paramDescriptionToken.text.substr(hyphenIndex + 1).trim();
490-
491-
if (!comment) {
492-
this.reportError('@param tag requires a description following the hyphen');
493-
return;
494-
}
495-
496-
const commentTextElement: IDocElement = DocElementParser.makeTextElement(comment);
497-
// Full param description may contain additional Tokens (Ex: @link)
498-
const remainingElements: IDocElement[] = DocElementParser.parse(tokenizer, this.reportError);
499-
const descriptionElements: IDocElement[] = [commentTextElement].concat(remainingElements);
500-
501-
const paramDocElement: IParam = {
502-
name: name,
503-
description: descriptionElements
504-
};
505-
return paramDocElement;
506-
}
479+
const paramDescriptionToken: Token = tokenizer.getToken();
480+
if (!paramDescriptionToken) {
481+
this.reportError('@param tag missing required description');
482+
return;
507483
}
484+
const hyphenIndex: number = paramDescriptionToken ? paramDescriptionToken.text.indexOf('-') : -1;
485+
if (hyphenIndex < 0) {
486+
this.reportError('No hyphens found in the @param line. ' +
487+
'There should be a hyphen between the parameter name and its description.');
488+
return;
489+
} else {
490+
const name: string = paramDescriptionToken.text.slice(0, hyphenIndex).trim();
491+
const comment: string = paramDescriptionToken.text.substr(hyphenIndex + 1).trim();
492+
493+
if (!comment) {
494+
this.reportError('@param tag requires a description following the hyphen');
495+
return;
496+
}
497+
498+
const commentTextElement: IDocElement = DocElementParser.makeTextElement(comment);
499+
// Full param description may contain additional Tokens (Ex: @link)
500+
const remainingElements: IDocElement[] = DocElementParser.parse(tokenizer, this.reportError);
501+
const descriptionElements: IDocElement[] = [commentTextElement].concat(remainingElements);
502+
503+
const paramDocElement: IParam = {
504+
name: name,
505+
description: descriptionElements
506+
};
507+
return paramDocElement;
508+
}
509+
}
510+
511+
private _reportBadJSDocTag(token: Token): void {
512+
const supportsRegular: boolean = ApiDocumentation._allowedRegularJsdocTags.indexOf(token.tag) >= 0;
513+
const supportsInline: boolean = ApiDocumentation._allowedInlineJsdocTags.indexOf(token.tag) >= 0;
514+
515+
if (!supportsRegular && !supportsInline) {
516+
this.reportError(`Unknown JSDoc tag \"${token.tag}\"`);
517+
return;
518+
}
519+
520+
if (token.type === TokenType.Inline && !supportsInline) {
521+
this.reportError(`The JSDoc tag \"${token.tag}\" must not use the non-inline syntax (no curly braces)`);
522+
return;
523+
}
524+
if (token.type === TokenType.Tag && !supportsRegular) {
525+
this.reportError(`The JSDoc tag \"${token.tag}\" must use the inline syntax (with curly braces)`);
526+
return;
527+
}
528+
529+
this.reportError(`The JSDoc tag \"${token.tag}\" is not supported in this context`);
530+
return;
531+
}
508532

509533
private _checkInheritDocStatus(): void {
510534
if (this.isDocInherited) {

0 commit comments

Comments
 (0)