Skip to content

Commit

Permalink
Add jsDocCompatibility option
Browse files Browse the repository at this point in the history
Resolves #2219
  • Loading branch information
Gerrit0 committed Apr 7, 2023
1 parent 14c325b commit 864db57
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -42,6 +42,7 @@
### Features

- Added `--useTsLinkResolution` option (on by default) which tells TypeDoc to use TypeScript's `@link` resolution.
- Added `--jsDocCompatibility` option (on by default) which controls TypeDoc's automatic detection of code blocks in `@example` and `@default` tags.
- Reworked default theme navigation to add support for a page table of contents, #1478, #2189.
- Added support for `@interface` on type aliases to tell TypeDoc to convert the fully resolved type as an interface, #1519
- Added support for `@namespace` on variable declarations to tell TypeDoc to convert the variable as a namespace, #2055.
Expand Down
6 changes: 5 additions & 1 deletion src/lib/converter/comments/index.ts
@@ -1,7 +1,10 @@
import ts from "typescript";
import { Comment, ReflectionKind } from "../../models";
import { assertNever, Logger } from "../../utils";
import type { CommentStyle } from "../../utils/options/declaration";
import type {
CommentStyle,
JsDocCompatibility,
} from "../../utils/options/declaration";
import { lexBlockComment } from "./blockLexer";
import {
DiscoveredComment,
Expand All @@ -15,6 +18,7 @@ export interface CommentParserConfig {
blockTags: Set<string>;
inlineTags: Set<string>;
modifierTags: Set<string>;
jsDocCompatibility: JsDocCompatibility;
}

const jsDocCommentKinds = [
Expand Down
43 changes: 41 additions & 2 deletions src/lib/converter/comments/parser.ts
Expand Up @@ -205,18 +205,57 @@ function blockTag(
const tagName = aliasedTags.get(blockTag.text) || blockTag.text;

let content: CommentDisplayPart[];
if (tagName === "@example") {
if (tagName === "@example" && config.jsDocCompatibility.exampleTag) {
content = exampleBlockContent(comment, lexer, config, warning);
} else if (tagName === "@default" && config.jsDocCompatibility.defaultTag) {
content = defaultBlockContent(comment, lexer, config, warning);
} else {
content = blockContent(comment, lexer, config, warning);
}

return new CommentTag(tagName as `@${string}`, content);
}

/**
* The `@default` tag gets a special case because otherwise we will produce many warnings
* about unescaped/mismatched/missing braces in legacy JSDoc comments
*/
function defaultBlockContent(
comment: Comment,
lexer: LookaheadGenerator<Token>,
config: CommentParserConfig,
warning: (msg: string, token: Token) => void
): CommentDisplayPart[] {
lexer.mark();
const content = blockContent(comment, lexer, config, () => {});
const end = lexer.done() || lexer.peek();
lexer.release();

if (content.some((part) => part.kind === "code")) {
return blockContent(comment, lexer, config, warning);
}

const tokens: Token[] = [];
while ((lexer.done() || lexer.peek()) !== end) {
tokens.push(lexer.take());
}

const blockText = tokens
.map((tok) => tok.text)
.join("")
.trim();

return [
{
kind: "code",
text: makeCodeBlock(blockText),
},
];
}

/**
* The `@example` tag gets a special case because otherwise we will produce many warnings
* about unescaped/mismatched/missing braces
* about unescaped/mismatched/missing braces in legacy JSDoc comments.
*/
function exampleBlockContent(
comment: Comment,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/converter/converter.ts
Expand Up @@ -229,6 +229,7 @@ export class Converter extends ChildableComponent<
this.resolve(context);

this.trigger(Converter.EVENT_END, context);
this._config = undefined;

return project;
}
Expand Down Expand Up @@ -493,6 +494,8 @@ export class Converter extends ChildableComponent<
modifierTags: new Set(
this.application.options.getValue("modifierTags")
),
jsDocCompatibility:
this.application.options.getValue("jsDocCompatibility"),
};
return this._config;
}
Expand Down
14 changes: 14 additions & 0 deletions src/lib/utils/options/declaration.ts
Expand Up @@ -140,6 +140,7 @@ export interface TypeDocOptionMap {
navigationLinks: ManuallyValidatedOption<Record<string, string>>;
sidebarLinks: ManuallyValidatedOption<Record<string, string>>;

jsDocCompatibility: JsDocCompatibility;
commentStyle: typeof CommentStyle;
useTsLinkResolution: boolean;
blockTags: `@${string}`[];
Expand Down Expand Up @@ -200,6 +201,19 @@ export type ValidationOptions = {
notDocumented: boolean;
};

export type JsDocCompatibility = {
/**
* If set, TypeDoc will treat `@example` blocks as code unless they contain a code block.
* On by default, this is how VSCode renders blocks.
*/
exampleTag: boolean;
/**
* If set, TypeDoc will treat `@default` blocks as code unless they contain a code block.
* On by default, this is how VSCode renders blocks.
*/
defaultTag: boolean;
};

/**
* Converts a given TypeDoc option key to the type of the declaration expected.
*/
Expand Down
6 changes: 3 additions & 3 deletions src/lib/utils/options/options.ts
Expand Up @@ -60,7 +60,7 @@ export interface OptionsReader {
const optionSnapshots = new WeakMap<
{ __optionSnapshot: never },
{
values: Record<string, unknown>;
values: string;
set: Set<string>;
}
>();
Expand Down Expand Up @@ -136,7 +136,7 @@ export class Options {
const key = {} as { __optionSnapshot: never };

optionSnapshots.set(key, {
values: { ...this._values },
values: JSON.stringify(this._values),
set: new Set(this._setOptions),
});

Expand All @@ -149,7 +149,7 @@ export class Options {
*/
restore(snapshot: { __optionSnapshot: never }) {
const data = optionSnapshots.get(snapshot)!;
this._values = { ...data.values };
this._values = JSON.parse(data.values);
this._setOptions = new Set(data.set);
}

Expand Down
10 changes: 10 additions & 0 deletions src/lib/utils/options/sources/typedoc.ts
Expand Up @@ -431,6 +431,16 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
///// Comment Options /////
///////////////////////////

options.addDeclaration({
name: "jsDocCompatibility",
help: "Sets compatibility options for comment parsing that increase similarity with JSDoc comments.",
type: ParameterType.Flags,
defaults: {
defaultTag: true,
exampleTag: true,
},
});

options.addDeclaration({
name: "commentStyle",
help: "Determines how TypeDoc searches for comments.",
Expand Down
66 changes: 61 additions & 5 deletions src/test/behavior.c2.test.ts
Expand Up @@ -76,7 +76,7 @@ const base = getConverter2Base();
const app = getConverter2App();
const program = getConverter2Program();

function doConvert(entry: string) {
function convert(entry: string) {
const entryPoint = [
join(base, `behavior/${entry}.ts`),
join(base, `behavior/${entry}.d.ts`),
Expand All @@ -91,6 +91,7 @@ function doConvert(entry: string) {
ok(sourceFile, `No source file found for ${entryPoint}`);

app.options.setValue("entryPoints", [entryPoint]);
clearCommentCache();
return app.converter.convert([
{
displayName: entry,
Expand All @@ -102,14 +103,11 @@ function doConvert(entry: string) {

describe("Behavior Tests", () => {
let logger: TestLogger;
let convert: (name: string) => ProjectReflection;
let optionsSnap: { __optionSnapshot: never };

beforeEach(() => {
app.logger = logger = new TestLogger();
optionsSnap = app.options.snapshot();
clearCommentCache();
convert = (name) => doConvert(name);
});

afterEach(() => {
Expand Down Expand Up @@ -261,7 +259,35 @@ describe("Behavior Tests", () => {
]);
});

it("Handles example tags", () => {
it("Handles @default tags with JSDoc compat turned on", () => {
const project = convert("defaultTag");
const foo = query(project, "foo");
const tags = foo.comment?.blockTags.map((tag) => tag.content);

equal(tags, [
[{ kind: "code", text: "```ts\n\n```" }],
[{ kind: "code", text: "```ts\nfn({})\n```" }],
]);

logger.expectNoOtherMessages();
});

it("Handles @default tags with JSDoc compat turned off", () => {
app.options.setValue("jsDocCompatibility", false);
const project = convert("defaultTag");
const foo = query(project, "foo");
const tags = foo.comment?.blockTags.map((tag) => tag.content);

equal(tags, [[], [{ kind: "text", text: "fn({})" }]]);

logger.expectMessage(
"warn: Encountered an unescaped open brace without an inline tag"
);
logger.expectMessage("warn: Unmatched closing brace");
logger.expectNoOtherMessages();
});

it("Handles @example tags with JSDoc compat turned on", () => {
const project = convert("exampleTags");
const foo = query(project, "foo");
const tags = foo.comment?.blockTags.map((tag) => tag.content);
Expand All @@ -288,6 +314,36 @@ describe("Behavior Tests", () => {
logger.expectNoOtherMessages();
});

it("Warns about example tags containing braces when compat options are off", () => {
app.options.setValue("jsDocCompatibility", false);
const project = convert("exampleTags");
const foo = query(project, "foo");
const tags = foo.comment?.blockTags.map((tag) => tag.content);

equal(tags, [
[{ kind: "text", text: "// JSDoc style\ncodeHere();" }],
[
{
kind: "text",
text: "<caption>JSDoc specialness</caption>\n// JSDoc style\ncodeHere();",
},
],
[
{
kind: "text",
text: "<caption>JSDoc with braces</caption>\nx.map(() => { return 1; })",
},
],
[{ kind: "code", text: "```ts\n// TSDoc style\ncodeHere();\n```" }],
]);

logger.expectMessage(
"warn: Encountered an unescaped open brace without an inline tag"
);
logger.expectMessage("warn: Unmatched closing brace");
logger.expectNoOtherMessages();
});

it("Handles excludeNotDocumentedKinds", () => {
app.options.setValue("excludeNotDocumented", true);
app.options.setValue("excludeNotDocumentedKinds", ["Property"]);
Expand Down
1 change: 1 addition & 0 deletions src/test/comments.test.ts
Expand Up @@ -1212,6 +1212,7 @@ describe("Comment Parser", () => {
"@event",
"@packageDocumentation",
]),
jsDocCompatibility: { defaultTag: true, exampleTag: true },
};

it("Should rewrite @inheritdoc to @inheritDoc", () => {
Expand Down
5 changes: 5 additions & 0 deletions src/test/converter2/behavior/defaultTag.ts
@@ -0,0 +1,5 @@
/**
* @default
* @default fn({})
*/
export const foo = 1;
22 changes: 15 additions & 7 deletions src/test/issues.c2.test.ts
Expand Up @@ -47,6 +47,7 @@ function doConvert(entry: string) {
ok(sourceFile, `No source file found for ${entryPoint}`);

app.options.setValue("entryPoints", [entryPoint]);
clearCommentCache();
return app.converter.convert([
{
displayName: entry,
Expand All @@ -70,7 +71,6 @@ describe("Issue Tests", () => {
beforeEach(function () {
app.logger = logger = new TestLogger();
optionsSnap = app.options.snapshot();
clearCommentCache();
const issueNumber = this.currentTest?.title.match(/#(\d+)/)?.[1];
ok(issueNumber, "Test name must contain an issue number.");
convert = (name = `gh${issueNumber}`) => doConvert(name);
Expand Down Expand Up @@ -678,12 +678,20 @@ describe("Issue Tests", () => {

it("#1967", () => {
const project = convert();
equal(query(project, "abc").comment?.getTag("@example")?.content, [
{
kind: "code",
text: "```ts\n\n```",
},
]);
equal(
query(project, "abc").comment,
new Comment(
[],
[
new CommentTag("@example", [
{
kind: "code",
text: "```ts\n\n```",
},
]),
]
)
);
});

it("#1968", () => {
Expand Down

0 comments on commit 864db57

Please sign in to comment.