From 671029c86b6681d9d113579d968d217ee14212d1 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Tue, 19 May 2020 11:22:23 -0400 Subject: [PATCH 1/2] Add semantic highlighting pt1 --- src/harness/client.ts | 16 +- src/services/semanticClassification.ts | 237 +++++++++++++++++++++++++ src/services/tsconfig.json | 1 + 3 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 src/services/semanticClassification.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 83e85cbc9a328..c6d612c5122c4 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -727,20 +727,28 @@ namespace ts.server { return notImplemented(); } + /** @deprecated Use getEncodedSyntacticClassifications instead. */ getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { return notImplemented(); } + /** @deprecated Use getEncodedSemanticClassifications instead. */ getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); + return notImplemented(); } getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); + return { + spans: [], + endOfLineState: EndOfLineState.None + }; } - getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); + getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications { + return { + spans: Semantic.getSemanticTokens(this, fileName, span), + endOfLineState: EndOfLineState.None + }; } private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { diff --git a/src/services/semanticClassification.ts b/src/services/semanticClassification.ts new file mode 100644 index 0000000000000..cb1ddb13734fe --- /dev/null +++ b/src/services/semanticClassification.ts @@ -0,0 +1,237 @@ +/* @internal */ +namespace ts.Semantic { + export const enum TokenType { + class, enum, interface, namespace, typeParameter, type, parameter, variable, enumMember, property, function, member, _ + } + + export const enum TokenModifier { + declaration, static, async, readonly, defaultLibrary, local, _ + } + + export const enum TokenEncodingConsts { + typeOffset = 8, + modifierMask = (1 << typeOffset) - 1 + } + + export declare const enum VersionRequirement { + major = 3, + minor = 7 + } + + export function getSemanticTokens(jsLanguageService: LanguageService, fileName: string, span: TextSpan): number[] { + const resultTokens: number[] = []; + + const collector = (node: Node, typeIdx: number, modifierSet: number) => { + resultTokens.push(node.getStart(), node.getWidth(), ((typeIdx + 1) << TokenEncodingConsts.typeOffset) + modifierSet); + }; + const program = jsLanguageService.getProgram(); + if (program) { + const sourceFile = program.getSourceFile(fileName); + if (sourceFile) { + collectTokens(program, sourceFile, span, collector); + } + } + return resultTokens; + } + + function collectTokens(program: Program, sourceFile: SourceFile, span: TextSpan, collector: (node: Node, tokenType: number, tokenModifier: number) => void) { + const typeChecker = program.getTypeChecker(); + + let inJSXElement = false; + + function visit(node: Node) { + if (!node || !textSpanIntersectsWith(span, node.pos, node.getFullWidth()) || node.getFullWidth() === 0) { + return; + } + const prevInJSXElement = inJSXElement; + if (isJsxElement(node) || isJsxSelfClosingElement(node)) { + inJSXElement = true; + } + if (isJsxExpression(node)) { + inJSXElement = false; + } + + if (isIdentifier(node) && !inJSXElement && !inImportClause(node)) { + let symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + if (symbol.flags & SymbolFlags.Alias) { + symbol = typeChecker.getAliasedSymbol(symbol); + } + let typeIdx = classifySymbol(symbol, getMeaningFromLocation(node)); + if (typeIdx !== undefined) { + let modifierSet = 0; + if (node.parent) { + const parentIsDeclaration = (isBindingElement(node.parent) || tokenFromDeclarationMapping[node.parent.kind] === typeIdx); + if (parentIsDeclaration && (node.parent).name === node) { + modifierSet = 1 << TokenModifier.declaration; + } + } + + // property declaration in constructor + if (typeIdx === TokenType.parameter && isRightSideOfQualifiedNameOrPropertyAccess(node)) { + typeIdx = TokenType.property; + } + + typeIdx = reclassifyByType(typeChecker, node, typeIdx); + + const decl = symbol.valueDeclaration; + if (decl) { + const modifiers = getCombinedModifierFlags(decl); + const nodeFlags = getCombinedNodeFlags(decl); + if (modifiers & ModifierFlags.Static) { + modifierSet |= 1 << TokenModifier.static; + } + if (modifiers & ModifierFlags.Async) { + modifierSet |= 1 << TokenModifier.async; + } + if (typeIdx !== TokenType.class && typeIdx !== TokenType.interface) { + if ((modifiers & ModifierFlags.Readonly) || (nodeFlags & NodeFlags.Const) || (symbol.getFlags() & SymbolFlags.EnumMember)) { + modifierSet |= 1 << TokenModifier.readonly; + } + } + if ((typeIdx === TokenType.variable || typeIdx === TokenType.function) && isLocalDeclaration(decl, sourceFile)) { + modifierSet |= 1 << TokenModifier.local; + } + if (program.isSourceFileDefaultLibrary(decl.getSourceFile())) { + modifierSet |= 1 << TokenModifier.defaultLibrary; + } + } + else if (symbol.declarations && symbol.declarations.some(d => program.isSourceFileDefaultLibrary(d.getSourceFile()))) { + modifierSet |= 1 << TokenModifier.defaultLibrary; + } + + collector(node, typeIdx, modifierSet); + + } + } + } + forEachChild(node, visit); + + inJSXElement = prevInJSXElement; + } + visit(sourceFile); + } + + function classifySymbol(symbol: Symbol, meaning: SemanticMeaning): TokenType | undefined { + const flags = symbol.getFlags(); + if (flags & SymbolFlags.Class) { + return TokenType.class; + } + else if (flags & SymbolFlags.Enum) { + return TokenType.enum; + } + else if (flags & SymbolFlags.TypeAlias) { + return TokenType.type; + } + else if (flags & SymbolFlags.Interface) { + if (meaning & SemanticMeaning.Type) { + return TokenType.interface; + } + } + else if (flags & SymbolFlags.TypeParameter) { + return TokenType.typeParameter; + } + let decl = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; + if (decl && isBindingElement(decl)) { + decl = getDeclarationForBindingElement(decl); + } + return decl && tokenFromDeclarationMapping[decl.kind]; + } + + function reclassifyByType(typeChecker: TypeChecker, node: Node, typeIdx: TokenType): TokenType { + // type based classifications + if (typeIdx === TokenType.variable || typeIdx === TokenType.property || typeIdx === TokenType.parameter) { + const type = typeChecker.getTypeAtLocation(node); + if (type) { + const test = (condition: (type: Type) => boolean) => { + return condition(type) || type.isUnion() && type.types.some(condition); + }; + if (typeIdx !== TokenType.parameter && test(t => t.getConstructSignatures().length > 0)) { + return TokenType.class; + } + if (test(t => t.getCallSignatures().length > 0) && !test(t => t.getProperties().length > 0) || isExpressionInCallExpression(node)) { + return typeIdx === TokenType.property ? TokenType.member : TokenType.function; + } + } + } + return typeIdx; + } + + function isLocalDeclaration(decl: Declaration, sourceFile: SourceFile): boolean { + if (isBindingElement(decl)) { + decl = getDeclarationForBindingElement(decl); + } + if (isVariableDeclaration(decl)) { + return (!isSourceFile(decl.parent.parent.parent) || isCatchClause(decl.parent)) && decl.getSourceFile() === sourceFile; + } + else if (isFunctionDeclaration(decl)) { + return !isSourceFile(decl.parent) && decl.getSourceFile() === sourceFile; + } + return false; + } + + function getDeclarationForBindingElement(element: BindingElement): VariableDeclaration | ParameterDeclaration { + while (true) { + if (isBindingElement(element.parent.parent)) { + element = element.parent.parent; + } + else { + return element.parent.parent; + } + } + } + + function inImportClause(node: Node): boolean { + const parent = node.parent; + return parent && (isImportClause(parent) || isImportSpecifier(parent) || isNamespaceImport(parent)); + } + + function isExpressionInCallExpression(node: Node): boolean { + while (isRightSideOfQualifiedNameOrPropertyAccess(node)) { + node = node.parent; + } + return isCallExpression(node.parent) && node.parent.expression === node; + } + + function isRightSideOfQualifiedNameOrPropertyAccess(node: Node): boolean { + return (isQualifiedName(node.parent) && node.parent.right === node) || (isPropertyAccessExpression(node.parent) && node.parent.name === node); + } + + const enum SemanticMeaning { + None = 0x0, + Value = 0x1, + Type = 0x2, + Namespace = 0x4, + All = Value | Type | Namespace + } + + function getMeaningFromLocation(node: Node): SemanticMeaning { + const f = (ts).getMeaningFromLocation; + if (typeof f === "function") { + return f(node); + } + return SemanticMeaning.All; + } + + const tokenFromDeclarationMapping: { [name: string]: TokenType } = { + [SyntaxKind.VariableDeclaration]: TokenType.variable, + [SyntaxKind.Parameter]: TokenType.parameter, + [SyntaxKind.PropertyDeclaration]: TokenType.property, + [SyntaxKind.ModuleDeclaration]: TokenType.namespace, + [SyntaxKind.EnumDeclaration]: TokenType.enum, + [SyntaxKind.EnumMember]: TokenType.enumMember, + [SyntaxKind.ClassDeclaration]: TokenType.class, + [SyntaxKind.MethodDeclaration]: TokenType.member, + [SyntaxKind.FunctionDeclaration]: TokenType.function, + [SyntaxKind.FunctionExpression]: TokenType.function, + [SyntaxKind.MethodSignature]: TokenType.member, + [SyntaxKind.GetAccessor]: TokenType.property, + [SyntaxKind.SetAccessor]: TokenType.property, + [SyntaxKind.PropertySignature]: TokenType.property, + [SyntaxKind.InterfaceDeclaration]: TokenType.interface, + [SyntaxKind.TypeAliasDeclaration]: TokenType.type, + [SyntaxKind.TypeParameter]: TokenType.typeParameter, + [SyntaxKind.PropertyAssignment]: TokenType.property, + [SyntaxKind.ShorthandPropertyAssignment]: TokenType.property + }; +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index bbe3956e173c1..0d9b0668847e2 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -31,6 +31,7 @@ "preProcess.ts", "rename.ts", "smartSelection.ts", + "semanticClassification.ts", "signatureHelp.ts", "sourcemaps.ts", "suggestionDiagnostics.ts", From 618b4d58bedeaeec527b9ed41f5c2ef447ca0a6a Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Wed, 17 Jun 2020 07:09:54 -0400 Subject: [PATCH 2/2] Make the public API unit tests echo out a diff --- package.json | 1 + src/harness/harnessIO.ts | 14 +- src/services/semanticClassification.ts | 237 ------------------------- src/services/types.ts | 3 +- src/testRunner/unittests/publicApi.ts | 2 +- 5 files changed, 15 insertions(+), 242 deletions(-) delete mode 100644 src/services/semanticClassification.ts diff --git a/package.json b/package.json index 7fc410ad60c40..4f4ae07a4f736 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "chalk": "latest", "convert-source-map": "latest", "del": "5.1.0", + "diff": "^4.0.2", "eslint": "6.8.0", "eslint-formatter-autolinkable-stylish": "1.1.2", "eslint-plugin-import": "2.20.2", diff --git a/src/harness/harnessIO.ts b/src/harness/harnessIO.ts index 47a744de2082d..a7c3be83048a6 100644 --- a/src/harness/harnessIO.ts +++ b/src/harness/harnessIO.ts @@ -1293,6 +1293,7 @@ namespace Harness { export interface BaselineOptions { Subfolder?: string; Baselinefolder?: string; + PrintDiff?: true; } export function localPath(fileName: string, baselineFolder?: string, subfolder?: string) { @@ -1347,7 +1348,7 @@ namespace Harness { return { expected, actual }; } - function writeComparison(expected: string, actual: string, relativeFileName: string, actualFileName: string) { + function writeComparison(expected: string, actual: string, relativeFileName: string, actualFileName: string, opts?: BaselineOptions) { // For now this is written using TypeScript, because sys is not available when running old test cases. // But we need to move to sys once we have // Creates the directory including its parent if not already present @@ -1381,7 +1382,14 @@ namespace Harness { else { IO.writeFile(actualFileName, encodedActual); } - throw new Error(`The baseline file ${relativeFileName} has changed.`); + if (require && opts && opts.PrintDiff) { + const Diff = require("diff"); + const patch = Diff.createTwoFilesPatch("Expected", "Actual", expected, actual, "The current baseline", "The new version"); + throw new Error(`The baseline file ${relativeFileName} has changed.\n\n${patch}`); + } + else { + throw new Error(`The baseline file ${relativeFileName} has changed.`); + } } } @@ -1391,7 +1399,7 @@ namespace Harness { throw new Error("The generated content was \"undefined\". Return \"null\" if no baselining is required.\""); } const comparison = compareToBaseline(actual, relativeFileName, opts); - writeComparison(comparison.expected, comparison.actual, relativeFileName, actualFileName); + writeComparison(comparison.expected, comparison.actual, relativeFileName, actualFileName, opts); } export function runMultifileBaseline(relativeFileBase: string, extension: string, generateContent: () => IterableIterator<[string, string, number]> | IterableIterator<[string, string]> | null, opts?: BaselineOptions, referencedExtensions?: string[]): void { diff --git a/src/services/semanticClassification.ts b/src/services/semanticClassification.ts deleted file mode 100644 index cb1ddb13734fe..0000000000000 --- a/src/services/semanticClassification.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* @internal */ -namespace ts.Semantic { - export const enum TokenType { - class, enum, interface, namespace, typeParameter, type, parameter, variable, enumMember, property, function, member, _ - } - - export const enum TokenModifier { - declaration, static, async, readonly, defaultLibrary, local, _ - } - - export const enum TokenEncodingConsts { - typeOffset = 8, - modifierMask = (1 << typeOffset) - 1 - } - - export declare const enum VersionRequirement { - major = 3, - minor = 7 - } - - export function getSemanticTokens(jsLanguageService: LanguageService, fileName: string, span: TextSpan): number[] { - const resultTokens: number[] = []; - - const collector = (node: Node, typeIdx: number, modifierSet: number) => { - resultTokens.push(node.getStart(), node.getWidth(), ((typeIdx + 1) << TokenEncodingConsts.typeOffset) + modifierSet); - }; - const program = jsLanguageService.getProgram(); - if (program) { - const sourceFile = program.getSourceFile(fileName); - if (sourceFile) { - collectTokens(program, sourceFile, span, collector); - } - } - return resultTokens; - } - - function collectTokens(program: Program, sourceFile: SourceFile, span: TextSpan, collector: (node: Node, tokenType: number, tokenModifier: number) => void) { - const typeChecker = program.getTypeChecker(); - - let inJSXElement = false; - - function visit(node: Node) { - if (!node || !textSpanIntersectsWith(span, node.pos, node.getFullWidth()) || node.getFullWidth() === 0) { - return; - } - const prevInJSXElement = inJSXElement; - if (isJsxElement(node) || isJsxSelfClosingElement(node)) { - inJSXElement = true; - } - if (isJsxExpression(node)) { - inJSXElement = false; - } - - if (isIdentifier(node) && !inJSXElement && !inImportClause(node)) { - let symbol = typeChecker.getSymbolAtLocation(node); - if (symbol) { - if (symbol.flags & SymbolFlags.Alias) { - symbol = typeChecker.getAliasedSymbol(symbol); - } - let typeIdx = classifySymbol(symbol, getMeaningFromLocation(node)); - if (typeIdx !== undefined) { - let modifierSet = 0; - if (node.parent) { - const parentIsDeclaration = (isBindingElement(node.parent) || tokenFromDeclarationMapping[node.parent.kind] === typeIdx); - if (parentIsDeclaration && (node.parent).name === node) { - modifierSet = 1 << TokenModifier.declaration; - } - } - - // property declaration in constructor - if (typeIdx === TokenType.parameter && isRightSideOfQualifiedNameOrPropertyAccess(node)) { - typeIdx = TokenType.property; - } - - typeIdx = reclassifyByType(typeChecker, node, typeIdx); - - const decl = symbol.valueDeclaration; - if (decl) { - const modifiers = getCombinedModifierFlags(decl); - const nodeFlags = getCombinedNodeFlags(decl); - if (modifiers & ModifierFlags.Static) { - modifierSet |= 1 << TokenModifier.static; - } - if (modifiers & ModifierFlags.Async) { - modifierSet |= 1 << TokenModifier.async; - } - if (typeIdx !== TokenType.class && typeIdx !== TokenType.interface) { - if ((modifiers & ModifierFlags.Readonly) || (nodeFlags & NodeFlags.Const) || (symbol.getFlags() & SymbolFlags.EnumMember)) { - modifierSet |= 1 << TokenModifier.readonly; - } - } - if ((typeIdx === TokenType.variable || typeIdx === TokenType.function) && isLocalDeclaration(decl, sourceFile)) { - modifierSet |= 1 << TokenModifier.local; - } - if (program.isSourceFileDefaultLibrary(decl.getSourceFile())) { - modifierSet |= 1 << TokenModifier.defaultLibrary; - } - } - else if (symbol.declarations && symbol.declarations.some(d => program.isSourceFileDefaultLibrary(d.getSourceFile()))) { - modifierSet |= 1 << TokenModifier.defaultLibrary; - } - - collector(node, typeIdx, modifierSet); - - } - } - } - forEachChild(node, visit); - - inJSXElement = prevInJSXElement; - } - visit(sourceFile); - } - - function classifySymbol(symbol: Symbol, meaning: SemanticMeaning): TokenType | undefined { - const flags = symbol.getFlags(); - if (flags & SymbolFlags.Class) { - return TokenType.class; - } - else if (flags & SymbolFlags.Enum) { - return TokenType.enum; - } - else if (flags & SymbolFlags.TypeAlias) { - return TokenType.type; - } - else if (flags & SymbolFlags.Interface) { - if (meaning & SemanticMeaning.Type) { - return TokenType.interface; - } - } - else if (flags & SymbolFlags.TypeParameter) { - return TokenType.typeParameter; - } - let decl = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; - if (decl && isBindingElement(decl)) { - decl = getDeclarationForBindingElement(decl); - } - return decl && tokenFromDeclarationMapping[decl.kind]; - } - - function reclassifyByType(typeChecker: TypeChecker, node: Node, typeIdx: TokenType): TokenType { - // type based classifications - if (typeIdx === TokenType.variable || typeIdx === TokenType.property || typeIdx === TokenType.parameter) { - const type = typeChecker.getTypeAtLocation(node); - if (type) { - const test = (condition: (type: Type) => boolean) => { - return condition(type) || type.isUnion() && type.types.some(condition); - }; - if (typeIdx !== TokenType.parameter && test(t => t.getConstructSignatures().length > 0)) { - return TokenType.class; - } - if (test(t => t.getCallSignatures().length > 0) && !test(t => t.getProperties().length > 0) || isExpressionInCallExpression(node)) { - return typeIdx === TokenType.property ? TokenType.member : TokenType.function; - } - } - } - return typeIdx; - } - - function isLocalDeclaration(decl: Declaration, sourceFile: SourceFile): boolean { - if (isBindingElement(decl)) { - decl = getDeclarationForBindingElement(decl); - } - if (isVariableDeclaration(decl)) { - return (!isSourceFile(decl.parent.parent.parent) || isCatchClause(decl.parent)) && decl.getSourceFile() === sourceFile; - } - else if (isFunctionDeclaration(decl)) { - return !isSourceFile(decl.parent) && decl.getSourceFile() === sourceFile; - } - return false; - } - - function getDeclarationForBindingElement(element: BindingElement): VariableDeclaration | ParameterDeclaration { - while (true) { - if (isBindingElement(element.parent.parent)) { - element = element.parent.parent; - } - else { - return element.parent.parent; - } - } - } - - function inImportClause(node: Node): boolean { - const parent = node.parent; - return parent && (isImportClause(parent) || isImportSpecifier(parent) || isNamespaceImport(parent)); - } - - function isExpressionInCallExpression(node: Node): boolean { - while (isRightSideOfQualifiedNameOrPropertyAccess(node)) { - node = node.parent; - } - return isCallExpression(node.parent) && node.parent.expression === node; - } - - function isRightSideOfQualifiedNameOrPropertyAccess(node: Node): boolean { - return (isQualifiedName(node.parent) && node.parent.right === node) || (isPropertyAccessExpression(node.parent) && node.parent.name === node); - } - - const enum SemanticMeaning { - None = 0x0, - Value = 0x1, - Type = 0x2, - Namespace = 0x4, - All = Value | Type | Namespace - } - - function getMeaningFromLocation(node: Node): SemanticMeaning { - const f = (ts).getMeaningFromLocation; - if (typeof f === "function") { - return f(node); - } - return SemanticMeaning.All; - } - - const tokenFromDeclarationMapping: { [name: string]: TokenType } = { - [SyntaxKind.VariableDeclaration]: TokenType.variable, - [SyntaxKind.Parameter]: TokenType.parameter, - [SyntaxKind.PropertyDeclaration]: TokenType.property, - [SyntaxKind.ModuleDeclaration]: TokenType.namespace, - [SyntaxKind.EnumDeclaration]: TokenType.enum, - [SyntaxKind.EnumMember]: TokenType.enumMember, - [SyntaxKind.ClassDeclaration]: TokenType.class, - [SyntaxKind.MethodDeclaration]: TokenType.member, - [SyntaxKind.FunctionDeclaration]: TokenType.function, - [SyntaxKind.FunctionExpression]: TokenType.function, - [SyntaxKind.MethodSignature]: TokenType.member, - [SyntaxKind.GetAccessor]: TokenType.property, - [SyntaxKind.SetAccessor]: TokenType.property, - [SyntaxKind.PropertySignature]: TokenType.property, - [SyntaxKind.InterfaceDeclaration]: TokenType.interface, - [SyntaxKind.TypeAliasDeclaration]: TokenType.type, - [SyntaxKind.TypeParameter]: TokenType.typeParameter, - [SyntaxKind.PropertyAssignment]: TokenType.property, - [SyntaxKind.ShorthandPropertyAssignment]: TokenType.property - }; -} diff --git a/src/services/types.ts b/src/services/types.ts index 24d04fb438e2f..793e8ccd8491c 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -255,7 +255,7 @@ namespace ts { /* * Required for full import and type reference completions. - * These should be unprefixed names. E.g. `getDirectories("/foo/bar")` should return `["a", "b"]`, not `["/foo/bar/a", "/foo/bar/b"]`. + * These should be un-prefixed names. E.g. `getDirectories("/foo/bar")` should return `["a", "b"]`, not `["/foo/bar/a", "/foo/bar/b"]`. */ getDirectories?(directoryName: string): string[]; @@ -409,6 +409,7 @@ namespace ts { getSmartSelectionRange(fileName: string, position: number): SelectionRange; + /** Test */ getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; diff --git a/src/testRunner/unittests/publicApi.ts b/src/testRunner/unittests/publicApi.ts index 7f141f16c2705..0f25edf7474dc 100644 --- a/src/testRunner/unittests/publicApi.ts +++ b/src/testRunner/unittests/publicApi.ts @@ -10,7 +10,7 @@ describe("unittests:: Public APIs", () => { }); it("should be acknowledged when they change", () => { - Harness.Baseline.runBaseline(api, fileContent); + Harness.Baseline.runBaseline(api, fileContent, { PrintDiff: true }); }); it("should compile", () => {