Skip to content

Commit

Permalink
feat: #764 - Prefer writing single line JS docs. Add JSDoc#isMultiLin…
Browse files Browse the repository at this point in the history
…e().

BREAKING CHANGE: JS docs will be written as a single line unless multi-line or starting with a newline. Additionally, getting a JS doc structure will have a newline at the start if the JS doc description is one line, but the JS doc is multi-line.
  • Loading branch information
dsherret committed Nov 25, 2019
1 parent b69750c commit 59c8d38
Show file tree
Hide file tree
Showing 27 changed files with 191 additions and 67 deletions.
6 changes: 6 additions & 0 deletions docs/details/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ Add or insert JS doc comments using the `addJsDoc()`, `addJsDocs()`, `insertJsDo
For example:

```ts
// adds /** Some description... */
const docNode = classDeclaration.addJsDoc({
description: "Some description..."
});

// or to force it to be multi-line, add a newline to the front of the string
classDeclaration.addJsDoc({
description: "\nSome description..."
});
```

Right now you can only add a description, but in the future support will be added for easily manipulating more JS doc syntax.
Expand Down
8 changes: 8 additions & 0 deletions packages/ts-morph/lib/ts-morph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5224,6 +5224,10 @@ declare const JSDocBase: typeof Node;
* JS doc node.
*/
export declare class JSDoc extends JSDocBase<ts.JSDoc> {
/**
* Gets if this JS doc spans multiple lines.
*/
isMultiLine(): boolean;
/**
* Gets the tags of the JSDoc.
*/
Expand Down Expand Up @@ -11491,6 +11495,10 @@ export interface JSDocStructure extends Structure, JSDocSpecificStructure {
}

interface JSDocSpecificStructure extends KindedStructure<StructureKind.JSDoc> {
/**
* The description of the JS doc.
* @remarks To force this to be multi-line, add a newline to the front of the string.
*/
description: string | WriterFunction;
}

Expand Down
20 changes: 19 additions & 1 deletion packages/ts-morph/src/compiler/ast/doc/JSDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export const JSDocBase = Node;
* JS doc node.
*/
export class JSDoc extends JSDocBase<ts.JSDoc> {
/**
* Gets if this JS doc spans multiple lines.
*/
isMultiLine() {
return this.getText().includes("\n");
}

/**
* Gets the tags of the JSDoc.
*/
Expand Down Expand Up @@ -111,7 +118,18 @@ export class JSDoc extends JSDocBase<ts.JSDoc> {
getStructure(): JSDocStructure {
return callBaseGetStructure<JSDocSpecificStructure>(JSDocBase.prototype, this, {
kind: StructureKind.JSDoc,
description: this.getInnerText() // good enough for now
description: getDescription.call(this)
});

function getDescription(this: JSDoc) {
const description = this.getInnerText(); // good enough for now

// If the jsdoc text is like /**\n * Some description.\n*/ then force its
// description to start with a newline so that ts-morph will later make this multi-line.
if (this.isMultiLine() && !description.includes("\n"))
return this._context.manipulationSettings.getNewLineKindAsString() + description;
else
return description;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,39 @@ export class JSDocStructurePrinter extends NodePrinter<OptionalKind<JSDocStructu
}

protected printTextInternal(writer: CodeBlockWriter, structure: OptionalKind<JSDocStructure> | string | WriterFunction) {
const lines = getText.call(this).split(/\r?\n/);
writer.writeLine("/**");
for (const line of lines) {
writer.write(` *`);
if (line.length > 0)
writer.write(` ${line}`);
const text = getText(this);
const lines = text.split(/\r?\n/);
const startsWithNewLine = lines[0].length === 0;
const isSingleLine = lines.length <= 1;
const startIndex = startsWithNewLine ? 1 : 0;

writer.write("/**");

if (isSingleLine)
writer.space();
else
writer.newLine();

if (isSingleLine)
writer.write(lines[startIndex]);
else {
for (let i = startIndex; i < lines.length; i++) {
writer.write(` *`);

if (lines[i].length > 0)
writer.write(` ${lines[i]}`);

writer.newLine();
}
}
writer.write(" */");

function getText(this: JSDocStructurePrinter) {
writer.spaceIfLastNot();
writer.write("*/");

function getText(jsdocPrinter: JSDocStructurePrinter) {
if (typeof structure === "string")
return structure;
const tempWriter = this.getNewWriter(writer);
const tempWriter = jsdocPrinter.getNewWriter(writer);
if (typeof structure === "function") {
structure(tempWriter);
return tempWriter.toString();
Expand Down
4 changes: 4 additions & 0 deletions packages/ts-morph/src/structures/doc/JSDocStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ export interface JSDocStructure extends Structure, JSDocSpecificStructure {
}

export interface JSDocSpecificStructure extends KindedStructure<StructureKind.JSDoc> {
/**
* The description of the JS doc.
* @remarks To force this to be multi-line, add a newline to the front of the string.
*/
description: string | WriterFunction;
}
42 changes: 28 additions & 14 deletions packages/ts-morph/src/tests/compiler/ast/base/jsDocableNodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,35 @@ describe(nameof(JSDocableNode), () => {
expect(sourceFile.getFullText()).to.equal(expectedCode);
}

it("should insert when none exist", () => {
doTest("function identifier() {}", 0, [{ description: "Description" }], "/**\n * Description\n */\nfunction identifier() {}");
it("should insert when none exist as single line", () => {
doTest("function identifier() {}", 0, [{ description: "Description" }], "/** Description */\nfunction identifier() {}");
});

it("should insert when none exist as multi-line line when adding a newline at the start", () => {
doTest("function identifier() {}", 0, [{ description: "\nDescription" }], "/**\n * Description\n */\nfunction identifier() {}");
});

it("should insert when none exist as multi-line line when adding a \\r\\n newline at the start", () => {
doTest("function identifier() {}", 0, [{ description: "\r\nDescription" }], "/**\n * Description\n */\nfunction identifier() {}");
});

it("should insert when none exist as multi-line line when multiple lines", () => {
doTest("function identifier() {}", 0, [{ description: "Description\nOther" }],
"/**\n * Description\n * Other\n */\nfunction identifier() {}");
});

it("should insert at start when indentation is different", () => {
doTest("namespace N {\n /**\n * Desc2\n */\n function identifier() {}\n}", 0, [{ description: "Desc1" }],
doTest("namespace N {\n /**\n * Desc2\n */\n function identifier() {}\n}", 0, [{ description: "\nDesc1" }],
"namespace N {\n /**\n * Desc1\n */\n /**\n * Desc2\n */\n function identifier() {}\n}");
});

it("should insert multiple at end", () => {
doTest("/**\n * Desc1\n */\nfunction identifier() {}", 1, [{ description: writer => writer.write("Desc2") }, "Desc3"],
"/**\n * Desc1\n */\n/**\n * Desc2\n */\n/**\n * Desc3\n */\nfunction identifier() {}");
doTest("/**\n * Desc1\n */\nfunction identifier() {}", 1, [{ description: writer => writer.newLine().write("Desc2") }, "Desc3"],
"/**\n * Desc1\n */\n/**\n * Desc2\n */\n/** Desc3 */\nfunction identifier() {}");
});

it("should insert in the middle", () => {
doTest("/**\n * Desc1\n */\n/**\n * Desc3\n */\nfunction identifier() {}", 1, [writer => writer.write("Desc2")],
doTest("/**\n * Desc1\n */\n/**\n * Desc3\n */\nfunction identifier() {}", 1, [writer => writer.write("\nDesc2")],
"/**\n * Desc1\n */\n/**\n * Desc2\n */\n/**\n * Desc3\n */\nfunction identifier() {}");
});

Expand All @@ -95,7 +108,7 @@ describe(nameof(JSDocableNode), () => {

describe("PropertyDeclaration", () => {
it("should add a js doc to a property declaration", () => {
doTest("class C {\n prop;\n}", 0, [{ description: "Testing" }], "class C {\n /**\n * Testing\n */\n prop;\n}",
doTest("class C {\n prop;\n}", 0, [{ description: "\nTesting" }], "class C {\n /**\n * Testing\n */\n prop;\n}",
SyntaxKind.PropertyDeclaration);
});
});
Expand All @@ -111,7 +124,7 @@ describe(nameof(JSDocableNode), () => {

it("should insert", () => {
doTest("/**\n * Desc2\n */\nfunction identifier() {}", 0, { description: "Desc1" },
"/**\n * Desc1\n */\n/**\n * Desc2\n */\nfunction identifier() {}");
"/** Desc1 */\n/**\n * Desc2\n */\nfunction identifier() {}");
});
});

Expand All @@ -124,8 +137,8 @@ describe(nameof(JSDocableNode), () => {
}

it("should add multiple at end", () => {
doTest("/**\n * Desc1\n */\nfunction identifier() {}", [{ description: "Desc2" }, "Desc3"],
"/**\n * Desc1\n */\n/**\n * Desc2\n */\n/**\n * Desc3\n */\nfunction identifier() {}");
doTest("/**\n * Desc1\n */\nfunction identifier() {}", [{ description: "Desc2" }, "\nDesc3"],
"/**\n * Desc1\n */\n/** Desc2 */\n/**\n * Desc3\n */\nfunction identifier() {}");
});
});

Expand All @@ -138,7 +151,7 @@ describe(nameof(JSDocableNode), () => {
}

it("should add at the end", () => {
doTest("/**\n * Desc1\n */\nfunction identifier() {}", { description: "Desc2" },
doTest("/**\n * Desc1\n */\nfunction identifier() {}", { description: "\nDesc2" },
"/**\n * Desc1\n */\n/**\n * Desc2\n */\nfunction identifier() {}");
});
});
Expand All @@ -151,15 +164,15 @@ describe(nameof(JSDocableNode), () => {
}

it("should modify when setting", () => {
doTest("class Identifier {}", { docs: [{ description: "Desc1" }, "Desc2"] }, "/**\n * Desc1\n */\n/**\n * Desc2\n */\nclass Identifier {}");
doTest("class Identifier {}", { docs: [{ description: "Desc1" }, "\nDesc2"] }, "/** Desc1 */\n/**\n * Desc2\n */\nclass Identifier {}");
});

it("should not modify anything if the structure doesn't specify a value", () => {
doTest("/** Test */\nclass Identifier {}", {}, "/** Test */\nclass Identifier {}");
});

it("should replace existing", () => {
doTest("/** Test */\nclass Identifier {}", { docs: [{ description: "New" }] }, "/**\n * New\n */\nclass Identifier {}");
doTest("/** Test */\nclass Identifier {}", { docs: [{ description: "New" }] }, "/** New */\nclass Identifier {}");
});

it("should remove existing when structure specifies a value", () => {
Expand All @@ -178,7 +191,8 @@ describe(nameof(JSDocableNode), () => {
});

it("should get the js docs when they exist", () => {
doTest("/** Test *//** Test2 */class Identifier {}", ["Test", "Test2"]);
// js docs that are multi-line, but a single line will have a newline at the start
doTest("/** Test *//**\n * Test2\n */class Identifier {}", ["Test", "\nTest2"]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe(nameof(TypeElementMemberedNode), () => {
returnType: "T",
typeParameters: [{ name: "T" }]
};
doTest("interface i {\n}", 0, [structure], "interface i {\n /**\n * Test\n */\n new<T>(param): T;\n}");
doTest("interface i {\n}", 0, [structure], "interface i {\n /** Test */\n new<T>(param): T;\n}");
});
});

Expand Down Expand Up @@ -260,7 +260,7 @@ describe(nameof(TypeElementMemberedNode), () => {
keyName: "keyName",
keyType: "number"
};
doTest("interface i {\n}", 0, [structure], "interface i {\n /**\n * Test\n */\n readonly [keyName: number]: string;\n}");
doTest("interface i {\n}", 0, [structure], "interface i {\n /** Test */\n readonly [keyName: number]: string;\n}");
});
});

Expand Down Expand Up @@ -375,7 +375,7 @@ describe(nameof(TypeElementMemberedNode), () => {
returnType: "T",
typeParameters: [{ name: "T" }]
};
doTest("interface i {\n}", 0, [structure], "interface i {\n /**\n * Test\n */\n <T>(param): T;\n}");
doTest("interface i {\n}", 0, [structure], "interface i {\n /** Test */\n <T>(param): T;\n}");
});
});

Expand Down Expand Up @@ -494,7 +494,7 @@ describe(nameof(TypeElementMemberedNode), () => {
parameters: [{ name: "param" }],
typeParameters: [{ name: "T" }]
};
doTest("interface i {\n}", 0, [structure], "interface i {\n /**\n * Test\n */\n method?<T>(param): number;\n}");
doTest("interface i {\n}", 0, [structure], "interface i {\n /** Test */\n method?<T>(param): number;\n}");
});
});

Expand Down Expand Up @@ -620,7 +620,7 @@ describe(nameof(TypeElementMemberedNode), () => {
type: "number",
initializer: "5" // doesn't make sense
};
doTest("interface i {\n}", 0, [structure], "interface i {\n /**\n * Test\n */\n readonly prop?: number = 5;\n}");
doTest("interface i {\n}", 0, [structure], "interface i {\n /** Test */\n readonly prop?: number = 5;\n}");
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ describe(nameof(ClassLikeDeclarationBase), () => {
}, "console.log('here');"]
};
doTest("class c {\n}", 0, structure,
"class c {\n public constructor();\n private constructor();\n /**\n * Test\n */\n public constructor<T>(param) {\n"
"class c {\n public constructor();\n private constructor();\n /** Test */\n public constructor<T>(param) {\n"
+ " type T = string;\n\n interface I {\n }\n\n enum E {\n }\n\n"
+ " function F() {\n }\n\n class C {\n }\n\n namespace N {\n }\n\n"
+ " console.log('here');\n"
Expand Down Expand Up @@ -401,7 +401,7 @@ describe(nameof(ClassLikeDeclarationBase), () => {
typeParameters: [{ name: "T" }],
statements: [{ kind: StructureKind.Class, name: "C" }, "console.log('here');"]
};
doTest("class c {\n}", 0, [structure], "class c {\n /**\n * Test\n */\n @dec\n public static get prop<T>(param): number {\n"
doTest("class c {\n}", 0, [structure], "class c {\n /** Test */\n @dec\n public static get prop<T>(param): number {\n"
+ " class C {\n }\n\n console.log('here');\n"
+ " }\n}");
});
Expand Down Expand Up @@ -488,7 +488,7 @@ describe(nameof(ClassLikeDeclarationBase), () => {
isReadonly: true
};
doTest("class c {\n}", 0, [structure, { name: "other", hasExclamationToken: true }],
"class c {\n /**\n * Test\n */\n @dec\n public static readonly prop?: number = 5;\n other!;\n"
"class c {\n /** Test */\n @dec\n public static readonly prop?: number = 5;\n other!;\n"
+ "}");
});
});
Expand Down Expand Up @@ -644,7 +644,7 @@ describe(nameof(ClassLikeDeclarationBase), () => {
typeParameters: [{ name: "T" }],
statements: [{ kind: StructureKind.Class, name: "C" }, "console.log('here');"]
};
doTest("class c {\n}", 0, [structure], "class c {\n /**\n * Test\n */\n @dec\n public static set prop<T>(param): number {\n"
doTest("class c {\n}", 0, [structure], "class c {\n /** Test */\n @dec\n public static set prop<T>(param): number {\n"
+ " class C {\n }\n\n console.log('here');\n"
+ " }\n}");
});
Expand Down Expand Up @@ -888,7 +888,7 @@ describe(nameof(ClassLikeDeclarationBase), () => {
statements: [{ kind: StructureKind.Class, name: "C" }, "console.log('here');"]
};
doTest("class c {\n}", 0, [structure], "class c {\n public static myMethod?();\n private myMethod();\n"
+ " /**\n * Test\n */\n @dec\n public static async myMethod?<T>(param): number {\n"
+ " /** Test */\n @dec\n public static async myMethod?<T>(param): number {\n"
+ " class C {\n }\n\n console.log('here');\n"
+ " }\n}");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,7 @@ class Test {
* Description1.
*/
method(param1: string): string;
/**
* Description2.
*/
/** Description2. */
method(param2: number): number;
/** Ignores this implementation */
method() {
Expand All @@ -433,7 +431,7 @@ class Test {
}],
methods: [{
kind: StructureKind.MethodSignature,
docs: [{ kind: StructureKind.JSDoc, description: "Description1." }],
docs: [{ kind: StructureKind.JSDoc, description: "\nDescription1." }],
name: "method",
returnType: "string",
hasQuestionToken: false,
Expand Down Expand Up @@ -541,7 +539,7 @@ class Test<T extends string = number, U> extends Base implements IBase {
name: "Name",
constructSignatures: [{
kind: StructureKind.ConstructSignature,
docs: [{ kind: StructureKind.JSDoc, description: "Description." }],
docs: [{ kind: StructureKind.JSDoc, description: "\nDescription." }],
parameters: [{
kind: StructureKind.Parameter,
decorators: [],
Expand Down Expand Up @@ -695,9 +693,7 @@ class Test {
* Description1.
*/
static method(param1: string): string;
/**
* Description2.
*/
/** Description2. */
static method(param2: number): number;
/** Ignores this implementation */
static method() {
Expand All @@ -707,7 +703,7 @@ class Test {
name: "Name",
constructSignatures: [{
kind: StructureKind.ConstructSignature,
docs: [{ kind: StructureKind.JSDoc, description: "Test." }],
docs: [{ kind: StructureKind.JSDoc, description: "\nTest." }],
parameters: [{
kind: StructureKind.Parameter,
decorators: [],
Expand All @@ -722,7 +718,7 @@ class Test {
returnType: "Test"
}, {
kind: StructureKind.ConstructSignature,
docs: [{ kind: StructureKind.JSDoc, description: "Test2." }],
docs: [{ kind: StructureKind.JSDoc, description: "\nTest2." }],
parameters: [{
kind: StructureKind.Parameter,
decorators: [],
Expand All @@ -739,7 +735,7 @@ class Test {
properties: [],
methods: [{
kind: StructureKind.MethodSignature,
docs: [{ kind: StructureKind.JSDoc, description: "Description1." }],
docs: [{ kind: StructureKind.JSDoc, description: "\nDescription1." }],
name: "method",
returnType: "string",
hasQuestionToken: false,
Expand Down

0 comments on commit 59c8d38

Please sign in to comment.