Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect semicolons before writing from TextChanges #31801

Merged
merged 4 commits into from
Jul 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 154 additions & 128 deletions src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,9 +834,10 @@ namespace ts.textChanges {

/** Note: output node may be mutated input 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;
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) };
}
}
Expand Down Expand Up @@ -874,143 +875,168 @@ namespace ts.textChanges {
return nodeArray;
}

class Writer implements EmitTextWriter, PrintHandlers {
private lastNonTriviaPosition = 0;
private readonly writer: EmitTextWriter;

public readonly onEmitNode: PrintHandlers["onEmitNode"];
public readonly onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"];
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);
}
};
}
interface TextChangesWriter extends EmitTextWriter, PrintHandlers {}

function createWriter(newLine: string, omitTrailingSemicolon?: boolean): TextChangesWriter {
let lastNonTriviaPosition = 0;


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)) {
this.lastNonTriviaPosition = this.writer.getTextPos();
lastNonTriviaPosition = writer.getTextPos();
let i = 0;
while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) {
i++;
}
// trim trailing whitespaces
this.lastNonTriviaPosition -= i;
lastNonTriviaPosition -= i;
}
}

write(s: string): void {
this.writer.write(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeComment(s: string): void {
this.writer.writeComment(s);
}
writeKeyword(s: string): void {
this.writer.writeKeyword(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeOperator(s: string): void {
this.writer.writeOperator(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writePunctuation(s: string): void {
this.writer.writePunctuation(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeTrailingSemicolon(s: string): void {
this.writer.writeTrailingSemicolon(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeParameter(s: string): void {
this.writer.writeParameter(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeProperty(s: string): void {
this.writer.writeProperty(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeSpace(s: string): void {
this.writer.writeSpace(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeStringLiteral(s: string): void {
this.writer.writeStringLiteral(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeSymbol(s: string, sym: Symbol): void {
this.writer.writeSymbol(s, sym);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeLine(): void {
this.writer.writeLine();
}
increaseIndent(): void {
this.writer.increaseIndent();
}
decreaseIndent(): void {
this.writer.decreaseIndent();
}
getText(): string {
return this.writer.getText();
}
rawWrite(s: string): void {
this.writer.rawWrite(s);
this.setLastNonTriviaPosition(s, /*force*/ false);
}
writeLiteral(s: string): void {
this.writer.writeLiteral(s);
this.setLastNonTriviaPosition(s, /*force*/ true);
}
getTextPos(): number {
return this.writer.getTextPos();
}
getLine(): number {
return this.writer.getLine();
}
getColumn(): number {
return this.writer.getColumn();
}
getIndent(): number {
return this.writer.getIndent();
}
isAtStartOfLine(): boolean {
return this.writer.isAtStartOfLine();
}
clear(): void {
this.writer.clear();
this.lastNonTriviaPosition = 0;
function write(s: string): void {
writer.write(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeComment(s: string): void {
writer.writeComment(s);
}
function writeKeyword(s: string): void {
writer.writeKeyword(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeOperator(s: string): void {
writer.writeOperator(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writePunctuation(s: string): void {
writer.writePunctuation(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeTrailingSemicolon(s: string): void {
writer.writeTrailingSemicolon(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeParameter(s: string): void {
writer.writeParameter(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeProperty(s: string): void {
writer.writeProperty(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeSpace(s: string): void {
writer.writeSpace(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeStringLiteral(s: string): void {
writer.writeStringLiteral(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeSymbol(s: string, sym: Symbol): void {
writer.writeSymbol(s, sym);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeLine(): void {
writer.writeLine();
}
function increaseIndent(): void {
writer.increaseIndent();
}
function decreaseIndent(): void {
writer.decreaseIndent();
}
function getText(): string {
return writer.getText();
}
function rawWrite(s: string): void {
writer.rawWrite(s);
setLastNonTriviaPosition(s, /*force*/ false);
}
function writeLiteral(s: string): void {
writer.writeLiteral(s);
setLastNonTriviaPosition(s, /*force*/ true);
}
function getTextPos(): number {
return writer.getTextPos();
}
function getLine(): number {
return writer.getLine();
}
function getColumn(): number {
return writer.getColumn();
}
function getIndent(): number {
return writer.getIndent();
}
function isAtStartOfLine(): boolean {
return writer.isAtStartOfLine();
}
function clear(): void {
writer.clear();
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 {
Expand Down
47 changes: 47 additions & 0 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1991,4 +1991,51 @@ namespace ts {
});
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 isn’t sufficient evidence to say the user
// doesn’t 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;
}
}
Loading