diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3397cd242b1..9928be4529b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -621,7 +621,7 @@ namespace ts { return node && (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression); } - export function isFunctionLike(node: Node): boolean { + export function isFunctionLike(node: Node): node is FunctionLikeDeclaration { if (node) { switch (node.kind) { case SyntaxKind.Constructor: diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 1d333a29ccb..7faf4d1cc0b 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1908,7 +1908,7 @@ module FourSlash { } if (actual.newText !== expected.newText) { - this.raiseError(name + ' failed - expected insertion:\n' + expected.newText + '\nactual insertion:\n' + actual.newText); + this.raiseError(name + ' failed - expected insertion:\n' + this.clarifyNewlines(expected.newText) + '\nactual insertion:\n' + this.clarifyNewlines(actual.newText)); } if (actual.caretOffset !== expected.caretOffset) { @@ -1917,6 +1917,13 @@ module FourSlash { } } + private clarifyNewlines(str: string) { + return str.replace(/\r?\n/g, lineEnding => { + const representation = lineEnding === "\r\n" ? "CRLF" : "LF"; + return "# - " + representation + lineEnding; + }); + } + public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) { this.taoInvalidReason = "verifyMatchingBracePosition NYI"; diff --git a/src/services/services.ts b/src/services/services.ts index 87cca1fe0f1..4326e57f93e 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -6997,8 +6997,12 @@ namespace ts { * Checks if position points to a valid position to add JSDoc comments, and if so, * returns the appropriate template. Otherwise returns an empty string. * Valid positions are - * - outside of comments, statements, and expressions, and - * - preceding a function declaration. + * - outside of comments, statements, and expressions, and + * - preceding a: + * - function/constructor/method declaration + * - class declarations + * - variable statements + * - namespace declarations * * Hosts should ideally check that: * - The line is all whitespace up to 'position' before performing the insertion. @@ -7025,16 +7029,37 @@ namespace ts { } // TODO: add support for: - // - methods - // - constructors - // - class decls - let containingFunction = getAncestor(tokenAtPos, SyntaxKind.FunctionDeclaration); + // - enums/enum members + // - interfaces + // - property declarations + // - potentially property assignments + let commentOwner: Node; + findOwner: for (commentOwner = tokenAtPos; commentOwner; commentOwner = commentOwner.parent) { + switch (commentOwner.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.VariableStatement: + break findOwner; + case SyntaxKind.SourceFile: + return undefined; + case SyntaxKind.ModuleDeclaration: + // If in walking up the tree, we hit a a nested namespace declaration, + // then we must be somewhere within a dotted namespace name; however we don't + // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'. + if (commentOwner.parent.kind === SyntaxKind.ModuleDeclaration) { + return undefined; + } + break findOwner; + } + } - if (!containingFunction || containingFunction.getStart() < position) { + if (!commentOwner || commentOwner.getStart() < position) { return undefined; } - let parameters = containingFunction.parameters; + let parameters = isFunctionLike(commentOwner) ? commentOwner.parameters : emptyArray; let posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position); let lineStart = sourceFile.getLineStarts()[posLineAndChar.line]; @@ -7043,9 +7068,15 @@ namespace ts { // TODO: call a helper method instead once PR #4133 gets merged in. const newLine = host.getNewLine ? host.getNewLine() : "\r\n"; - let docParams = parameters.reduce((prev, cur, index) => - prev + - indentationStr + " * @param " + (cur.name.kind === SyntaxKind.Identifier ? (cur.name).text : "param" + index) + newLine, ""); + let docParams = ""; + for (let i = 0, numParams = parameters.length; i < numParams; i++) { + const currentName = parameters[i].name; + const paramName = currentName.kind === SyntaxKind.Identifier ? + (currentName).text : + "param" + i; + + docParams += `${indentationStr} * @param ${paramName}${newLine}`; + } // A doc comment consists of the following // * The opening comment line diff --git a/tests/cases/fourslash/docCommentTemplateClassDecl01.ts b/tests/cases/fourslash/docCommentTemplateClassDecl01.ts new file mode 100644 index 00000000000..958a8c60fa4 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateClassDecl01.ts @@ -0,0 +1,36 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, newTextOffset: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, newTextOffset); +} + +/////*decl*/class C { +//// private p; +//// constructor(a, b, c, d); +//// constructor(public a, private b, protected c, d, e?) { +//// } +//// +//// foo(); +//// foo(a?, b?, ...args) { +//// } +////} + +confirmNormalizedJsDoc("decl", /*newTextOffset*/ 8, ` +/** + * + */ +`); diff --git a/tests/cases/fourslash/docCommentTemplateClassDeclMethods01.ts b/tests/cases/fourslash/docCommentTemplateClassDeclMethods01.ts new file mode 100644 index 00000000000..da407b632ef --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateClassDeclMethods01.ts @@ -0,0 +1,79 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, indentation: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, indentation); +} + +const enum Indentation { + Standard = 8, + Indented = 12, +} + + +////class C { +/////*0*/ /*1*/ +//// foo(); +//// /*2*/foo(a); +//// /*3*/foo(a, b); +//// /*4*/ foo(a, {x: string}, [c]); +//// /*5*/foo(a?, b?, ...args) { +//// } +////} + +confirmNormalizedJsDoc("0", Indentation.Standard, ` +/** + * + */`); + + +confirmNormalizedJsDoc("1", Indentation.Indented, + `/** + * + */`); + + +confirmNormalizedJsDoc("2", Indentation.Indented, + `/** + * + * @param a + */ + `); + +confirmNormalizedJsDoc("3", Indentation.Indented, + `/** + * + * @param a + * @param b + */ + `); + +confirmNormalizedJsDoc("4", Indentation.Indented, + `/** + * + * @param a + * @param param1 + * @param param2 + */`); + +confirmNormalizedJsDoc("5", Indentation.Indented, + `/** + * + * @param a + * @param b + * @param args + */ + `); diff --git a/tests/cases/fourslash/docCommentTemplateClassDeclMethods02.ts b/tests/cases/fourslash/docCommentTemplateClassDeclMethods02.ts new file mode 100644 index 00000000000..99392cf3855 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateClassDeclMethods02.ts @@ -0,0 +1,46 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, indentation: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, indentation); +} + +const enum Indentation { + Indented = 12, +} + + +////class C { +//// /*0*/ +//// [Symbol.iterator]() { +//// return undefined; +//// } +//// /*1*/ +//// [1 + 2 + 3 + Math.rand()](x: number, y: string, z = true) { } +////} + +confirmNormalizedJsDoc("0", Indentation.Indented, + `/** + * + */`); + +confirmNormalizedJsDoc("1", Indentation.Indented, + `/** + * + * @param x + * @param y + * @param z + */`); \ No newline at end of file diff --git a/tests/cases/fourslash/docCommentTemplateConstructor01.ts b/tests/cases/fourslash/docCommentTemplateConstructor01.ts new file mode 100644 index 00000000000..b26ece7a5e6 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateConstructor01.ts @@ -0,0 +1,52 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, newTextOffset: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, newTextOffset); +} + +////class C { +//// private p; +//// /*0*/ +//// constructor(a, b, c, d); +//// /*1*/ +//// constructor(public a, private b, protected c, d, e?) { +//// } +//// +//// foo(); +//// foo(a?, b?, ...args) { +//// } +////} + +const newTextOffset = 12; +confirmNormalizedJsDoc("0", /*newTextOffset*/ newTextOffset, + `/** + * + * @param a + * @param b + * @param c + * @param d + */`); + +confirmNormalizedJsDoc("1", /*newTextOffset*/ newTextOffset, + `/** + * + * @param a + * @param b + * @param c + * @param d + * @param e + */`); diff --git a/tests/cases/fourslash/docCommentTemplateNamespacesAndModules01.ts b/tests/cases/fourslash/docCommentTemplateNamespacesAndModules01.ts new file mode 100644 index 00000000000..4d9fb987be5 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateNamespacesAndModules01.ts @@ -0,0 +1,46 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, charOffset: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, charOffset); +} + +/////*namespaceN*/ +////namespace n { +////} +//// +/////*namespaceM*/ +////module m { +////} +//// +/////*ambientModule*/ +////module "ambientModule" { +////} + +confirmNormalizedJsDoc("namespaceN", /*indentation*/ 8, ` +/** + * + */`); + +confirmNormalizedJsDoc("namespaceM", /*indentation*/ 8, ` +/** + * + */`); + +confirmNormalizedJsDoc("namespaceM", /*indentation*/ 8, ` +/** + * + */`); diff --git a/tests/cases/fourslash/docCommentTemplateNamespacesAndModules02.ts b/tests/cases/fourslash/docCommentTemplateNamespacesAndModules02.ts new file mode 100644 index 00000000000..e59b16d6163 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateNamespacesAndModules02.ts @@ -0,0 +1,36 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, charOffset: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, charOffset); +} + +/////*top*/ +////namespace n1. +//// /*n2*/ n2. +//// /*n3*/ n3 { +////} + +confirmNormalizedJsDoc("top", /*indentation*/ 8, ` +/** + * + */`); + +goTo.marker("n2"); +verify.noDocCommentTemplate(); + +goTo.marker("n3"); +verify.noDocCommentTemplate(); \ No newline at end of file diff --git a/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts b/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts new file mode 100644 index 00000000000..4af1b60c698 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts @@ -0,0 +1,46 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, indentation: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, indentation); +} + +const enum Indentation { + Indented = 12, +} + + +////var x = { +//// /*0*/ +//// foo() { +//// return undefined; +//// } +//// /*1*/ +//// [1 + 2 + 3 + Math.rand()](x: number, y: string, z = true) { } +////} + +confirmNormalizedJsDoc("0", Indentation.Indented, + `/** + * + */`); + +confirmNormalizedJsDoc("1", Indentation.Indented, + `/** + * + * @param x + * @param y + * @param z + */`); \ No newline at end of file diff --git a/tests/cases/fourslash/docCommentTemplateVariableStatements01.ts b/tests/cases/fourslash/docCommentTemplateVariableStatements01.ts new file mode 100644 index 00000000000..6859d9aa1ec --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateVariableStatements01.ts @@ -0,0 +1,54 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, newTextOffset: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, newTextOffset); +} + +/////*a*/ +////var a = 10; +//// +/////*b*/ +////let b = ""; +//// +/////*c*/ +////const c = 30; +//// +/////*d*/ +////let d = function d(x, y, z) { +//// return +(x + y + z); +////}; +//// +/////*e*/ +////let e = class E { +//// constructor(a, b, c) { +//// this.a = a; +//// this.b = b || (this.c = c); +//// } +////} +//// +/////*f*/ +////let f = { +//// foo: 10, +//// bar: "20" +////}; + +for (const varName of "abcdef".split("")) { + confirmNormalizedJsDoc(varName, /*newTextOffset*/ 8, ` +/** + * + */`); +} diff --git a/tests/cases/fourslash/docCommentTemplateVariableStatements02.ts b/tests/cases/fourslash/docCommentTemplateVariableStatements02.ts new file mode 100644 index 00000000000..9339e703570 --- /dev/null +++ b/tests/cases/fourslash/docCommentTemplateVariableStatements02.ts @@ -0,0 +1,54 @@ +/// + +const CRLF = "\r\n"; +/** + * @returns the given value with '\n' normalized to '\r\n' and with no leading newline + */ +function useCRLFAndStripLeadingNewline(str: string): string { + str = str.replace(/\r?\n/g, CRLF); + if (str.indexOf(CRLF) === 0) { + str = str.slice(CRLF.length); + } + return str; +} + +function confirmNormalizedJsDoc(markerName: string, newTextOffset: number, template: string): void { + goTo.marker(markerName); + const normalized = useCRLFAndStripLeadingNewline(template); + verify.DocCommentTemplate(normalized, newTextOffset); +} + +/////*a*/ +////var a1 = 10, a2 = 20; +//// +/////*b*/ +////let b1 = "", b2 = true; +//// +/////*c*/ +////const c1 = 30, c2 = 40; +//// +/////*d*/ +////let d1 = function d(x, y, z) { +//// return +(x + y + z); +////}, d2 = 50; +//// +/////*e*/ +////let e1 = class E { +//// constructor(a, b, c) { +//// this.a = a; +//// this.b = b || (this.c = c); +//// } +////}, e2 = () => 100; +//// +/////*f*/ +////let f1 = { +//// foo: 10, +//// bar: "20" +////}, f2 = null; + +for (const varName of "abcdef".split("")) { + confirmNormalizedJsDoc(varName, /*newTextOffset*/ 8, ` +/** + * + */`); +}