UNPKG

9.87 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.splitSummary = exports.getReferencedDocParams = exports.parseSymbolDocumentation = void 0;
4/**
5 * Doc Comment parsing
6 *
7 * I tried using TSDoc here.
8 *
9 * Pro:
10 * - Future standard.
11 * - Does validating parsing and complains on failure.
12 * - Has a more flexible interpretation of the @example tag (starts in text mode).
13 *
14 * Con:
15 * - Different tags from JSDoc (@defaultValue instead of @default, "@param name
16 * description" instead "@param name description".
17 * - @example tag has a different interpretation than VSCode and JSDoc
18 * (VSCode/JSDoc starts in code mode), which is confusing for syntax
19 * highlighting in the editor.
20 * - Allows no unknown tags.
21 * - Wants to be in charge of parsing TypeScript, integrating into other build is
22 * possible but harder.
23 * - Parse to a DOM with no easy way to roundtrip back to equivalent MarkDown.
24 *
25 * Especially the last point: while parsing to and storing the parsed docs DOM
26 * in the jsii assembly is superior in the long term (for example for
27 * converting to different output formats, JavaDoc, C# docs, refdocs which all
28 * accept slightly different syntaxes), right now we can get 80% of the bang
29 * for 10% of the buck by just reading, storing and reproducing MarkDown, which
30 * is Readable Enough(tm).
31 *
32 * If we ever want to attempt TSDoc again, this would be a good place to look at:
33 *
34 * https://github.com/Microsoft/tsdoc/blob/master/api-demo/src/advancedDemo.ts
35 */
36const spec = require("@jsii/spec");
37const ts = require("typescript");
38/**
39 * Tags that we recognize
40 */
41var DocTag;
42(function (DocTag) {
43 DocTag["PARAM"] = "param";
44 DocTag["DEFAULT"] = "default";
45 DocTag["DEFAULT_VALUE"] = "defaultValue";
46 DocTag["DEPRECATED"] = "deprecated";
47 DocTag["RETURNS"] = "returns";
48 DocTag["RETURN"] = "return";
49 DocTag["STABLE"] = "stable";
50 DocTag["EXPERIMENTAL"] = "experimental";
51 DocTag["SEE"] = "see";
52 DocTag["SUBCLASSABLE"] = "subclassable";
53 DocTag["EXAMPLE"] = "example";
54 DocTag["STABILITY"] = "stability";
55 DocTag["STRUCT"] = "struct";
56 // Not forwarded, this is compiler-internal.
57 DocTag["JSII"] = "jsii";
58})(DocTag || (DocTag = {}));
59const RECOGNIZED_TAGS = new Set(Object.values(DocTag));
60/**
61 * Parse all doc comments that apply to a symbol into JSIIDocs format
62 */
63function parseSymbolDocumentation(sym, typeChecker) {
64 const comment = ts.displayPartsToString(sym.getDocumentationComment(typeChecker)).trim();
65 const tags = reabsorbExampleTags(sym.getJsDocTags());
66 // Right here we'll just guess that the first declaration site is the most important one.
67 return parseDocParts(comment, tags);
68}
69exports.parseSymbolDocumentation = parseSymbolDocumentation;
70/**
71 * Return the list of parameter names that are referenced in the docstring for this symbol
72 */
73function getReferencedDocParams(sym) {
74 const ret = new Array();
75 for (const tag of sym.getJsDocTags()) {
76 if (tag.name === DocTag.PARAM) {
77 const parts = ts.displayPartsToString(tag.text).split(' ');
78 ret.push(parts[0]);
79 }
80 }
81 return ret;
82}
83exports.getReferencedDocParams = getReferencedDocParams;
84function parseDocParts(comments, tags) {
85 const diagnostics = new Array();
86 const docs = {};
87 const hints = {};
88 [docs.summary, docs.remarks] = splitSummary(comments);
89 const tagNames = new Map();
90 for (const tag of tags) {
91 // 'param' gets parsed as a tag and as a comment for a method
92 // 'jsii' is internal-only and shouldn't surface in the API doc
93 if (tag.name !== DocTag.PARAM && tag.name !== DocTag.JSII) {
94 tagNames.set(tag.name, tag.text && ts.displayPartsToString(tag.text));
95 }
96 }
97 function eatTag(...names) {
98 for (const name of names) {
99 if (tagNames.has(name)) {
100 const ret = tagNames.get(name);
101 tagNames.delete(name);
102 return ret ?? '';
103 }
104 }
105 return undefined;
106 }
107 if (eatTag(DocTag.STRUCT) != null) {
108 hints.struct = true;
109 }
110 docs.default = eatTag(DocTag.DEFAULT, DocTag.DEFAULT_VALUE);
111 docs.deprecated = eatTag(DocTag.DEPRECATED);
112 docs.example = eatTag(DocTag.EXAMPLE);
113 docs.returns = eatTag(DocTag.RETURNS, DocTag.RETURN);
114 docs.see = eatTag(DocTag.SEE);
115 docs.subclassable = eatTag(DocTag.SUBCLASSABLE) !== undefined ? true : undefined;
116 docs.stability = parseStability(eatTag(DocTag.STABILITY), diagnostics);
117 // @experimental is a shorthand for '@stability experimental', same for '@stable'
118 const experimental = eatTag(DocTag.EXPERIMENTAL) !== undefined;
119 const stable = eatTag(DocTag.STABLE) !== undefined;
120 // Can't combine them
121 if (countBools(docs.stability !== undefined, experimental, stable) > 1) {
122 diagnostics.push('Use only one of @stability, @experimental or @stable');
123 }
124 if (experimental) {
125 docs.stability = spec.Stability.Experimental;
126 }
127 if (stable) {
128 docs.stability = spec.Stability.Stable;
129 }
130 // Can combine '@stability deprecated' with '@deprecated <reason>'
131 if (docs.deprecated !== undefined) {
132 if (docs.stability !== undefined && docs.stability !== spec.Stability.Deprecated) {
133 diagnostics.push("@deprecated tag requires '@stability deprecated' or no @stability at all.");
134 }
135 docs.stability = spec.Stability.Deprecated;
136 }
137 if (docs.example?.includes('```')) {
138 // This is currently what the JSDoc standard expects, and VSCode highlights it in
139 // this way as well. TSDoc disagrees and says that examples start in text mode
140 // which I tend to agree with, but that hasn't become a widely used standard yet.
141 //
142 // So we conform to existing reality.
143 diagnostics.push('@example must be code only, no code block fences allowed.');
144 }
145 if (docs.deprecated?.trim() === '') {
146 diagnostics.push('@deprecated tag needs a reason and/or suggested alternatives.');
147 }
148 if (tagNames.size > 0) {
149 docs.custom = {};
150 for (const [key, value] of tagNames.entries()) {
151 docs.custom[key] = value ?? 'true'; // Key must have a value or it will be stripped from the assembly
152 }
153 }
154 return { docs, diagnostics, hints };
155}
156/**
157 * Split the doc comment into summary and remarks
158 *
159 * Normally, we'd expect people to split into a summary line and detail lines using paragraph
160 * markers. However, a LOT of people do not do this, and just paste a giant comment block into
161 * the docstring. If we detect that situation, we will try and extract the first sentence (using
162 * a period) as the summary.
163 */
164function splitSummary(docBlock) {
165 if (!docBlock) {
166 return [undefined, undefined];
167 }
168 const summary = summaryLine(docBlock);
169 const remarks = uberTrim(docBlock.slice(summary.length));
170 return [endWithPeriod(noNewlines(summary.trim())), remarks];
171}
172exports.splitSummary = splitSummary;
173/**
174 * Replace newlines with spaces for use in tables
175 */
176function noNewlines(s) {
177 return s.replace(/\r?\n/g, ' ');
178}
179function endWithPeriod(s) {
180 return ENDS_WITH_PUNCTUATION_REGEX.test(s) ? s : `${s}.`;
181}
182/**
183 * Trims a string and turns it into `undefined` if the result would have been an
184 * empty string.
185 */
186function uberTrim(str) {
187 str = str.trim();
188 return str === '' ? undefined : str;
189}
190const SUMMARY_MAX_WORDS = 20;
191/**
192 * Find the summary line for a doc comment
193 *
194 * In principle we'll take the first paragraph, but if there are no paragraphs
195 * (because people don't put in paragraph breaks) or the first paragraph is too
196 * long, we'll take the first sentence (terminated by a punctuation).
197 */
198function summaryLine(str) {
199 const paras = str.split('\n\n');
200 if (paras.length > 1 && paras[0].split(' ').length < SUMMARY_MAX_WORDS) {
201 return paras[0];
202 }
203 const m = FIRST_SENTENCE_REGEX.exec(str);
204 if (m) {
205 return m[1];
206 }
207 return paras[0];
208}
209const PUNCTUATION = ['!', '?', '.', ';'].map((s) => `\\${s}`).join('');
210const ENDS_WITH_PUNCTUATION_REGEX = new RegExp(`[${PUNCTUATION}]$`);
211const FIRST_SENTENCE_REGEX = new RegExp(`^([^${PUNCTUATION}]+[${PUNCTUATION}][ \n\r])`); // Needs a whitespace after the punctuation.
212function intBool(x) {
213 return x ? 1 : 0;
214}
215function countBools(...x) {
216 return x.map(intBool).reduce((a, b) => a + b, 0);
217}
218function parseStability(s, diagnostics) {
219 if (s === undefined) {
220 return undefined;
221 }
222 switch (s) {
223 case 'stable':
224 return spec.Stability.Stable;
225 case 'experimental':
226 return spec.Stability.Experimental;
227 case 'external':
228 return spec.Stability.External;
229 case 'deprecated':
230 return spec.Stability.Deprecated;
231 }
232 diagnostics.push(`Unrecognized @stability: '${s}'`);
233 return undefined;
234}
235/**
236 * Unrecognized tags that follow an '@ example' tag will be absorbed back into the example value
237 *
238 * The TypeScript parser by itself is naive and will start parsing a new tag there.
239 *
240 * We do this until we encounter a supported @ keyword.
241 */
242function reabsorbExampleTags(tags) {
243 var _a;
244 const ret = [...tags];
245 let i = 0;
246 while (i < ret.length) {
247 if (ret[i].name === 'example') {
248 while (i + 1 < ret.length && !RECOGNIZED_TAGS.has(ret[i + 1].name)) {
249 // Incorrectly classified as @tag, absorb back into example
250 (_a = ret[i]).text ?? (_a.text = []);
251 ret[i].text.push({
252 text: `@${ret[i + 1].name}${ret[i + 1].text}`,
253 kind: '',
254 });
255 ret.splice(i + 1, 1);
256 }
257 }
258 i++;
259 }
260 return ret;
261}
262//# sourceMappingURL=docs.js.map
\No newline at end of file