UNPKG

jsii

Version:

[![Join the chat at https://cdk.Dev](https://img.shields.io/static/v1?label=Slack&message=cdk.dev&color=brightgreen&logo=slack)](https://cdk.dev) [![All Contributors](https://img.shields.io/github/all-contributors/aws/jsii/main?label=%E2%9C%A8%20All%20Con

262 lines • 9.87 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.splitSummary = exports.getReferencedDocParams = exports.parseSymbolDocumentation = void 0; /** * Doc Comment parsing * * I tried using TSDoc here. * * Pro: * - Future standard. * - Does validating parsing and complains on failure. * - Has a more flexible interpretation of the @example tag (starts in text mode). * * Con: * - Different tags from JSDoc (@defaultValue instead of @default, "@param name * description" instead "@param name description". * - @example tag has a different interpretation than VSCode and JSDoc * (VSCode/JSDoc starts in code mode), which is confusing for syntax * highlighting in the editor. * - Allows no unknown tags. * - Wants to be in charge of parsing TypeScript, integrating into other build is * possible but harder. * - Parse to a DOM with no easy way to roundtrip back to equivalent MarkDown. * * Especially the last point: while parsing to and storing the parsed docs DOM * in the jsii assembly is superior in the long term (for example for * converting to different output formats, JavaDoc, C# docs, refdocs which all * accept slightly different syntaxes), right now we can get 80% of the bang * for 10% of the buck by just reading, storing and reproducing MarkDown, which * is Readable Enough(tm). * * If we ever want to attempt TSDoc again, this would be a good place to look at: * * https://github.com/Microsoft/tsdoc/blob/master/api-demo/src/advancedDemo.ts */ const spec = require("@jsii/spec"); const ts = require("typescript"); /** * Tags that we recognize */ var DocTag; (function (DocTag) { DocTag["PARAM"] = "param"; DocTag["DEFAULT"] = "default"; DocTag["DEFAULT_VALUE"] = "defaultValue"; DocTag["DEPRECATED"] = "deprecated"; DocTag["RETURNS"] = "returns"; DocTag["RETURN"] = "return"; DocTag["STABLE"] = "stable"; DocTag["EXPERIMENTAL"] = "experimental"; DocTag["SEE"] = "see"; DocTag["SUBCLASSABLE"] = "subclassable"; DocTag["EXAMPLE"] = "example"; DocTag["STABILITY"] = "stability"; DocTag["STRUCT"] = "struct"; // Not forwarded, this is compiler-internal. DocTag["JSII"] = "jsii"; })(DocTag || (DocTag = {})); const RECOGNIZED_TAGS = new Set(Object.values(DocTag)); /** * Parse all doc comments that apply to a symbol into JSIIDocs format */ function parseSymbolDocumentation(sym, typeChecker) { const comment = ts.displayPartsToString(sym.getDocumentationComment(typeChecker)).trim(); const tags = reabsorbExampleTags(sym.getJsDocTags()); // Right here we'll just guess that the first declaration site is the most important one. return parseDocParts(comment, tags); } exports.parseSymbolDocumentation = parseSymbolDocumentation; /** * Return the list of parameter names that are referenced in the docstring for this symbol */ function getReferencedDocParams(sym) { const ret = new Array(); for (const tag of sym.getJsDocTags()) { if (tag.name === DocTag.PARAM) { const parts = ts.displayPartsToString(tag.text).split(' '); ret.push(parts[0]); } } return ret; } exports.getReferencedDocParams = getReferencedDocParams; function parseDocParts(comments, tags) { const diagnostics = new Array(); const docs = {}; const hints = {}; [docs.summary, docs.remarks] = splitSummary(comments); const tagNames = new Map(); for (const tag of tags) { // 'param' gets parsed as a tag and as a comment for a method // 'jsii' is internal-only and shouldn't surface in the API doc if (tag.name !== DocTag.PARAM && tag.name !== DocTag.JSII) { tagNames.set(tag.name, tag.text && ts.displayPartsToString(tag.text)); } } function eatTag(...names) { for (const name of names) { if (tagNames.has(name)) { const ret = tagNames.get(name); tagNames.delete(name); return ret ?? ''; } } return undefined; } if (eatTag(DocTag.STRUCT) != null) { hints.struct = true; } docs.default = eatTag(DocTag.DEFAULT, DocTag.DEFAULT_VALUE); docs.deprecated = eatTag(DocTag.DEPRECATED); docs.example = eatTag(DocTag.EXAMPLE); docs.returns = eatTag(DocTag.RETURNS, DocTag.RETURN); docs.see = eatTag(DocTag.SEE); docs.subclassable = eatTag(DocTag.SUBCLASSABLE) !== undefined ? true : undefined; docs.stability = parseStability(eatTag(DocTag.STABILITY), diagnostics); // @experimental is a shorthand for '@stability experimental', same for '@stable' const experimental = eatTag(DocTag.EXPERIMENTAL) !== undefined; const stable = eatTag(DocTag.STABLE) !== undefined; // Can't combine them if (countBools(docs.stability !== undefined, experimental, stable) > 1) { diagnostics.push('Use only one of @stability, @experimental or @stable'); } if (experimental) { docs.stability = spec.Stability.Experimental; } if (stable) { docs.stability = spec.Stability.Stable; } // Can combine '@stability deprecated' with '@deprecated <reason>' if (docs.deprecated !== undefined) { if (docs.stability !== undefined && docs.stability !== spec.Stability.Deprecated) { diagnostics.push("@deprecated tag requires '@stability deprecated' or no @stability at all."); } docs.stability = spec.Stability.Deprecated; } if (docs.example?.includes('```')) { // This is currently what the JSDoc standard expects, and VSCode highlights it in // this way as well. TSDoc disagrees and says that examples start in text mode // which I tend to agree with, but that hasn't become a widely used standard yet. // // So we conform to existing reality. diagnostics.push('@example must be code only, no code block fences allowed.'); } if (docs.deprecated?.trim() === '') { diagnostics.push('@deprecated tag needs a reason and/or suggested alternatives.'); } if (tagNames.size > 0) { docs.custom = {}; for (const [key, value] of tagNames.entries()) { docs.custom[key] = value ?? 'true'; // Key must have a value or it will be stripped from the assembly } } return { docs, diagnostics, hints }; } /** * Split the doc comment into summary and remarks * * Normally, we'd expect people to split into a summary line and detail lines using paragraph * markers. However, a LOT of people do not do this, and just paste a giant comment block into * the docstring. If we detect that situation, we will try and extract the first sentence (using * a period) as the summary. */ function splitSummary(docBlock) { if (!docBlock) { return [undefined, undefined]; } const summary = summaryLine(docBlock); const remarks = uberTrim(docBlock.slice(summary.length)); return [endWithPeriod(noNewlines(summary.trim())), remarks]; } exports.splitSummary = splitSummary; /** * Replace newlines with spaces for use in tables */ function noNewlines(s) { return s.replace(/\r?\n/g, ' '); } function endWithPeriod(s) { return ENDS_WITH_PUNCTUATION_REGEX.test(s) ? s : `${s}.`; } /** * Trims a string and turns it into `undefined` if the result would have been an * empty string. */ function uberTrim(str) { str = str.trim(); return str === '' ? undefined : str; } const SUMMARY_MAX_WORDS = 20; /** * Find the summary line for a doc comment * * In principle we'll take the first paragraph, but if there are no paragraphs * (because people don't put in paragraph breaks) or the first paragraph is too * long, we'll take the first sentence (terminated by a punctuation). */ function summaryLine(str) { const paras = str.split('\n\n'); if (paras.length > 1 && paras[0].split(' ').length < SUMMARY_MAX_WORDS) { return paras[0]; } const m = FIRST_SENTENCE_REGEX.exec(str); if (m) { return m[1]; } return paras[0]; } const PUNCTUATION = ['!', '?', '.', ';'].map((s) => `\\${s}`).join(''); const ENDS_WITH_PUNCTUATION_REGEX = new RegExp(`[${PUNCTUATION}]$`); const FIRST_SENTENCE_REGEX = new RegExp(`^([^${PUNCTUATION}]+[${PUNCTUATION}][ \n\r])`); // Needs a whitespace after the punctuation. function intBool(x) { return x ? 1 : 0; } function countBools(...x) { return x.map(intBool).reduce((a, b) => a + b, 0); } function parseStability(s, diagnostics) { if (s === undefined) { return undefined; } switch (s) { case 'stable': return spec.Stability.Stable; case 'experimental': return spec.Stability.Experimental; case 'external': return spec.Stability.External; case 'deprecated': return spec.Stability.Deprecated; } diagnostics.push(`Unrecognized @stability: '${s}'`); return undefined; } /** * Unrecognized tags that follow an '@ example' tag will be absorbed back into the example value * * The TypeScript parser by itself is naive and will start parsing a new tag there. * * We do this until we encounter a supported @ keyword. */ function reabsorbExampleTags(tags) { var _a; const ret = [...tags]; let i = 0; while (i < ret.length) { if (ret[i].name === 'example') { while (i + 1 < ret.length && !RECOGNIZED_TAGS.has(ret[i + 1].name)) { // Incorrectly classified as @tag, absorb back into example (_a = ret[i]).text ?? (_a.text = []); ret[i].text.push({ text: `@${ret[i + 1].name}${ret[i + 1].text}`, kind: '', }); ret.splice(i + 1, 1); } } i++; } return ret; } //# sourceMappingURL=docs.js.map