Detect semicolons in file before writing quick fixes

This commit is contained in:
Andrew Branch 2019-06-06 10:49:09 -07:00
parent 7815778fb0
commit b37875ff7a
No known key found for this signature in database
GPG key ID: 22CCA4B120C427D2
2 changed files with 177 additions and 104 deletions

View file

@ -834,9 +834,10 @@ namespace ts.textChanges {
/** Note: output node may be mutated input node. */ /** Note: output node may be mutated input node. */
export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } { export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } {
const writer = new Writer(newLineCharacter); const omitTrailingSemicolon = !!sourceFile && !probablyUsesSemicolons(sourceFile);
const writer = createWriter(newLineCharacter, omitTrailingSemicolon);
const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed; const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed;
createPrinter({ newLine, neverAsciiEscape: true }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer); createPrinter({ newLine, neverAsciiEscape: true, omitTrailingSemicolon }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer);
return { text: writer.getText(), node: assignPositionsToNode(node) }; return { text: writer.getText(), node: assignPositionsToNode(node) };
} }
} }
@ -874,143 +875,168 @@ namespace ts.textChanges {
return nodeArray; return nodeArray;
} }
class Writer implements EmitTextWriter, PrintHandlers { interface TextChangesWriter extends EmitTextWriter, PrintHandlers {}
private lastNonTriviaPosition = 0;
private readonly writer: EmitTextWriter;
public readonly onEmitNode: PrintHandlers["onEmitNode"]; function createWriter(newLine: string, omitTrailingSemicolon?: boolean): TextChangesWriter {
public readonly onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"]; let lastNonTriviaPosition = 0;
public readonly onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"];
public readonly onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"];
public readonly onAfterEmitToken: PrintHandlers["onAfterEmitToken"];
constructor(newLine: string) {
this.writer = createTextWriter(newLine);
this.onEmitNode = (hint, node, printCallback) => {
if (node) {
setPos(node, this.lastNonTriviaPosition);
}
printCallback(hint, node);
if (node) {
setEnd(node, this.lastNonTriviaPosition);
}
};
this.onBeforeEmitNodeArray = nodes => {
if (nodes) {
setPos(nodes, this.lastNonTriviaPosition);
}
};
this.onAfterEmitNodeArray = nodes => {
if (nodes) {
setEnd(nodes, this.lastNonTriviaPosition);
}
};
this.onBeforeEmitToken = node => {
if (node) {
setPos(node, this.lastNonTriviaPosition);
}
};
this.onAfterEmitToken = node => {
if (node) {
setEnd(node, this.lastNonTriviaPosition);
}
};
}
private setLastNonTriviaPosition(s: string, force: boolean) { const writer = omitTrailingSemicolon ? getTrailingSemicolonOmittingWriter(createTextWriter(newLine)) : createTextWriter(newLine);
const onEmitNode: PrintHandlers["onEmitNode"] = (hint, node, printCallback) => {
if (node) {
setPos(node, lastNonTriviaPosition);
}
printCallback(hint, node);
if (node) {
setEnd(node, lastNonTriviaPosition);
}
};
const onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"] = nodes => {
if (nodes) {
setPos(nodes, lastNonTriviaPosition);
}
};
const onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"] = nodes => {
if (nodes) {
setEnd(nodes, lastNonTriviaPosition);
}
};
const onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"] = node => {
if (node) {
setPos(node, lastNonTriviaPosition);
}
};
const onAfterEmitToken: PrintHandlers["onAfterEmitToken"] = node => {
if (node) {
setEnd(node, lastNonTriviaPosition);
}
};
function setLastNonTriviaPosition(s: string, force: boolean) {
if (force || !isTrivia(s)) { if (force || !isTrivia(s)) {
this.lastNonTriviaPosition = this.writer.getTextPos(); lastNonTriviaPosition = writer.getTextPos();
let i = 0; let i = 0;
while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) { while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) {
i++; i++;
} }
// trim trailing whitespaces // trim trailing whitespaces
this.lastNonTriviaPosition -= i; lastNonTriviaPosition -= i;
} }
} }
write(s: string): void { function write(s: string): void {
this.writer.write(s); writer.write(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeComment(s: string): void { function writeComment(s: string): void {
this.writer.writeComment(s); writer.writeComment(s);
} }
writeKeyword(s: string): void { function writeKeyword(s: string): void {
this.writer.writeKeyword(s); writer.writeKeyword(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeOperator(s: string): void { function writeOperator(s: string): void {
this.writer.writeOperator(s); writer.writeOperator(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writePunctuation(s: string): void { function writePunctuation(s: string): void {
this.writer.writePunctuation(s); writer.writePunctuation(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeTrailingSemicolon(s: string): void { function writeTrailingSemicolon(s: string): void {
this.writer.writeTrailingSemicolon(s); writer.writeTrailingSemicolon(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeParameter(s: string): void { function writeParameter(s: string): void {
this.writer.writeParameter(s); writer.writeParameter(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeProperty(s: string): void { function writeProperty(s: string): void {
this.writer.writeProperty(s); writer.writeProperty(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeSpace(s: string): void { function writeSpace(s: string): void {
this.writer.writeSpace(s); writer.writeSpace(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeStringLiteral(s: string): void { function writeStringLiteral(s: string): void {
this.writer.writeStringLiteral(s); writer.writeStringLiteral(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeSymbol(s: string, sym: Symbol): void { function writeSymbol(s: string, sym: Symbol): void {
this.writer.writeSymbol(s, sym); writer.writeSymbol(s, sym);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeLine(): void { function writeLine(): void {
this.writer.writeLine(); writer.writeLine();
} }
increaseIndent(): void { function increaseIndent(): void {
this.writer.increaseIndent(); writer.increaseIndent();
} }
decreaseIndent(): void { function decreaseIndent(): void {
this.writer.decreaseIndent(); writer.decreaseIndent();
} }
getText(): string { function getText(): string {
return this.writer.getText(); return writer.getText();
} }
rawWrite(s: string): void { function rawWrite(s: string): void {
this.writer.rawWrite(s); writer.rawWrite(s);
this.setLastNonTriviaPosition(s, /*force*/ false); setLastNonTriviaPosition(s, /*force*/ false);
} }
writeLiteral(s: string): void { function writeLiteral(s: string): void {
this.writer.writeLiteral(s); writer.writeLiteral(s);
this.setLastNonTriviaPosition(s, /*force*/ true); setLastNonTriviaPosition(s, /*force*/ true);
} }
getTextPos(): number { function getTextPos(): number {
return this.writer.getTextPos(); return writer.getTextPos();
} }
getLine(): number { function getLine(): number {
return this.writer.getLine(); return writer.getLine();
} }
getColumn(): number { function getColumn(): number {
return this.writer.getColumn(); return writer.getColumn();
} }
getIndent(): number { function getIndent(): number {
return this.writer.getIndent(); return writer.getIndent();
} }
isAtStartOfLine(): boolean { function isAtStartOfLine(): boolean {
return this.writer.isAtStartOfLine(); return writer.isAtStartOfLine();
} }
clear(): void { function clear(): void {
this.writer.clear(); writer.clear();
this.lastNonTriviaPosition = 0; lastNonTriviaPosition = 0;
} }
return {
onEmitNode,
onBeforeEmitNodeArray,
onAfterEmitNodeArray,
onBeforeEmitToken,
onAfterEmitToken,
write,
writeComment,
writeKeyword,
writeOperator,
writePunctuation,
writeTrailingSemicolon,
writeParameter,
writeProperty,
writeSpace,
writeStringLiteral,
writeSymbol,
writeLine,
increaseIndent,
decreaseIndent,
getText,
rawWrite,
writeLiteral,
getTextPos,
getLine,
getColumn,
getIndent,
isAtStartOfLine,
clear
};
} }
function getInsertionPositionAtSourceFileTop(sourceFile: SourceFile): number { function getInsertionPositionAtSourceFileTop(sourceFile: SourceFile): number {

View file

@ -1991,4 +1991,51 @@ namespace ts {
}); });
return typeIsAccessible ? res : undefined; return typeIsAccessible ? res : undefined;
} }
export function syntaxUsuallyHasTrailingSemicolon(kind: SyntaxKind) {
return kind === SyntaxKind.VariableStatement
|| kind === SyntaxKind.ExpressionStatement
|| kind === SyntaxKind.DoStatement
|| kind === SyntaxKind.ContinueStatement
|| kind === SyntaxKind.BreakStatement
|| kind === SyntaxKind.ReturnStatement
|| kind === SyntaxKind.ThrowStatement
|| kind === SyntaxKind.DebuggerStatement
|| kind === SyntaxKind.PropertyDeclaration
|| kind === SyntaxKind.TypeAliasDeclaration
|| kind === SyntaxKind.ImportDeclaration
|| kind === SyntaxKind.ImportEqualsDeclaration
|| kind === SyntaxKind.ExportDeclaration;
}
export function probablyUsesSemicolons(sourceFile: SourceFile): boolean {
let withSemicolon = 0;
let withoutSemicolon = 0;
const nStatementsToObserve = 5;
forEachChild(sourceFile, function visit(node): boolean | undefined {
if (syntaxUsuallyHasTrailingSemicolon(node.kind)) {
const lastToken = node.getLastToken(sourceFile);
if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) {
withSemicolon++;
}
else {
withoutSemicolon++;
}
}
if (withSemicolon + withoutSemicolon >= nStatementsToObserve) {
return true;
}
return forEachChild(node, visit);
});
// One statement missing a semicolon isnt sufficient evidence to say the user
// doesnt want semicolons, because they may not even be done writing that statement.
if (withSemicolon === 0 && withoutSemicolon <= 1) {
return true;
}
// If even 2/5 places have a semicolon, the user probably wants semicolons
return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve;
}
} }