Merge existing JSDoc comments (#27978)

* Correct indentation, using correct (I hope) indentation code

Note that part of the code, in formatting.ts, is cloned but should be
extracted to a function instead.

* Remove some possibly-superfluous code

But I see 4 failures with whitespace, so perhaps not.

* Restrict indentation change to avoid breaking baselines

The indentation code is very complex so I'm just going to avoid breaking
our single-line tests for now, plus add a simple jsdoc test to show that
multiline jsdoc indentation isn't destroyed in the common case.

* Switched over to construction for @return/@type

Still doesn't merge correctly though

* Add @return tags to emitter

* Merge multiple jsdocs

(not for @param yet)

* Merge multiple jsdoc for parameters too

* Emit more jsdoc tags

Not all of them; I got cold feet since I'll have to write tests for
them. I'll do that tomorrow.

* Many fixes to JSDoc emit

And single tests (at least) for all tags

* Cleanup in textChanges.ts

* Cleanup in formatting.ts

(Plus a little more in textChanges.ts)

* Cleanup in inferFromUsage.ts

* Fix minor omissions

* Separate merged top-level JSDoc comments with \n

instead of space.

* Don't delete intrusive non-jsdoc comments

* Cleanup from PR comments

1. Refactor emit code into smaller functions.
2. Preceding-whitespace utility is slightly easier to use.
3. Better casts and types in inferFromUsage make it easier to read.

* Fix bogus newline

* Use @andy-ms' cleanup annotateJSDocParameters
This commit is contained in:
Nathan Shively-Sanders
2018-10-24 16:14:52 -07:00
committed by GitHub
parent e46c846ee6
commit fe2a33fcbc
20 changed files with 654 additions and 249 deletions
+187 -2
View File
@@ -862,7 +862,34 @@ namespace ts {
case SyntaxKind.EnumMember:
return emitEnumMember(<EnumMember>node);
// JSDoc nodes (ignored)
// JSDoc nodes (only used in codefixes currently)
case SyntaxKind.JSDocParameterTag:
case SyntaxKind.JSDocPropertyTag:
return emitJSDocPropertyLikeTag(node as JSDocPropertyLikeTag);
case SyntaxKind.JSDocReturnTag:
case SyntaxKind.JSDocTypeTag:
case SyntaxKind.JSDocThisTag:
case SyntaxKind.JSDocEnumTag:
return emitJSDocSimpleTypedTag(node as JSDocTypeTag);
case SyntaxKind.JSDocAugmentsTag:
return emitJSDocAugmentsTag(node as JSDocAugmentsTag);
case SyntaxKind.JSDocTemplateTag:
return emitJSDocTemplateTag(node as JSDocTemplateTag);
case SyntaxKind.JSDocTypedefTag:
return emitJSDocTypedefTag(node as JSDocTypedefTag);
case SyntaxKind.JSDocCallbackTag:
return emitJSDocCallbackTag(node as JSDocCallbackTag);
case SyntaxKind.JSDocSignature:
return emitJSDocSignature(node as JSDocSignature);
case SyntaxKind.JSDocTypeLiteral:
return emitJSDocTypeLiteral(node as JSDocTypeLiteral);
case SyntaxKind.JSDocClassTag:
case SyntaxKind.JSDocTag:
return emitJSDocSimpleTag(node as JSDocTag);
case SyntaxKind.JSDocComment:
return emitJSDoc(node as JSDoc);
// Transformation nodes (ignored)
}
@@ -2584,6 +2611,154 @@ namespace ts {
emitInitializer(node.initializer, node.name.end, node);
}
//
// JSDoc
//
function emitJSDoc(node: JSDoc) {
write("/**");
if (node.comment) {
const lines = node.comment.split(/\r\n?|\n/g);
for (const line of lines) {
writeLine();
writeSpace();
writePunctuation("*");
writeSpace();
write(line);
}
}
if (node.tags) {
if (node.tags.length === 1 && node.tags[0].kind === SyntaxKind.JSDocTypeTag && !node.comment) {
writeSpace();
emit(node.tags[0]);
}
else {
emitList(node, node.tags, ListFormat.JSDocComment);
}
}
writeSpace();
write("*/");
}
function emitJSDocSimpleTypedTag(tag: JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocReturnTag) {
emitJSDocTagName(tag.tagName);
emitJSDocTypeExpression(tag.typeExpression);
emitJSDocComment(tag.comment);
}
function emitJSDocAugmentsTag(tag: JSDocAugmentsTag) {
emitJSDocTagName(tag.tagName);
writeSpace();
writePunctuation("{");
emit(tag.class);
writePunctuation("}");
emitJSDocComment(tag.comment);
}
function emitJSDocTemplateTag(tag: JSDocTemplateTag) {
emitJSDocTagName(tag.tagName);
emitJSDocTypeExpression(tag.constraint);
writeSpace();
emitList(tag, tag.typeParameters, ListFormat.CommaListElements);
emitJSDocComment(tag.comment);
}
function emitJSDocTypedefTag(tag: JSDocTypedefTag) {
emitJSDocTagName(tag.tagName);
if (tag.typeExpression) {
if (tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
emitJSDocTypeExpression(tag.typeExpression);
}
else {
writeSpace();
writePunctuation("{");
write("Object");
if (tag.typeExpression.isArrayType) {
writePunctuation("[");
writePunctuation("]");
}
writePunctuation("}");
}
}
if (tag.fullName) {
writeSpace();
emit(tag.fullName);
}
emitJSDocComment(tag.comment);
if (tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeLiteral) {
emitJSDocTypeLiteral(tag.typeExpression);
}
}
function emitJSDocCallbackTag(tag: JSDocCallbackTag) {
emitJSDocTagName(tag.tagName);
if (tag.name) {
writeSpace();
emit(tag.name);
}
emitJSDocComment(tag.comment);
emitJSDocSignature(tag.typeExpression);
}
function emitJSDocSimpleTag(tag: JSDocTag) {
emitJSDocTagName(tag.tagName);
emitJSDocComment(tag.comment);
}
function emitJSDocTypeLiteral(lit: JSDocTypeLiteral) {
emitList(lit, createNodeArray(lit.jsDocPropertyTags), ListFormat.JSDocComment);
}
function emitJSDocSignature(sig: JSDocSignature) {
if (sig.typeParameters) {
emitList(sig, createNodeArray(sig.typeParameters), ListFormat.JSDocComment);
}
if (sig.parameters) {
emitList(sig, createNodeArray(sig.parameters), ListFormat.JSDocComment);
}
if (sig.type) {
writeLine();
writeSpace();
writePunctuation("*");
writeSpace();
emit(sig.type);
}
}
function emitJSDocPropertyLikeTag(param: JSDocPropertyLikeTag) {
emitJSDocTagName(param.tagName);
emitJSDocTypeExpression(param.typeExpression);
writeSpace();
if (param.isBracketed) {
writePunctuation("[");
}
emit(param.name);
if (param.isBracketed) {
writePunctuation("]");
}
emitJSDocComment(param.comment);
}
function emitJSDocTagName(tagName: Identifier) {
writePunctuation("@");
emit(tagName);
}
function emitJSDocComment(comment: string | undefined) {
if (comment) {
writeSpace();
write(comment);
}
}
function emitJSDocTypeExpression(typeExpression: JSDocTypeExpression | undefined) {
if (typeExpression) {
writeSpace();
writePunctuation("{");
emit(typeExpression.type);
writePunctuation("}");
}
}
//
// Top-level nodes
//
@@ -2875,6 +3050,11 @@ namespace ts {
writeSpace();
writePunctuation("|");
break;
case ListFormat.AsteriskDelimited:
writeSpace();
writePunctuation("*");
writeSpace();
break;
case ListFormat.AmpersandDelimited:
writeSpace();
writePunctuation("&");
@@ -2944,7 +3124,12 @@ namespace ts {
const child = children![start + i];
// Write the delimiter if this is not the first node.
if (previousSibling) {
if (format & ListFormat.AsteriskDelimited) {
// always write JSDoc in the format "\n *"
writeLine();
writeDelimiter(format);
}
else if (previousSibling) {
// i.e
// function commentedParameters(
// /* Parameter a */
+51
View File
@@ -2170,6 +2170,57 @@ namespace ts {
: node;
}
// JSDoc
/* @internal */
export function createJSDocTypeExpression(type: TypeNode): JSDocTypeExpression {
const node = createSynthesizedNode(SyntaxKind.JSDocTypeExpression) as JSDocTypeExpression;
node.type = type;
return node;
}
/* @internal */
export function createJSDocTypeTag(typeExpression?: JSDocTypeExpression, comment?: string): JSDocTypeTag {
const tag = createJSDocTag<JSDocTypeTag>(SyntaxKind.JSDocTypeTag, "type");
tag.typeExpression = typeExpression;
tag.comment = comment;
return tag;
}
/* @internal */
export function createJSDocReturnTag(typeExpression?: JSDocTypeExpression, comment?: string): JSDocReturnTag {
const tag = createJSDocTag<JSDocReturnTag>(SyntaxKind.JSDocReturnTag, "returns");
tag.typeExpression = typeExpression;
tag.comment = comment;
return tag;
}
/* @internal */
export function createJSDocParamTag(name: EntityName, isBracketed: boolean, typeExpression?: JSDocTypeExpression, comment?: string): JSDocParameterTag {
const tag = createJSDocTag<JSDocParameterTag>(SyntaxKind.JSDocParameterTag, "param");
tag.typeExpression = typeExpression;
tag.name = name;
tag.isBracketed = isBracketed;
tag.comment = comment;
return tag;
}
/* @internal */
export function createJSDocComment(comment?: string | undefined, tags?: NodeArray<JSDocTag> | undefined) {
const node = createSynthesizedNode(SyntaxKind.JSDocComment) as JSDoc;
node.comment = comment;
node.tags = tags;
return node;
}
/* @internal */
function createJSDocTag<T extends JSDocTag>(kind: T["kind"], tagName: string): T {
const node = createSynthesizedNode(kind) as T;
node.atToken = createToken(SyntaxKind.AtToken);
node.tagName = createIdentifier(tagName);
return node;
}
// JSX
export function createJsxElement(openingElement: JsxOpeningElement, children: ReadonlyArray<JsxChild>, closingElement: JsxClosingElement) {
+19 -17
View File
@@ -2447,7 +2447,7 @@ namespace ts {
export interface JSDocTemplateTag extends JSDocTag {
kind: SyntaxKind.JSDocTemplateTag;
constraint: TypeNode | undefined;
constraint: JSDocTypeExpression | undefined;
typeParameters: NodeArray<TypeParameterDeclaration>;
}
@@ -5532,33 +5532,34 @@ namespace ts {
BarDelimited = 1 << 2, // Each list item is space-and-bar (" |") delimited.
AmpersandDelimited = 1 << 3, // Each list item is space-and-ampersand (" &") delimited.
CommaDelimited = 1 << 4, // Each list item is comma (",") delimited.
DelimitersMask = BarDelimited | AmpersandDelimited | CommaDelimited,
AsteriskDelimited = 1 << 5, // Each list item is asterisk ("\n *") delimited, used with JSDoc.
DelimitersMask = BarDelimited | AmpersandDelimited | CommaDelimited | AsteriskDelimited,
AllowTrailingComma = 1 << 5, // Write a trailing comma (",") if present.
AllowTrailingComma = 1 << 6, // Write a trailing comma (",") if present.
// Whitespace
Indented = 1 << 6, // The list should be indented.
SpaceBetweenBraces = 1 << 7, // Inserts a space after the opening brace and before the closing brace.
SpaceBetweenSiblings = 1 << 8, // Inserts a space between each sibling node.
Indented = 1 << 7, // The list should be indented.
SpaceBetweenBraces = 1 << 8, // Inserts a space after the opening brace and before the closing brace.
SpaceBetweenSiblings = 1 << 9, // Inserts a space between each sibling node.
// Brackets/Braces
Braces = 1 << 9, // The list is surrounded by "{" and "}".
Parenthesis = 1 << 10, // The list is surrounded by "(" and ")".
AngleBrackets = 1 << 11, // The list is surrounded by "<" and ">".
SquareBrackets = 1 << 12, // The list is surrounded by "[" and "]".
Braces = 1 << 10, // The list is surrounded by "{" and "}".
Parenthesis = 1 << 11, // The list is surrounded by "(" and ")".
AngleBrackets = 1 << 12, // The list is surrounded by "<" and ">".
SquareBrackets = 1 << 13, // The list is surrounded by "[" and "]".
BracketsMask = Braces | Parenthesis | AngleBrackets | SquareBrackets,
OptionalIfUndefined = 1 << 13, // Do not emit brackets if the list is undefined.
OptionalIfEmpty = 1 << 14, // Do not emit brackets if the list is empty.
OptionalIfUndefined = 1 << 14, // Do not emit brackets if the list is undefined.
OptionalIfEmpty = 1 << 15, // Do not emit brackets if the list is empty.
Optional = OptionalIfUndefined | OptionalIfEmpty,
// Other
PreferNewLine = 1 << 15, // Prefer adding a LineTerminator between synthesized nodes.
NoTrailingNewLine = 1 << 16, // Do not emit a trailing NewLine for a MultiLine list.
NoInterveningComments = 1 << 17, // Do not emit comments between each node
PreferNewLine = 1 << 16, // Prefer adding a LineTerminator between synthesized nodes.
NoTrailingNewLine = 1 << 17, // Do not emit a trailing NewLine for a MultiLine list.
NoInterveningComments = 1 << 18, // Do not emit comments between each node
NoSpaceIfEmpty = 1 << 18, // If the literal is empty, do not add spaces between braces.
SingleElement = 1 << 19,
NoSpaceIfEmpty = 1 << 19, // If the literal is empty, do not add spaces between braces.
SingleElement = 1 << 20,
// Precomputed Formats
Modifiers = SingleLine | SpaceBetweenSiblings | NoInterveningComments,
@@ -5598,6 +5599,7 @@ namespace ts {
TypeParameters = CommaDelimited | SpaceBetweenSiblings | SingleLine | AngleBrackets | Optional,
Parameters = CommaDelimited | SpaceBetweenSiblings | SingleLine | Parenthesis,
IndexSignatureParameters = CommaDelimited | SpaceBetweenSiblings | SingleLine | Indented | SquareBrackets,
JSDocComment = MultiLine | AsteriskDelimited,
}
/* @internal */
+51 -7
View File
@@ -73,7 +73,9 @@ namespace ts.codefix {
const type = inferTypeForVariableFromUsage(parent.name, program, cancellationToken);
const typeNode = type && getTypeNodeIfAccessible(type, parent, program, host);
if (typeNode) {
changes.tryInsertJSDocType(sourceFile, parent, typeNode);
// Note that the codefix will never fire with an existing `@type` tag, so there is no need to merge tags
const typeTag = createJSDocTypeTag(createJSDocTypeExpression(typeNode), /*comment*/ "");
addJSDocTags(changes, sourceFile, cast(parent.parent.parent, isExpressionStatement), [typeTag]);
}
return parent;
}
@@ -192,7 +194,13 @@ namespace ts.codefix {
const typeNode = type && getTypeNodeIfAccessible(type, declaration, program, host);
if (typeNode) {
if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) {
changes.tryInsertJSDocType(sourceFile, declaration, typeNode);
const parent = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : declaration;
if (!parent) {
return;
}
const typeExpression = createJSDocTypeExpression(typeNode);
const typeTag = isGetAccessorDeclaration(declaration) ? createJSDocReturnTag(typeExpression, "") : createJSDocTypeTag(typeExpression, "");
addJSDocTags(changes, sourceFile, parent, [typeTag]);
}
else {
changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode);
@@ -200,16 +208,52 @@ namespace ts.codefix {
}
}
function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: ParameterInference[], program: Program, host: LanguageServiceHost): void {
const result = mapDefined(parameterInferences, inference => {
function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: ReadonlyArray<ParameterInference>, program: Program, host: LanguageServiceHost): void {
const signature = parameterInferences.length && parameterInferences[0].declaration.parent;
if (!signature) {
return;
}
const paramTags = mapDefined(parameterInferences, inference => {
const param = inference.declaration;
// only infer parameters that have (1) no type and (2) an accessible inferred type
if (param.initializer || getJSDocType(param) || !isIdentifier(param.name)) return;
const typeNode = inference.type && getTypeNodeIfAccessible(inference.type, param, program, host);
return typeNode && !param.initializer && !getJSDocType(param) ? { ...inference, typeNode } : undefined;
return typeNode && createJSDocParamTag(param.name, !!inference.isOptional, createJSDocTypeExpression(typeNode), "");
});
changes.tryInsertJSDocParameters(sourceFile, result);
addJSDocTags(changes, sourceFile, signature, paramTags);
}
function getTypeNodeIfAccessible(type: Type, enclosingScope: Node, program: Program, host: LanguageServiceHost): TypeNode | undefined {
function addJSDocTags(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parent: HasJSDoc, newTags: ReadonlyArray<JSDocTag>): void {
const comments = mapDefined(parent.jsDoc, j => j.comment);
const oldTags = flatMap(parent.jsDoc, j => j.tags);
const unmergedNewTags = newTags.filter(newTag => !oldTags || !oldTags.some((tag, i) => {
const merged = tryMergeJsdocTags(tag, newTag);
if (merged) oldTags[i] = merged;
return !!merged;
}));
const tag = createJSDocComment(comments.join("\n"), createNodeArray([...(oldTags || emptyArray), ...unmergedNewTags]));
changes.insertJsdocCommentBefore(sourceFile, parent, tag);
}
function tryMergeJsdocTags(oldTag: JSDocTag, newTag: JSDocTag): JSDocTag | undefined {
if (oldTag.kind !== newTag.kind) {
return undefined;
}
switch (oldTag.kind) {
case SyntaxKind.JSDocParameterTag: {
const oldParam = oldTag as JSDocParameterTag;
const newParam = newTag as JSDocParameterTag;
return isIdentifier(oldParam.name) && isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText
? createJSDocParamTag(newParam.name, newParam.isBracketed, newParam.typeExpression, oldParam.comment)
: undefined;
}
case SyntaxKind.JSDocReturnTag:
return createJSDocReturnTag((newTag as JSDocReturnTag).typeExpression, oldTag.comment);
}
}
function getTypeNodeIfAccessible(type: Type, enclosingScope: Node, program: Program, host: LanguageServiceHost): TypeNode | undefined {
const checker = program.getTypeChecker();
let typeIsAccessible = true;
const notAccessible = () => { typeIsAccessible = false; };
+51 -34
View File
@@ -365,16 +365,21 @@ namespace ts.formatting {
function formatSpan(originalRange: TextRange, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] {
// find the smallest node that fully wraps the range and compute the initial indentation for the node
const enclosingNode = findEnclosingNode(originalRange, sourceFile);
return getFormattingScanner(sourceFile.text, sourceFile.languageVariant, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end, scanner => formatSpanWorker(
originalRange,
enclosingNode,
SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options),
getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile),
scanner,
formatContext,
requestKind,
prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange),
sourceFile));
return getFormattingScanner(
sourceFile.text,
sourceFile.languageVariant,
getScanStartPosition(enclosingNode, originalRange, sourceFile),
originalRange.end,
scanner => formatSpanWorker(
originalRange,
enclosingNode,
SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options),
getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile),
scanner,
formatContext,
requestKind,
prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange),
sourceFile));
}
function formatSpanWorker(originalRange: TextRange,
@@ -413,7 +418,8 @@ namespace ts.formatting {
if (!formattingScanner.isOnToken()) {
const leadingTrivia = formattingScanner.getCurrentLeadingTrivia();
if (leadingTrivia) {
processTrivia(leadingTrivia, enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!); // TODO: GH#18217
indentTriviaItems(leadingTrivia, initialIndentation, /*indentNextTokenOrTrivia*/ false,
item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!));
trimTrailingWhitespacesForRemainingRange();
}
}
@@ -814,27 +820,8 @@ namespace ts.formatting {
let indentNextTokenOrTrivia = true;
if (currentTokenInfo.leadingTrivia) {
const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container);
for (const triviaItem of currentTokenInfo.leadingTrivia) {
const triviaInRange = rangeContainsRange(originalRange, triviaItem);
switch (triviaItem.kind) {
case SyntaxKind.MultiLineCommentTrivia:
if (triviaInRange) {
indentMultilineCommentOrJsxText(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia);
}
indentNextTokenOrTrivia = false;
break;
case SyntaxKind.SingleLineCommentTrivia:
if (indentNextTokenOrTrivia && triviaInRange) {
insertIndentation(triviaItem.pos, commentIndentation, /*lineAdded*/ false);
}
indentNextTokenOrTrivia = false;
break;
case SyntaxKind.NewLineTrivia:
indentNextTokenOrTrivia = true;
break;
}
}
indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia,
item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false));
}
// indent token only if is it is in target range and does not overlap with any error ranges
@@ -852,6 +839,34 @@ namespace ts.formatting {
}
}
function indentTriviaItems(
trivia: TextRangeWithKind[],
commentIndentation: number,
indentNextTokenOrTrivia: boolean,
indentSingleLine: (item: TextRangeWithKind) => void) {
for (const triviaItem of trivia) {
const triviaInRange = rangeContainsRange(originalRange, triviaItem);
switch (triviaItem.kind) {
case SyntaxKind.MultiLineCommentTrivia:
if (triviaInRange) {
indentMultilineCommentOrJsxText(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia);
}
indentNextTokenOrTrivia = false;
break;
case SyntaxKind.SingleLineCommentTrivia:
if (indentNextTokenOrTrivia && triviaInRange) {
indentSingleLine(triviaItem);
}
indentNextTokenOrTrivia = false;
break;
case SyntaxKind.NewLineTrivia:
indentNextTokenOrTrivia = true;
break;
}
}
return indentNextTokenOrTrivia;
}
function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void {
for (const triviaItem of trivia) {
if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) {
@@ -861,7 +876,6 @@ namespace ts.formatting {
}
}
// TODO: GH#18217 use an enum instead of `boolean | undefined`
function processRange(range: TextRangeWithKind,
rangeStart: LineAndCharacter,
parent: Node,
@@ -1072,7 +1086,10 @@ namespace ts.formatting {
* Trimming will be done for lines after the previous range
*/
function trimTrailingWhitespacesForRemainingRange() {
const startPosition = previousRange ? previousRange.end : originalRange.pos;
if (!previousRange) {
return;
}
const startPosition = previousRange.end;
const startLine = sourceFile.getLineAndCharacterOfPosition(startPosition).line;
const endLine = sourceFile.getLineAndCharacterOfPosition(originalRange.end).line;
+3 -3
View File
@@ -79,14 +79,14 @@ namespace ts.formatting {
return findFirstNonWhitespaceColumn(getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options);
}
const startPostionOfLine = getStartPositionOfLine(previousLine, sourceFile);
const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPostionOfLine, position, sourceFile, options);
const startPositionOfLine = getStartPositionOfLine(previousLine, sourceFile);
const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options);
if (column === 0) {
return column;
}
const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPostionOfLine + character);
const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPositionOfLine + character);
return firstNonWhitespaceCharacterCode === CharacterCodes.asterisk ? column - 1 : column;
}
+13 -48
View File
@@ -209,12 +209,6 @@ namespace ts.textChanges {
export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature;
interface JSDocParameter {
declaration: ParameterDeclaration;
typeNode: TypeNode;
isOptional?: boolean;
}
export class ChangeTracker {
private readonly changes: Change[] = [];
private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
@@ -345,10 +339,19 @@ namespace ts.textChanges {
this.insertText(sourceFile, token.getStart(sourceFile), text);
}
public insertCommentThenNewline(sourceFile: SourceFile, character: number, position: number, commentText: string): void {
const token = getTouchingToken(sourceFile, position);
const text = "/**" + commentText + "*/" + this.newLineCharacter + repeatString(" ", character);
this.insertText(sourceFile, token.getStart(sourceFile), text);
public insertJsdocCommentBefore(sourceFile: SourceFile, node: HasJSDoc, tag: JSDoc) {
const fnStart = node.getStart(sourceFile);
if (node.jsDoc) {
for (const jsdoc of node.jsDoc) {
this.deleteRange(sourceFile, {
pos: getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile),
end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {})
});
}
}
const startPosition = getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1);
const indent = sourceFile.text.slice(startPosition, fnStart);
this.insertNodeAt(sourceFile, fnStart, tag, { preserveLeadingWhitespace: false, suffix: this.newLineCharacter + indent });
}
public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string) {
@@ -359,23 +362,6 @@ namespace ts.textChanges {
this.replaceRangeWithText(sourceFile, createRange(pos), text);
}
public tryInsertJSDocParameters(sourceFile: SourceFile, parameters: JSDocParameter[]) {
if (parameters.length === 0) {
return;
}
const parent = parameters[0].declaration.parent;
const indent = getLineAndCharacterOfPosition(sourceFile, parent.getStart()).character;
let commentText = "\n";
for (const { declaration, typeNode, isOptional } of parameters) {
if (isIdentifier(declaration.name)) {
const printed = changesToText.getNonformattedText(typeNode, sourceFile, this.newLineCharacter).text;
commentText += this.printJSDocParameter(indent, printed, declaration.name, isOptional);
}
}
commentText += repeatString(" ", indent + 1);
this.insertCommentThenNewline(sourceFile, indent, parent.getStart(), commentText);
}
/** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */
public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): void {
let endNode: Node | undefined;
@@ -394,27 +380,6 @@ namespace ts.textChanges {
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
}
public tryInsertJSDocType(sourceFile: SourceFile, node: Node, type: TypeNode): void {
const printed = changesToText.getNonformattedText(type, sourceFile, this.newLineCharacter).text;
let commentText;
if (isGetAccessorDeclaration(node)) {
commentText = ` @return {${printed}} `;
}
else {
commentText = ` @type {${printed}} `;
node = node.parent;
}
this.insertCommentThenNewline(sourceFile, getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).character, node.getStart(sourceFile), commentText);
}
private printJSDocParameter(indent: number, printed: string, name: Identifier, isOptionalParameter: boolean | undefined) {
let printName = unescapeLeadingUnderscores(name.escapedText);
if (isOptionalParameter) {
printName = `[${printName}]`;
}
return repeatString(" ", indent) + ` * @param {${printed}} ${printName}\n`;
}
public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: ReadonlyArray<TypeParameterDeclaration>): void {
// If no `(`, is an arrow function `x => x`, so use the pos of the first parameter
const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile);
+7
View File
@@ -1672,6 +1672,13 @@ namespace ts {
return position;
}
export function getPrecedingNonSpaceCharacterPosition(text: string, position: number) {
while (position > -1 && isWhiteSpaceSingleLine(text.charCodeAt(position))) {
position -= 1;
}
return position + 1;
}
/**
* Creates a deep, memberwise clone of a node with no source map location.
*
+55 -53
View File
@@ -1573,7 +1573,7 @@ declare namespace ts {
}
interface JSDocTemplateTag extends JSDocTag {
kind: SyntaxKind.JSDocTemplateTag;
constraint: TypeNode | undefined;
constraint: JSDocTypeExpression | undefined;
typeParameters: NodeArray<TypeParameterDeclaration>;
}
interface JSDocReturnTag extends JSDocTag {
@@ -2942,60 +2942,62 @@ declare namespace ts {
BarDelimited = 4,
AmpersandDelimited = 8,
CommaDelimited = 16,
DelimitersMask = 28,
AllowTrailingComma = 32,
Indented = 64,
SpaceBetweenBraces = 128,
SpaceBetweenSiblings = 256,
Braces = 512,
Parenthesis = 1024,
AngleBrackets = 2048,
SquareBrackets = 4096,
BracketsMask = 7680,
OptionalIfUndefined = 8192,
OptionalIfEmpty = 16384,
Optional = 24576,
PreferNewLine = 32768,
NoTrailingNewLine = 65536,
NoInterveningComments = 131072,
NoSpaceIfEmpty = 262144,
SingleElement = 524288,
Modifiers = 131328,
HeritageClauses = 256,
SingleLineTypeLiteralMembers = 384,
MultiLineTypeLiteralMembers = 16449,
TupleTypeElements = 272,
UnionTypeConstituents = 260,
IntersectionTypeConstituents = 264,
ObjectBindingPatternElements = 262576,
ArrayBindingPatternElements = 262448,
ObjectLiteralExpressionProperties = 263122,
ArrayLiteralExpressionElements = 4466,
CommaListElements = 272,
CallExpressionArguments = 1296,
NewExpressionArguments = 9488,
TemplateExpressionSpans = 131072,
SingleLineBlockStatements = 384,
MultiLineBlockStatements = 65,
VariableDeclarationList = 272,
SingleLineFunctionBodyStatements = 384,
AsteriskDelimited = 32,
DelimitersMask = 60,
AllowTrailingComma = 64,
Indented = 128,
SpaceBetweenBraces = 256,
SpaceBetweenSiblings = 512,
Braces = 1024,
Parenthesis = 2048,
AngleBrackets = 4096,
SquareBrackets = 8192,
BracketsMask = 15360,
OptionalIfUndefined = 16384,
OptionalIfEmpty = 32768,
Optional = 49152,
PreferNewLine = 65536,
NoTrailingNewLine = 131072,
NoInterveningComments = 262144,
NoSpaceIfEmpty = 524288,
SingleElement = 1048576,
Modifiers = 262656,
HeritageClauses = 512,
SingleLineTypeLiteralMembers = 768,
MultiLineTypeLiteralMembers = 32897,
TupleTypeElements = 528,
UnionTypeConstituents = 516,
IntersectionTypeConstituents = 520,
ObjectBindingPatternElements = 525136,
ArrayBindingPatternElements = 524880,
ObjectLiteralExpressionProperties = 526226,
ArrayLiteralExpressionElements = 8914,
CommaListElements = 528,
CallExpressionArguments = 2576,
NewExpressionArguments = 18960,
TemplateExpressionSpans = 262144,
SingleLineBlockStatements = 768,
MultiLineBlockStatements = 129,
VariableDeclarationList = 528,
SingleLineFunctionBodyStatements = 768,
MultiLineFunctionBodyStatements = 1,
ClassHeritageClauses = 0,
ClassMembers = 65,
InterfaceMembers = 65,
EnumMembers = 81,
CaseBlockClauses = 65,
NamedImportsOrExportsElements = 262576,
JsxElementOrFragmentChildren = 131072,
JsxElementAttributes = 131328,
CaseOrDefaultClauseStatements = 81985,
HeritageClauseTypes = 272,
SourceFileStatements = 65537,
Decorators = 24577,
TypeArguments = 26896,
TypeParameters = 26896,
Parameters = 1296,
IndexSignatureParameters = 4432
ClassMembers = 129,
InterfaceMembers = 129,
EnumMembers = 145,
CaseBlockClauses = 129,
NamedImportsOrExportsElements = 525136,
JsxElementOrFragmentChildren = 262144,
JsxElementAttributes = 262656,
CaseOrDefaultClauseStatements = 163969,
HeritageClauseTypes = 528,
SourceFileStatements = 131073,
Decorators = 49153,
TypeArguments = 53776,
TypeParameters = 53776,
Parameters = 2576,
IndexSignatureParameters = 8848,
JSDocComment = 33
}
interface UserPreferences {
readonly disableSuggestions?: boolean;
+55 -53
View File
@@ -1573,7 +1573,7 @@ declare namespace ts {
}
interface JSDocTemplateTag extends JSDocTag {
kind: SyntaxKind.JSDocTemplateTag;
constraint: TypeNode | undefined;
constraint: JSDocTypeExpression | undefined;
typeParameters: NodeArray<TypeParameterDeclaration>;
}
interface JSDocReturnTag extends JSDocTag {
@@ -2942,60 +2942,62 @@ declare namespace ts {
BarDelimited = 4,
AmpersandDelimited = 8,
CommaDelimited = 16,
DelimitersMask = 28,
AllowTrailingComma = 32,
Indented = 64,
SpaceBetweenBraces = 128,
SpaceBetweenSiblings = 256,
Braces = 512,
Parenthesis = 1024,
AngleBrackets = 2048,
SquareBrackets = 4096,
BracketsMask = 7680,
OptionalIfUndefined = 8192,
OptionalIfEmpty = 16384,
Optional = 24576,
PreferNewLine = 32768,
NoTrailingNewLine = 65536,
NoInterveningComments = 131072,
NoSpaceIfEmpty = 262144,
SingleElement = 524288,
Modifiers = 131328,
HeritageClauses = 256,
SingleLineTypeLiteralMembers = 384,
MultiLineTypeLiteralMembers = 16449,
TupleTypeElements = 272,
UnionTypeConstituents = 260,
IntersectionTypeConstituents = 264,
ObjectBindingPatternElements = 262576,
ArrayBindingPatternElements = 262448,
ObjectLiteralExpressionProperties = 263122,
ArrayLiteralExpressionElements = 4466,
CommaListElements = 272,
CallExpressionArguments = 1296,
NewExpressionArguments = 9488,
TemplateExpressionSpans = 131072,
SingleLineBlockStatements = 384,
MultiLineBlockStatements = 65,
VariableDeclarationList = 272,
SingleLineFunctionBodyStatements = 384,
AsteriskDelimited = 32,
DelimitersMask = 60,
AllowTrailingComma = 64,
Indented = 128,
SpaceBetweenBraces = 256,
SpaceBetweenSiblings = 512,
Braces = 1024,
Parenthesis = 2048,
AngleBrackets = 4096,
SquareBrackets = 8192,
BracketsMask = 15360,
OptionalIfUndefined = 16384,
OptionalIfEmpty = 32768,
Optional = 49152,
PreferNewLine = 65536,
NoTrailingNewLine = 131072,
NoInterveningComments = 262144,
NoSpaceIfEmpty = 524288,
SingleElement = 1048576,
Modifiers = 262656,
HeritageClauses = 512,
SingleLineTypeLiteralMembers = 768,
MultiLineTypeLiteralMembers = 32897,
TupleTypeElements = 528,
UnionTypeConstituents = 516,
IntersectionTypeConstituents = 520,
ObjectBindingPatternElements = 525136,
ArrayBindingPatternElements = 524880,
ObjectLiteralExpressionProperties = 526226,
ArrayLiteralExpressionElements = 8914,
CommaListElements = 528,
CallExpressionArguments = 2576,
NewExpressionArguments = 18960,
TemplateExpressionSpans = 262144,
SingleLineBlockStatements = 768,
MultiLineBlockStatements = 129,
VariableDeclarationList = 528,
SingleLineFunctionBodyStatements = 768,
MultiLineFunctionBodyStatements = 1,
ClassHeritageClauses = 0,
ClassMembers = 65,
InterfaceMembers = 65,
EnumMembers = 81,
CaseBlockClauses = 65,
NamedImportsOrExportsElements = 262576,
JsxElementOrFragmentChildren = 131072,
JsxElementAttributes = 131328,
CaseOrDefaultClauseStatements = 81985,
HeritageClauseTypes = 272,
SourceFileStatements = 65537,
Decorators = 24577,
TypeArguments = 26896,
TypeParameters = 26896,
Parameters = 1296,
IndexSignatureParameters = 4432
ClassMembers = 129,
InterfaceMembers = 129,
EnumMembers = 145,
CaseBlockClauses = 129,
NamedImportsOrExportsElements = 525136,
JsxElementOrFragmentChildren = 262144,
JsxElementAttributes = 262656,
CaseOrDefaultClauseStatements = 163969,
HeritageClauseTypes = 528,
SourceFileStatements = 131073,
Decorators = 49153,
TypeArguments = 53776,
TypeParameters = 53776,
Parameters = 2576,
IndexSignatureParameters = 8848,
JSDocComment = 33
}
interface UserPreferences {
readonly disableSuggestions?: boolean;
@@ -0,0 +1,95 @@
/// <reference path='fourslash.ts' />
// @allowJs: true
// @checkJs: true
// @noImplicitAny: true
// @strictNullChecks: false
// @Filename: important.js
/////** @param x no types here! */
/////**
//// * 1
//// * @param x a duplicate!
//// * @param y yy
//// */
/////**
//// * 2
//// * @param z zz
//// */
////function f(x) {
//// return x * 1
////}
////
////var o = {
//// /** 1
//// * @return First one
//// */
//// // intrusive comment (should not be deleted)
//// /** 2
//// * @see also
//// */
//// /** 3
//// * @return Second one
//// * @extends {C<number>} nothing really
//// * @class
//// * @this {*} doesn't make sense here
//// * @enum wat
//// */
//// /**
//// * @typedef {number} Meter or something
//// * @typedef {Object} Position Comment!
//// * @property {number} x what a horrible place for a type
//// * @property {number} y please don't do this
//// */
//// /**
//// * @template {string} T postfix comment
//// * @callback Action not sure what this will do
//// * @param {T} thing
//// * @returns {string} oh no
//// */
//// get m() { return undefined }
////}
////o.m = 1
verify.codeFixAll({
fixId: "inferFromUsage",
fixAllDescription: "Infer all types from usage",
newFileContent:
`/**
* 1
* 2
* @param {number} x no types here!
* @param x a duplicate!
* @param y yy
* @param z zz
*/
function f(x) {
return x * 1
}
var o = {
// intrusive comment (should not be deleted)
/**
* 1
* 2
* 3
* @returns {number} First one
* @see also
* @return Second one
* @extends {C<number>} nothing really
* @class
* @this {*} doesn't make sense here
* @enum {wat}
* @typedef {number} Meter or something
* @typedef {Object} Position Comment!
* @property {number} x what a horrible place for a type
* @property {number} y please don't do this
* @template {string} T postfix comment
* @callback Action not sure what this will do
* @param {T} thing
* @returns {string} oh no
*/
get m() { return undefined }
}
o.m = 1`,
});
@@ -7,26 +7,34 @@
// @Filename: important.js
////class C {
//// constructor() {
//// [|this.p|] = undefined;
//// /** this is fine */
//// this.p = undefined;
//// this.q = undefined
//// }
//// method() {
//// this.p.push(1)
//// this.q.push(1);
//// }
////}
// Note: Should be number[] | undefined, but inference currently privileges assignments
// over usage (even when the only result is undefined) and infers only undefined.
verify.codeFix({
description: "Infer type of 'p' from usage",
index: 2,
verify.codeFixAll({
fixId: "inferFromUsage",
fixAllDescription: "Infer all types from usage",
newFileContent:
`class C {
constructor() {
/** @type {undefined} */
/**
* this is fine
* @type {undefined}
*/
this.p = undefined;
/** @type {undefined} */
this.q = undefined
}
method() {
this.p.push(1)
this.q.push(1);
}
}`
});
}`});
@@ -19,8 +19,6 @@ verify.codeFix({
newFileContent:
`/**
* @param {*} y
*/
/**
* @param {number} x
* @param {number} z
*/
@@ -17,8 +17,8 @@ verify.codeFix({
description: "Infer parameter types from usage",
index: 2,
newFileContent:
`/** @param {number} a */
/**
`/**
* @param {number} a
* @param {(string | boolean)[]} rest
*/
function f(a, ...rest){
@@ -14,8 +14,8 @@ verify.codeFix({
description: "Infer parameter types from usage",
index: 2,
newFileContent:
`/** @param {number} a */
/**
`/**
* @param {number} a
* @param {number[]} rest
*/
function f(a, ...rest){
@@ -17,8 +17,8 @@ verify.codeFix({
description: "Infer parameter types from usage",
index: 4,
newFileContent:
`/** @param {number} a */
/**
`/**
* @param {number} a
* @param {string[]} rest
*/
function f(a: number, ...rest){
@@ -14,9 +14,9 @@ verify.codeFix({
index: 2,
newFileContent:
`class C {/**
* @param {number} x
*/
m(x) {return x;}}
* @param {number} x
*/
m(x) {return x;}}
var c = new C()
c.m(1)`,
});
@@ -0,0 +1,29 @@
/// <reference path="fourslash.ts" />
/////**
//// * JSDoc for things
//// */
////function f() {
//// /** more
//// jsdoc */
//// var t;
//// /**
//// * multiline
//// */
//// var multiline;
////}
format.document();
verify.currentFileContentIs(`/**
* JSDoc for things
*/
function f() {
/** more
jsdoc */
var t;
/**
* multiline
*/
var multiline;
}`);
@@ -1,9 +1,9 @@
/// <reference path="fourslash.ts" />
////
////// whitespace below
////// 1 below
////
////// whitespace above
////// 2 above
////
////let x;
////
@@ -11,25 +11,25 @@
////
////let y;
////
////// whitespace above again
////// 3 above
////
////while (true) {
//// while (true) {
//// }
////
//// // whitespace above
//// // 4 above
////}
////
////// whitespace above again
////// 5 above
////
////
format.document();
verify.currentFileContentIs(`
// whitespace below
// 1 below
// whitespace above
// 2 above
let x;
@@ -37,15 +37,15 @@ let x;
let y;
// whitespace above again
// 3 above
while (true) {
while (true) {
}
// whitespace above
// 4 above
}
// whitespace above again
// 5 above
`);
@@ -2,12 +2,12 @@
////function foo(a,
//// /*2*/b,/*0*/
//// //comment/*3*/
//// //ABC/*3*/
//// /*4*/c
//// ) {
////};
////var x = [
//// /*5*///comment/*1*/
//// /*5*///DEF/*1*/
//// 1,/*6*/
//// 2/*7*/
////]
@@ -17,14 +17,14 @@ verify.indentationIs(4);
goTo.marker("2");
verify.currentLineContentIs(" b,");
goTo.marker("3");
verify.currentLineContentIs(" //comment");
verify.currentLineContentIs(" //ABC");
goTo.marker("4");
verify.currentLineContentIs(" c");
goTo.marker("1");
edit.insert("\n");
verify.indentationIs(4);
goTo.marker("5");
verify.currentLineContentIs(" //comment");
verify.currentLineContentIs(" //DEF");
goTo.marker("6");
verify.currentLineContentIs(" 1,");
goTo.marker("7");