// // Copyright (c) Microsoft Corporation. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // /// /// module FourSlash { // Represents a parsed source file with metadata export interface FourSlashFile { // The contents of the file (with markers, etc stripped out) content: string; fileName: string; // File-specific options (name/value pairs) fileOptions: { [index: string]: string; }; } // Represents a set of parsed source files and options export interface FourSlashData { // Global options (name/value pairs) globalOptions: { [index: string]: string; }; files: FourSlashFile[]; // A mapping from marker names to name/position pairs markerPositions: { [index: string]: Marker; }; markers: Marker[]; ranges: Range[]; } export interface TestXmlData { invalidReason: string; originalName: string; actions: string[]; } interface MemberListData { result: { maybeInaccurate: boolean; isMemberCompletion: boolean; entries: { name: string; type: string; kind: string; kindModifiers: string; }[]; }; } export interface Marker { fileName: string; position: number; data?: any; } interface MarkerMap { [index: string]: Marker; } export interface Range { fileName: string; start: number; end: number; marker?: Marker; } interface ILocationInformation { position: number; sourcePosition: number; sourceLine: number; sourceColumn: number; } interface IRangeLocationInformation extends ILocationInformation { marker?: Marker; } export interface TextSpan { start: number; end: number; } export enum IncrementalEditValidation { None, SyntacticOnly, Complete } export enum TypingFidelity { /// Performs typing and formatting (if formatting is enabled) Low, /// Performs typing, checks completion lists, signature help, and formatting (if enabled) High } var entityMap: TypeScript.IIndexable = { '&': '&', '"': '"', "'": ''', '/': '/', '<': '<', '>': '>' }; export function escapeXmlAttributeValue(s: string) { return s.replace(/[&<>"'\/]/g, ch => entityMap[ch]); } // List of allowed metadata names var fileMetadataNames = ['Filename']; var globalMetadataNames = ['Module', 'Target', 'BaselineFile']; // Note: Only BaselineFile is actually supported at the moment export var currentTestState: TestState = null; export class TestCancellationToken implements TypeScript.ICancellationToken { // 0 - cancelled // >0 - not cancelled // <0 - not cancelled and value denotes number of isCancellationRequested after which token become cancelled private static NotCancelled: number = -1; private numberOfCallsBeforeCancellation: number = TestCancellationToken.NotCancelled; public isCancellationRequested(): boolean { if (this.numberOfCallsBeforeCancellation < 0) { return false; } if (this.numberOfCallsBeforeCancellation > 0) { this.numberOfCallsBeforeCancellation--; return false; } return true; } public setCancelled(numberOfCalls: number = 0): void { TypeScript.Debug.assert(numberOfCalls >= 0); this.numberOfCallsBeforeCancellation = numberOfCalls; } public resetCancelled(): void { this.numberOfCallsBeforeCancellation = TestCancellationToken.NotCancelled; } } export function verifyOperationIsCancelled(f: () => void) { try { f(); } catch (e) { if (e instanceof TypeScript.OperationCanceledException) { return; } } throw new Error("Operation should be cancelled"); } export class TestState { // Language service instance public languageServiceShimHost: Harness.TypeScriptLS = null; private languageService: TypeScript.Services.ILanguageService = null; private newLanguageService: ts.LanguageService = null; // A reference to the language service's compiler state's compiler instance private compiler: () => { getSyntaxTree(fileName: string): TypeScript.SyntaxTree; getSourceUnit(fileName: string): TypeScript.SourceUnitSyntax; }; // The current caret position in the active file public currentCaretPosition = 0; public lastKnownMarker: string = ""; // The file that's currently 'opened' public activeFile: FourSlashFile = null; // Whether or not we should format on keystrokes public enableFormatting = true; public formatCodeOptions: TypeScript.Services.FormatCodeOptions = null; public cancellationToken: TestCancellationToken; public editValidation = IncrementalEditValidation.Complete; public typingFidelity = TypingFidelity.Low; private scenarioActions: string[] = []; private taoInvalidReason: string = null; constructor(public testData: FourSlashData) { // Initialize the language service with all the scripts this.cancellationToken = new TestCancellationToken(); this.languageServiceShimHost = new Harness.TypeScriptLS(this.cancellationToken); var harnessCompiler = Harness.Compiler.getCompiler(); var inputFiles: { unitName: string; content: string }[] = []; testData.files.forEach(file => { var fixedPath = file.fileName.substr(file.fileName.indexOf('tests/')); harnessCompiler.addInputFile({ unitName: fixedPath, content: file.content }); }); // If the last unit contains require( or /// reference then consider it the only input file // and the rest will be added via resolution. If not, then assume we have multiple files // with 0 references in any of them. We could be smarter here to allow scenarios like // 2 files without references and 1 file with a reference but we have 0 tests like that // at the moment and an exhaustive search of the test files for that content could be quite slow. var lastFile = testData.files[testData.files.length - 1]; if (/require\(/.test(lastFile.content) || /reference\spath/.test(lastFile.content)) { inputFiles.push({ unitName: lastFile.fileName, content: lastFile.content }); } else { inputFiles = testData.files.map(file => { return { unitName: file.fileName, content: file.content }; }); } // NEWTODO: Re-implement commented-out section // harnessCompiler.addInputFiles(inputFiles); try { // var resolvedFiles = harnessCompiler.resolve(); //resolvedFiles.forEach(file => { // if (!Harness.isLibraryFile(file.path)) { // var fixedPath = file.path.substr(file.path.indexOf('tests/')); // var content = harnessCompiler.getContentForFile(fixedPath); // this.languageServiceShimHost.addScript(fixedPath, content); // } //}); // NEWTODO: For now do not resolve, just use the input files inputFiles.forEach(file => { if (!Harness.isLibraryFile(file.unitName)) { this.languageServiceShimHost.addScript(file.unitName, file.content); } }); this.languageServiceShimHost.addScript('lib.d.ts', Harness.Compiler.libTextMinimal); } finally { // harness no longer needs the results of the above work, make sure the next test operations are in a clean state //harnessCompiler.reset(); } // Sneak into the language service and get its compiler so we can examine the syntax trees this.languageService = this.languageServiceShimHost.getLanguageService().languageService; this.newLanguageService = this.languageServiceShimHost.newLS; var compilerState = (this.languageService).compiler; this.compiler = () => compilerState.compiler; this.formatCodeOptions = new TypeScript.Services.FormatCodeOptions(); this.testData.files.forEach(file => { var filename = file.fileName.replace(Harness.IO.directoryName(file.fileName), '').substr(1); var filenameWithoutExtension = filename.substr(0, filename.lastIndexOf(".")); this.scenarioActions.push(''); }); // Open the first file by default this.openFile(0); } // Entry points from fourslash.ts public goToMarker(name = '') { var marker = this.getMarkerByName(name); if (this.activeFile.fileName !== marker.fileName) { this.openFile(marker.fileName); } var scriptSnapshot = this.languageServiceShimHost.getScriptSnapshot(marker.fileName); if (marker.position === -1 || marker.position > scriptSnapshot.getLength()) { throw new Error('Marker "' + name + '" has been invalidated by unrecoverable edits to the file.'); } this.lastKnownMarker = name; this.goToPosition(marker.position); } public goToPosition(pos: number) { this.currentCaretPosition = pos; var lineCharPos = TypeScript.LineMap1.fromString(this.getCurrentFileContent()).getLineAndCharacterFromPosition(pos); this.scenarioActions.push(''); } public moveCaretRight(count = 1) { this.currentCaretPosition += count; this.currentCaretPosition = Math.min(this.currentCaretPosition, this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getLength()); if (count > 0) { this.scenarioActions.push(''); } else { this.scenarioActions.push(''); } } // Opens a file given its 0-based index or fileName public openFile(index: number): void; public openFile(name: string): void; public openFile(indexOrName: any) { var fileToOpen: FourSlashFile = this.findFile(indexOrName); fileToOpen.fileName = Harness.Path.switchToForwardSlashes(fileToOpen.fileName); this.activeFile = fileToOpen; var filename = fileToOpen.fileName.replace(Harness.IO.directoryName(fileToOpen.fileName), '').substr(1); this.scenarioActions.push(''); } public verifyErrorExistsBetweenMarkers(startMarkerName: string, endMarkerName: string, negative: boolean) { var startMarker = this.getMarkerByName(startMarkerName); var endMarker = this.getMarkerByName(endMarkerName); var predicate = function (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) { // NEWTODO: make this more specific again //return ((errorMinChar === startPos) && (errorLimChar === endPos)) ? true : false; return ((errorMinChar >= startPos) && (errorLimChar <= endPos)) ? true : false; }; var exists = this.anyErrorInRange(predicate, startMarker, endMarker); this.taoInvalidReason = 'verifyErrorExistsBetweenMarkers NYI'; if (exists !== negative) { this.new_printErrorLog(negative, this.new_getAllDiagnostics()); throw new Error("Failure between markers: " + startMarkerName + ", " + endMarkerName); } } private getDiagnostics(fileName: string): TypeScript.Diagnostic[] { var syntacticErrors = this.languageService.getSyntacticDiagnostics(fileName); var semanticErrors = this.languageService.getSemanticDiagnostics(fileName); var diagnostics: TypeScript.Diagnostic[] = []; diagnostics.push.apply(diagnostics, syntacticErrors); diagnostics.push.apply(diagnostics, semanticErrors); return diagnostics; } private new_getDiagnostics(fileName: string): ts.Diagnostic[] { var syntacticErrors = this.newLanguageService.getSyntacticDiagnostics(fileName); var semanticErrors = this.newLanguageService.getSemanticDiagnostics(fileName); var diagnostics: ts.Diagnostic[] = []; diagnostics.push.apply(diagnostics, syntacticErrors); diagnostics.push.apply(diagnostics, semanticErrors); return diagnostics; } private getAllDiagnostics(): TypeScript.Diagnostic[] { var diagnostics: TypeScript.Diagnostic[] = []; var fileNames = JSON.parse(this.languageServiceShimHost.getScriptFileNames()); for (var i = 0, n = fileNames.length; i < n; i++) { diagnostics.push.apply(this.getDiagnostics(fileNames[i])); } return diagnostics; } private new_getAllDiagnostics(): ts.Diagnostic[] { var diagnostics: ts.Diagnostic[] = []; var fileNames = JSON.parse(this.languageServiceShimHost.getScriptFileNames()); for (var i = 0, n = fileNames.length; i < n; i++) { diagnostics.push.apply(this.new_getDiagnostics(fileNames[i])); } return diagnostics; } public verifyErrorExistsAfterMarker(markerName: string, negative: boolean, after: boolean) { var marker: Marker = this.getMarkerByName(markerName); var predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) => boolean; if (after) { predicate = function (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) { return ((errorMinChar >= startPos) && (errorLimChar >= startPos)) ? true : false; }; } else { predicate = function (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) { return ((errorMinChar <= startPos) && (errorLimChar <= startPos)) ? true : false; }; } this.taoInvalidReason = 'verifyErrorExistsAfterMarker NYI'; var exists = this.anyErrorInRange(predicate, marker); var diagnostics = this.new_getAllDiagnostics(); if (exists !== negative) { this.new_printErrorLog(negative, diagnostics); throw new Error("Failure at marker: " + markerName); } } private anyErrorInRange(predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) => boolean, startMarker: Marker, endMarker?: Marker) { var errors = this.new_getDiagnostics(startMarker.fileName); var exists = false; var startPos = startMarker.position; if (endMarker !== undefined) { var endPos = endMarker.position; } errors.forEach((error)=> { if (predicate(error.start, error.start + error.length, startPos, endPos)) { exists = true; } }); return exists; } private printErrorLog(expectErrors: boolean, errors: TypeScript.Diagnostic[]) { if (expectErrors) { Harness.IO.log("Expected error not found. Error list is:"); } else { Harness.IO.log("Unexpected error(s) found. Error list is:"); } errors.forEach(function (error: TypeScript.Diagnostic) { Harness.IO.log(" minChar: " + error.start() + ", limChar: " + (error.start() + error.length()) + ", message: " + error.message() + "\n"); }); } private new_printErrorLog(expectErrors: boolean, errors: ts.Diagnostic[]) { if (expectErrors) { Harness.IO.log("Expected error not found. Error list is:"); } else { Harness.IO.log("Unexpected error(s) found. Error list is:"); } errors.forEach(error => { Harness.IO.log(" minChar: " + error.start + ", limChar: " + (error.start + error.length) + ", message: " + error.messageText + "\n"); }); } public verifyNumberOfErrorsInCurrentFile(expected: number) { var errors = this.getDiagnostics(this.activeFile.fileName); var actual = errors.length; this.scenarioActions.push(''); if (actual !== expected) { var errorMsg = "Actual number of errors (" + actual + ") does not match expected number (" + expected + ")"; Harness.IO.log(errorMsg); throw new Error(errorMsg); } } public verifyEval(expr: string, value: any) { var emit = this.languageService.getEmitOutput(this.activeFile.fileName); if (emit.outputFiles.length !== 1) { throw new Error("Expected exactly one output from emit of " + this.activeFile.fileName); } this.taoInvalidReason = 'verifyEval impossible'; var evaluation = new Function(emit.outputFiles[0].text + ';\r\nreturn (' + expr + ');')(); if (evaluation !== value) { throw new Error('Expected evaluation of expression "' + expr + '" to equal "' + value + '", but got "' + evaluation + '"'); } } public verifyMemberListContains(symbol: string, type?: string, docComment?: string, fullSymbolName?: string, kind?: string) { this.scenarioActions.push(''); this.scenarioActions.push(''); if (type || docComment || fullSymbolName || kind) { this.taoInvalidReason = 'verifyMemberListContains only supports the "symbol" parameter'; } var members = this.getMemberListAtCaret(); if (members) { this.assertItemInCompletionList(members.entries, symbol, type, docComment, fullSymbolName, kind); } else { throw new Error("Expected a member list, but none was provided") } } public verifyMemberListCount(expectedCount: number, negative: boolean) { if (expectedCount === 0) { if (negative) { this.verifyMemberListIsEmpty(false); return; } else { this.scenarioActions.push(''); } } else { this.scenarioActions.push(''); this.scenarioActions.push(''); } var members = this.getMemberListAtCaret(); if (members) { var match = members.entries.length === expectedCount; if ((!match && !negative) || (match && negative)) { throw new Error("Member list count was " + members.entries.length + ". Expected " + expectedCount); } } else if (expectedCount) { throw new Error("Member list count was 0. Expected " + expectedCount); } } public verifyMemberListDoesNotContain(symbol: string) { this.scenarioActions.push(''); this.scenarioActions.push(''); var members = this.getMemberListAtCaret(); if (members.entries.filter(e => e.name === symbol).length !== 0) { throw new Error('Member list did contain ' + symbol); } } public verifyCompletionListItemsCountIsGreaterThan(count: number) { this.taoInvalidReason = 'verifyCompletionListItemsCountIsGreaterThan NYI'; var completions = this.getCompletionListAtCaret(); var itemsCount = completions.entries.length; if (itemsCount <= count) { throw new Error('Expected completion list items count to be greater than ' + count + ', but is actually ' + itemsCount); } } public verifyMemberListIsEmpty(negative: boolean) { if (negative) { this.scenarioActions.push(''); } else { this.scenarioActions.push(''); } var members = this.getMemberListAtCaret(); if ((!members || members.entries.length === 0) && negative) { throw new Error("Member list is empty at Caret"); } else if ((members && members.entries.length !== 0) && !negative) { var errorMsg = "\n" + "Member List contains: [" + members.entries[0].name; for (var i = 1; i < members.entries.length; i++) { errorMsg += ", " + members.entries[i].name; } errorMsg += "]\n"; Harness.IO.log(errorMsg); throw new Error("Member list is not empty at Caret"); } } public verifyCompletionListIsEmpty(negative: boolean) { this.scenarioActions.push(''); var completions = this.getCompletionListAtCaret(); if ((!completions || completions.entries.length === 0) && negative) { throw new Error("Completion list is empty at Caret"); } else if ((completions && completions.entries.length !== 0) && !negative) { var errorMsg = "\n" + "Completion List contains: [" + completions.entries[0].name; for (var i = 1; i < completions.entries.length; i++) { errorMsg += ", " + completions.entries[i].name; } errorMsg += "]\n"; Harness.IO.log(errorMsg); throw new Error("Completion list is not empty at Caret"); } } public verifyCompletionListContains(symbol: string, type?: string, docComment?: string, fullSymbolName?: string, kind?: string) { var completions = this.getCompletionListAtCaret(); this.assertItemInCompletionList(completions.entries, symbol, type, docComment, fullSymbolName, kind); } public verifyCompletionListDoesNotContain(symbol: string) { this.scenarioActions.push(''); this.scenarioActions.push(''); var completions = this.getCompletionListAtCaret(); if (completions && completions.entries && completions.entries.filter(e => e.name === symbol).length !== 0) { throw new Error('Completion list did contain ' + symbol); } } public verifyCompletionEntryDetails(entryName: string, type: string, docComment?: string, fullSymbolName?: string, kind?: string) { this.taoInvalidReason = 'verifyCompletionEntryDetails NYI'; var details = this.getCompletionEntryDetails(entryName); assert.equal(details.type, type); if (docComment != undefined) { assert.equal(details.docComment, docComment); } if (fullSymbolName !== undefined) { assert.equal(details.fullSymbolName, fullSymbolName); } if (kind !== undefined) { assert.equal(details.kind, kind); } } public verifyReferencesCountIs(count: number, localFilesOnly: boolean = true) { this.taoInvalidReason = 'verifyReferences NYI'; var references = this.getReferencesAtCaret(); var referencesCount = 0; if (localFilesOnly) { var localFiles = this.testData.files.map(file => file.fileName); // Count only the references in local files. Filter the ones in lib and other files. references.forEach((entry) => { if (localFiles.some((filename) => filename === entry.fileName)) { ++referencesCount; } }); } else { referencesCount = references.length; } if (referencesCount !== count) { var condition = localFilesOnly ? "excluding libs" : "including libs"; throw new Error("Expected references count (" + condition + ") to be " + count + ", but is actually " + references.length); } } public verifyImplementorsCountIs(count: number, localFilesOnly: boolean = true) { var implementors = this.getImplementorsAtCaret(); var implementorsCount = 0; if (localFilesOnly) { var localFiles = this.testData.files.map(file => file.fileName); // Count only the references in local files. Filter the ones in lib and other files. implementors.forEach((entry) => { if (localFiles.some((filename) => filename === entry.fileName)) { ++implementorsCount; } }); } else { implementorsCount = implementors.length; } if (implementorsCount !== count) { var condition = localFilesOnly ? "excluding libs" : "including libs"; throw new Error("Expected implementors count (" + condition + ") to be " + count + ", but is actually " + implementors.length); } } private getMemberListAtCaret() { return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, true); } private getCompletionListAtCaret() { return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, false); } private getCompletionEntryDetails(entryName: string) { return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName); } private getReferencesAtCaret() { return this.languageService.getReferencesAtPosition(this.activeFile.fileName, this.currentCaretPosition); } private getImplementorsAtCaret() { return this.languageService.getImplementorsAtPosition(this.activeFile.fileName, this.currentCaretPosition); } public verifyQuickInfo(negative: boolean, expectedTypeName?: string, docComment?: string, symbolName?: string, kind?: string) { [expectedTypeName, docComment, symbolName, kind].forEach(str => { if (str) { this.scenarioActions.push(''); this.scenarioActions.push(''); } }); var actualQuickInfo = this.languageService.getTypeAtPosition(this.activeFile.fileName, this.currentCaretPosition); var actualQuickInfoMemberName = actualQuickInfo ? actualQuickInfo.memberName.toString() : ""; var actualQuickInfoDocComment = actualQuickInfo ? actualQuickInfo.docComment : ""; var actualQuickInfoSymbolName = actualQuickInfo ? actualQuickInfo.fullSymbolName : ""; var actualQuickInfoKind = actualQuickInfo ? actualQuickInfo.kind : ""; function assertionMessage(name: string, actualValue: string, expectedValue: string) { return "\nActual " + name + ":\n\t" + actualValue + "\nExpected value:\n\t" + expectedValue; } if (negative) { if (expectedTypeName !== undefined) { assert.notEqual(actualQuickInfoMemberName, expectedTypeName, assertionMessage("quick info member name", actualQuickInfoMemberName, expectedTypeName)); } if (docComment != undefined) { assert.notEqual(actualQuickInfoDocComment, docComment, assertionMessage("quick info doc comment", actualQuickInfoDocComment, docComment)); } if (symbolName !== undefined) { assert.notEqual(actualQuickInfoSymbolName, symbolName, assertionMessage("quick info symbol name", actualQuickInfoSymbolName, symbolName)); } if (kind !== undefined) { assert.notEqual(actualQuickInfoKind, kind, assertionMessage("quick info kind", actualQuickInfoKind, kind)); } } else { if (expectedTypeName !== undefined) { assert.equal(actualQuickInfoMemberName, expectedTypeName, assertionMessage("quick info member", actualQuickInfoMemberName, expectedTypeName)); } if (docComment != undefined) { assert.equal(actualQuickInfoDocComment, docComment, assertionMessage("quick info doc", actualQuickInfoDocComment, docComment)); } if (symbolName !== undefined) { assert.equal(actualQuickInfoSymbolName, symbolName, assertionMessage("quick info symbol name", actualQuickInfoSymbolName, symbolName)); } if (kind !== undefined) { assert.equal(actualQuickInfoKind, kind, assertionMessage("quick info kind", actualQuickInfoKind, kind)); } } } public verifyQuickInfoExists(negative: number) { this.taoInvalidReason = 'verifyQuickInfoExists NYI'; var actualQuickInfo = this.languageService.getTypeAtPosition(this.activeFile.fileName, this.currentCaretPosition); if (negative) { if (actualQuickInfo) { throw new Error('verifyQuickInfoExists failed. Expected quick info NOT to exist'); } } else { if (!actualQuickInfo) { throw new Error('verifyQuickInfoExists failed. Expected quick info to exist'); } } } public verifyCurrentSignatureHelpIs(expected: string) { this.taoInvalidReason = 'verifyCurrentSignatureHelpIs NYI'; var help = this.getActiveSignatureHelp(); assert.equal(help.prefix + help.parameters.map(p => p.display).join(help.separator) + help.suffix, expected); } public verifyCurrentParameterIsVariable(isVariable: boolean) { this.taoInvalidReason = 'verifyCurrentParameterIsVariable NYI'; var signature = this.getActiveSignatureHelp(); assert.isNotNull(signature); assert.equal(isVariable, signature.isVariadic); } public verifyCurrentParameterHelpName(name: string) { this.taoInvalidReason = 'verifyCurrentParameterHelpName NYI'; var activeParameter = this.getActiveParameter(); var activeParameterName = activeParameter.name; assert.equal(activeParameterName, name); } public verifyCurrentParameterSpanIs(parameter: string) { this.taoInvalidReason = 'verifyCurrentParameterSpanIs NYI'; var activeSignature = this.getActiveSignatureHelp(); var activeParameter = this.getActiveParameter(); assert.equal(activeParameter.display, parameter); } public verifyCurrentParameterHelpDocComment(docComment: string) { this.taoInvalidReason = 'verifyCurrentParameterHelpDocComment NYI'; var activeParameter = this.getActiveParameter(); var activeParameterDocComment = activeParameter.documentation; assert.equal(activeParameterDocComment, docComment); } public verifyCurrentSignatureHelpParameterCount(expectedCount: number) { this.taoInvalidReason = 'verifyCurrentSignatureHelpParameterCount NYI'; assert.equal(this.getActiveSignatureHelp().parameters.length, expectedCount); } public verifyCurrentSignatureHelpTypeParameterCount(expectedCount: number) { this.taoInvalidReason = 'verifyCurrentSignatureHelpTypeParameterCount NYI'; // assert.equal(this.getActiveSignatureHelp().typeParameters.length, expectedCount); } public verifyCurrentSignatureHelpDocComment(docComment: string) { this.taoInvalidReason = 'verifyCurrentSignatureHelpDocComment NYI'; var actualDocComment = this.getActiveSignatureHelp().documentation; assert.equal(actualDocComment, docComment); } public verifySignatureHelpCount(expected: number) { this.scenarioActions.push(''); this.scenarioActions.push(''); var help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); var actual = help && help.items ? help.items.length : 0; assert.equal(actual, expected); } public verifySignatureHelpPresent(shouldBePresent = true) { this.taoInvalidReason = 'verifySignatureHelpPresent NYI'; var actual = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); if (shouldBePresent) { if (!actual) { throw new Error("Expected signature help to be present, but it wasn't"); } } else { if (actual) { throw new Error("Expected no signature help, but got '" + JSON.stringify(actual) + "'"); } } } //private getFormalParameter() { // var help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); // return help.formal; //} private getActiveSignatureHelp() { var help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); // If the signature hasn't been narrowed down yet (e.g. no parameters have yet been entered), // 'activeFormal' will be -1 (even if there is only 1 signature). Signature help will show the // first signature in the signature group, so go with that var index = help.selectedItemIndex < 0 ? 0 : help.selectedItemIndex; return help.items[index]; } private getActiveParameter(): TypeScript.Services.SignatureHelpParameter { var currentSig = this.getActiveSignatureHelp(); var help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); var item = help.items[help.selectedItemIndex]; var state = this.languageService.getSignatureHelpCurrentArgumentState(this.activeFile.fileName, this.currentCaretPosition, help.applicableSpan.start()); // Same logic as in getActiveSignatureHelp - this value might be -1 until a parameter value actually gets typed var currentParam = state === null ? 0 : state.argumentIndex; return item.parameters[currentParam]; } public getBreakpointStatementLocation(pos: number) { this.taoInvalidReason = 'getBreakpointStatementLocation NYI'; var spanInfo = this.languageService.getBreakpointStatementAtPosition(this.activeFile.fileName, pos); var resultString = "\n**Pos: " + pos + " SpanInfo: " + JSON.stringify(spanInfo) + "\n** Statement: "; if (spanInfo !== null) { resultString = resultString + this.activeFile.content.substr(spanInfo.start(), spanInfo.length()); } return resultString; } public baselineCurrentFileBreakpointLocations() { this.taoInvalidReason = 'baselineCurrentFileBreakpointLocations impossible'; Harness.Baseline.runBaseline( "Breakpoint Locations for " + this.activeFile.fileName, this.testData.globalOptions['BaselineFile'], () => { var fileLength = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getLength(); var resultString = ""; for (var pos = 0; pos < fileLength; pos++) { resultString = resultString + this.getBreakpointStatementLocation(pos); } return resultString; }); } public printBreakpointLocation(pos: number) { Harness.IO.log(this.getBreakpointStatementLocation(pos)); } public printBreakpointAtCurrentLocation() { this.printBreakpointLocation(this.currentCaretPosition); } public printCurrentParameterHelp() { var help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); Harness.IO.log(JSON.stringify(help)); } public printCurrentQuickInfo() { var quickInfo = this.languageService.getTypeAtPosition(this.activeFile.fileName, this.currentCaretPosition); Harness.IO.log(JSON.stringify(quickInfo)); } public printErrorList() { Harness.IO.log("--------------"); Harness.IO.log("Old Errors"); Harness.IO.log("--------------"); var syntacticErrors = this.languageService.getSyntacticDiagnostics(this.activeFile.fileName); var semanticErrors = this.languageService.getSemanticDiagnostics(this.activeFile.fileName); var errorList = syntacticErrors.concat(semanticErrors); Harness.IO.log('Error list (' + errorList.length + ' errors)'); if (errorList.length) { errorList.forEach(err => { Harness.IO.log("start: " + err.start() + ", length: " + err.length() + ", message: " + err.message()); }); } Harness.IO.log("--------------"); Harness.IO.log("New Errors"); Harness.IO.log("--------------"); this.new_printErrorList(); } public new_printErrorList() { var syntacticErrors = this.newLanguageService.getSyntacticDiagnostics(this.activeFile.fileName); var semanticErrors = this.newLanguageService.getSemanticDiagnostics(this.activeFile.fileName); var errorList = syntacticErrors.concat(semanticErrors); Harness.IO.log('Error list (' + errorList.length + ' errors)'); if (errorList.length) { errorList.forEach(error => { Harness.IO.log("start: " + error.start + ", length: " + error.length + ", message: " + error.messageText); }); } } public printCurrentFileState(makeWhitespaceVisible = false, makeCaretVisible = true) { for (var i = 0; i < this.testData.files.length; i++) { var file = this.testData.files[i]; var active = (this.activeFile === file); Harness.IO.log('=== Script (' + file.fileName + ') ' + (active ? '(active, cursor at |)' : '') + ' ==='); var snapshot = this.languageServiceShimHost.getScriptSnapshot(file.fileName); var content = snapshot.getText(0, snapshot.getLength()); if (active) { content = content.substr(0, this.currentCaretPosition) + (makeCaretVisible ? '|' : "") + content.substr(this.currentCaretPosition); } if (makeWhitespaceVisible) { content = TestState.makeWhitespaceVisible(content); } Harness.IO.log(content); } } public printCurrentSignatureHelp() { var sigHelp = this.getActiveSignatureHelp(); Harness.IO.log(JSON.stringify(sigHelp)); } public printMemberListMembers() { var members = this.getMemberListAtCaret(); Harness.IO.log(JSON.stringify(members)); } public printCompletionListMembers() { var completions = this.getCompletionListAtCaret(); Harness.IO.log(JSON.stringify(completions)); } private editCheckpoint(filename: string) { // TODO: What's this for? It is being called by deleteChar // this.languageService.getScriptLexicalStructure(filename); } public deleteChar(count = 1) { this.scenarioActions.push(''); var offset = this.currentCaretPosition; var ch = ""; for (var i = 0; i < count; i++) { // Make the edit this.languageServiceShimHost.editScript(this.activeFile.fileName, offset, offset + 1, ch); this.updateMarkersForEdit(this.activeFile.fileName, offset, offset + 1, ch); this.editCheckpoint(this.activeFile.fileName); // Handle post-keystroke formatting if (this.enableFormatting) { var edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); offset += this.applyEdits(this.activeFile.fileName, edits, true); } } // Move the caret to wherever we ended up this.currentCaretPosition = offset; this.fixCaretPosition(); this.checkPostEditInvariants(); } public replace(start: number, length: number, text: string) { this.taoInvalidReason = 'replace NYI'; this.languageServiceShimHost.editScript(this.activeFile.fileName, start, start + length, text); this.updateMarkersForEdit(this.activeFile.fileName, start, start + length, text); this.editCheckpoint(this.activeFile.fileName); this.checkPostEditInvariants(); } public deleteCharBehindMarker(count = 1) { this.scenarioActions.push(''); var offset = this.currentCaretPosition; var ch = ""; for (var i = 0; i < count; i++) { offset--; // Make the edit this.languageServiceShimHost.editScript(this.activeFile.fileName, offset, offset + 1, ch); this.updateMarkersForEdit(this.activeFile.fileName, offset, offset + 1, ch); this.editCheckpoint(this.activeFile.fileName); // Handle post-keystroke formatting if (this.enableFormatting) { var edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); offset += this.applyEdits(this.activeFile.fileName, edits, true); this.editCheckpoint(this.activeFile.fileName); } } // Move the caret to wherever we ended up this.currentCaretPosition = offset; this.fixCaretPosition(); this.checkPostEditInvariants(); } // Enters lines of text at the current caret position public type(text: string) { if (text === '') { this.taoInvalidReason = 'Test used empty-insert workaround.'; } else { this.scenarioActions.push(''); } if (this.typingFidelity === TypingFidelity.Low) { return this.typeLowFidelity(text); } else { return this.typeHighFidelity(text); } } private typeLowFidelity(text: string) { var offset = this.currentCaretPosition; for (var i = 0; i < text.length; i++) { // Make the edit var ch = text.charAt(i); this.languageServiceShimHost.editScript(this.activeFile.fileName, offset, offset, ch); this.updateMarkersForEdit(this.activeFile.fileName, offset, offset, ch); this.editCheckpoint(this.activeFile.fileName); offset++; // Handle post-keystroke formatting if (this.enableFormatting) { var edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); offset += this.applyEdits(this.activeFile.fileName, edits, true); this.editCheckpoint(this.activeFile.fileName); } } // Move the caret to wherever we ended up this.currentCaretPosition = offset; this.fixCaretPosition(); this.checkPostEditInvariants(); } // Enters lines of text at the current caret position, invoking // language service APIs to mimic Visual Studio's behavior // as much as possible private typeHighFidelity(text: string, errorCadence = 5) { var offset = this.currentCaretPosition; var prevChar = ' '; for (var i = 0; i < text.length; i++) { // Make the edit var ch = text.charAt(i); this.languageServiceShimHost.editScript(this.activeFile.fileName, offset, offset, ch); this.updateMarkersForEdit(this.activeFile.fileName, offset, offset, ch); this.editCheckpoint(this.activeFile.fileName); offset++; if (ch === '(' || ch === ',') { // Signature help this.languageService.getSignatureHelpItems(this.activeFile.fileName, offset); } else if (prevChar === ' ' && /A-Za-z_/.test(ch)) { // Completions this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, false); } if (i % errorCadence === 0) { this.languageService.getSyntacticDiagnostics(this.activeFile.fileName); this.languageService.getSemanticDiagnostics(this.activeFile.fileName); } // Handle post-keystroke formatting if (this.enableFormatting) { var edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); offset += this.applyEdits(this.activeFile.fileName, edits, true); this.editCheckpoint(this.activeFile.fileName); } } // Move the caret to wherever we ended up this.currentCaretPosition = offset; this.fixCaretPosition(); this.checkPostEditInvariants(); } // Enters text as if the user had pasted it public paste(text: string) { this.scenarioActions.push(''); var start = this.currentCaretPosition; var offset = this.currentCaretPosition; this.languageServiceShimHost.editScript(this.activeFile.fileName, offset, offset, text); this.updateMarkersForEdit(this.activeFile.fileName, offset, offset, text); this.editCheckpoint(this.activeFile.fileName); offset += text.length; // Handle formatting if (this.enableFormatting) { var edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, offset, this.formatCodeOptions); offset += this.applyEdits(this.activeFile.fileName, edits, true); this.editCheckpoint(this.activeFile.fileName); } // Move the caret to wherever we ended up this.currentCaretPosition = offset; this.fixCaretPosition(); this.checkPostEditInvariants(); } private checkPostEditInvariants() { if (this.editValidation === IncrementalEditValidation.None) { return; } // Get syntactic errors (to force a refresh) var incrSyntaxErrs = JSON.stringify(this.languageService.getSyntacticDiagnostics(this.activeFile.fileName)); // Check syntactic structure var compilationSettings = new TypeScript.CompilationSettings(); compilationSettings.codeGenTarget = TypeScript.LanguageVersion.EcmaScript5; var immutableSettings = TypeScript.ImmutableCompilationSettings.fromCompilationSettings(compilationSettings); var parseOptions = immutableSettings.codeGenTarget(); var snapshot = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName); var content = snapshot.getText(0, snapshot.getLength()); var refSyntaxTree = TypeScript.Parser.parse(this.activeFile.fileName, TypeScript.SimpleText.fromString(content), parseOptions, TypeScript.isDTSFile(this.activeFile.fileName)); var fullSyntaxErrs = JSON.stringify(refSyntaxTree.diagnostics()); if (incrSyntaxErrs !== fullSyntaxErrs) { throw new Error('Mismatched incremental/full syntactic errors for file ' + this.activeFile.fileName + '.\n=== Incremental errors ===\n' + incrSyntaxErrs + '\n=== Full Errors ===\n' + fullSyntaxErrs); } if (this.editValidation !== IncrementalEditValidation.SyntacticOnly) { var compiler = new TypeScript.TypeScriptCompiler(); for (var i = 0; i < this.testData.files.length; i++) { snapshot = this.languageServiceShimHost.getScriptSnapshot(this.testData.files[i].fileName); compiler.addFile(this.testData.files[i].fileName, TypeScript.ScriptSnapshot.fromString(snapshot.getText(0, snapshot.getLength())), TypeScript.ByteOrderMark.None, "0", true); } compiler.addFile('lib.d.ts', TypeScript.ScriptSnapshot.fromString(Harness.Compiler.libTextMinimal), TypeScript.ByteOrderMark.None, "0", true); for (var i = 0; i < this.testData.files.length; i++) { var refSemanticErrs = JSON.stringify(compiler.getSemanticDiagnostics(this.testData.files[i].fileName)); var incrSemanticErrs = JSON.stringify(this.languageService.getSemanticDiagnostics(this.testData.files[i].fileName)); if (incrSemanticErrs !== refSemanticErrs) { throw new Error('Mismatched incremental/full semantic errors for file ' + this.testData.files[i].fileName + '\n=== Incremental errors ===\n' + incrSemanticErrs + '\n=== Full Errors ===\n' + refSemanticErrs); } } } } private fixCaretPosition() { // The caret can potentially end up between the \r and \n, which is confusing. If // that happens, move it back one character if (this.currentCaretPosition > 0) { var ch = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getText(this.currentCaretPosition - 1, this.currentCaretPosition); if (ch === '\r') { this.currentCaretPosition--; } }; } private applyEdits(fileName: string, edits: TypeScript.Services.TextChange[], isFormattingEdit = false): number { // We get back a set of edits, but langSvc.editScript only accepts one at a time. Use this to keep track // of the incremental offest from each edit to the next. Assumption is that these edit ranges don't overlap var runningOffset = 0; edits = edits.sort((a, b) => a.span.start() - b.span.start()); // Get a snapshot of the content of the file so we can make sure any formatting edits didn't destroy non-whitespace characters var snapshot = this.languageServiceShimHost.getScriptSnapshot(fileName); var oldContent = snapshot.getText(0, snapshot.getLength()); for (var j = 0; j < edits.length; j++) { this.languageServiceShimHost.editScript(fileName, edits[j].span.start() + runningOffset, edits[j].span.end() + runningOffset, edits[j].newText); this.updateMarkersForEdit(fileName, edits[j].span.start() + runningOffset, edits[j].span.end() + runningOffset, edits[j].newText); var change = (edits[j].span.start() - edits[j].span.end()) + edits[j].newText.length; runningOffset += change; // TODO: Consider doing this at least some of the time for higher fidelity. Currently causes a failure (bug 707150) // this.languageService.getScriptLexicalStructure(fileName); } if (isFormattingEdit) { snapshot = this.languageServiceShimHost.getScriptSnapshot(fileName); var newContent = snapshot.getText(0, snapshot.getLength()); if (newContent.replace(/\s/g, '') !== oldContent.replace(/\s/g, '')) { throw new Error('Formatting operation destroyed non-whitespace content'); } } return runningOffset; } public formatDocument() { this.scenarioActions.push(''); var edits = this.languageService.getFormattingEditsForDocument(this.activeFile.fileName, this.formatCodeOptions); this.currentCaretPosition += this.applyEdits(this.activeFile.fileName, edits, true); this.fixCaretPosition(); } public formatSelection(start: number, end: number) { this.taoInvalidReason = 'formatSelection NYI'; var edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, end, this.formatCodeOptions); this.currentCaretPosition += this.applyEdits(this.activeFile.fileName, edits, true); this.fixCaretPosition(); } private updateMarkersForEdit(fileName: string, minChar: number, limChar: number, text: string) { for (var i = 0; i < this.testData.markers.length; i++) { var marker = this.testData.markers[i]; if (marker.fileName === fileName) { if (marker.position > minChar) { if (marker.position < limChar) { // Marker is inside the edit - mark it as invalidated (?) marker.position = -1; } else { // Move marker back/forward by the appropriate amount marker.position += (minChar - limChar) + text.length; } } } } } public goToBOF() { this.goToPosition(0); } public goToEOF() { var len = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getLength(); this.goToPosition(len); } public goToDefinition(definitionIndex: number) { if (definitionIndex === 0) { this.scenarioActions.push(''); } else { this.taoInvalidReason = 'GoToDefinition not supported for non-zero definition indices'; } var definitions = this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition); if (!definitions || !definitions.length) { throw new Error('goToDefinition failed - expected to at least one defintion location but got 0'); } if (definitionIndex >= definitions.length) { throw new Error('goToDefinition failed - definitionIndex value (' + definitionIndex + ') exceeds definition list size (' + definitions.length + ')'); } var definition = definitions[definitionIndex]; this.openFile(definition.fileName); this.currentCaretPosition = definition.textSpan.start(); } public verifyDefinitionLocationExists(negative: boolean) { this.taoInvalidReason = 'verifyDefinitionLocationExists NYI'; var definitions = this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition); var foundDefinitions = definitions && definitions.length; if (foundDefinitions && negative) { throw new Error('goToDefinition - expected to 0 defintion locations but got ' + definitions.length); } else if (!foundDefinitions && !negative) { throw new Error('goToDefinition - expected to at least one defintion location but got 0'); } } public getMarkers(): Marker[] { // Return a copy of the list return this.testData.markers.slice(0); } public getRanges(): Range[] { // Return a copy of the list return this.testData.ranges.slice(0); } public verifyCaretAtMarker(markerName = '') { this.taoInvalidReason = 'verifyCaretAtMarker NYI'; var pos = this.getMarkerByName(markerName); if (pos.fileName !== this.activeFile.fileName) { throw new Error('verifyCaretAtMarker failed - expected to be in file "' + pos.fileName + '", but was in file "' + this.activeFile.fileName + '"'); } if (pos.position !== this.currentCaretPosition) { throw new Error('verifyCaretAtMarker failed - expected to be at marker "/*' + markerName + '*' + '/, but was at position ' + this.currentCaretPosition + '(' + this.getLineColStringAtCaret() + ')'); } } private getIndentation(fileName: string, position: number): number { return this.languageService.getIndentationAtPosition(fileName, position, this.formatCodeOptions); } public verifyIndentationAtCurrentPosition(numberOfSpaces: number) { this.taoInvalidReason = 'verifyIndentationAtCurrentPosition NYI'; var actual = this.getIndentation(this.activeFile.fileName, this.currentCaretPosition); if (actual != numberOfSpaces) { throw new Error('verifyIndentationAtCurrentPosition failed - expected: ' + numberOfSpaces + ', actual: ' + actual); } } public verifyIndentationAtPosition(fileName: string, position: number, numberOfSpaces: number) { this.taoInvalidReason = 'verifyIndentationAtPosition NYI'; var actual = this.getIndentation(fileName, position); if (actual !== numberOfSpaces) { throw new Error('verifyIndentationAtPosition failed - expected: ' + numberOfSpaces + ', actual: ' + actual); } } public verifyCurrentLineContent(text: string) { this.taoInvalidReason = 'verifyCurrentLineContent NYI'; var actual = this.getCurrentLineContent(); if (actual !== text) { throw new Error('verifyCurrentLineContent\n' + '\tExpected: "' + text + '"\n' + '\t Actual: "' + actual + '"'); } } public verifyCurrentFileContent(text: string) { this.taoInvalidReason = 'verifyCurrentFileContent NYI'; var actual = this.getCurrentFileContent(); var replaceNewlines = (str: string) => str.replace(/\r\n/g, "\n"); if (replaceNewlines(actual) !== replaceNewlines(text)) { throw new Error('verifyCurrentFileContent\n' + '\tExpected: "' + text + '"\n' + '\t Actual: "' + actual + '"'); } } public verifyTextAtCaretIs(text: string) { this.taoInvalidReason = 'verifyCurrentFileContent NYI'; var actual = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getText(this.currentCaretPosition, this.currentCaretPosition + text.length); if (actual !== text) { throw new Error('verifyTextAtCaretIs\n' + '\tExpected: "' + text + '"\n' + '\t Actual: "' + actual + '"'); } } public verifyCurrentNameOrDottedNameSpanText(text: string) { this.taoInvalidReason = 'verifyCurrentNameOrDottedNameSpanText NYI'; var span = this.languageService.getNameOrDottedNameSpan(this.activeFile.fileName, this.currentCaretPosition, this.currentCaretPosition); if (span === null) { throw new Error('verifyCurrentNameOrDottedNameSpanText\n' + '\tExpected: "' + text + '"\n' + '\t Actual: null'); } var actual = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getText(span.start(), span.end()); if (actual !== text) { throw new Error('verifyCurrentNameOrDottedNameSpanText\n' + '\tExpected: "' + text + '"\n' + '\t Actual: "' + actual + '"'); } } private getNameOrDottedNameSpan(pos: number) { var spanInfo = this.languageService.getNameOrDottedNameSpan(this.activeFile.fileName, pos, pos); var resultString = "\n**Pos: " + pos + " SpanInfo: " + JSON.stringify(spanInfo) + "\n** Statement: "; if (spanInfo !== null) { resultString = resultString + this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getText(spanInfo.start(), spanInfo.end()); } return resultString; } public baselineCurrentFileNameOrDottedNameSpans() { this.taoInvalidReason = 'baselineCurrentFileNameOrDottedNameSpans impossible'; Harness.Baseline.runBaseline( "Name OrDottedNameSpans for " + this.activeFile.fileName, this.testData.globalOptions['BaselineFile'], () => { var fileLength = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getLength(); var resultString = ""; for (var pos = 0; pos < fileLength; pos++) { resultString = resultString + this.getNameOrDottedNameSpan(pos); } return resultString; }); } public printNameOrDottedNameSpans(pos: number) { Harness.IO.log(this.getNameOrDottedNameSpan(pos)); } public verifyOutliningSpans(spans: TextSpan[]) { this.taoInvalidReason = 'verifyOutliningSpans NYI'; var actual = this.languageService.getOutliningSpans(this.activeFile.fileName); if (actual.length !== spans.length) { throw new Error('verifyOutliningSpans failed - expected total spans to be ' + spans.length + ', but was ' + actual.length); } for (var i = 0; i < spans.length; i++) { var expectedSpan = spans[i]; var actualSpan = actual[i]; if (expectedSpan.start !== actualSpan.textSpan.start() || expectedSpan.end !== actualSpan.textSpan.end()) { throw new Error('verifyOutliningSpans failed - span ' + (i + 1) + ' expected: (' + expectedSpan.start + ',' + expectedSpan.end + '), actual: (' + actualSpan.textSpan.start() + ',' + actualSpan.textSpan.end() + ')'); } } } public verifyTodoComments(descriptors: string[], spans: TextSpan[]) { var actual = this.languageService.getTodoComments(this.activeFile.fileName, descriptors.map(d => new TypeScript.Services.TodoCommentDescriptor(d, 0))); if (actual.length !== spans.length) { throw new Error('verifyTodoComments failed - expected total spans to be ' + spans.length + ', but was ' + actual.length); } for (var i = 0; i < spans.length; i++) { var expectedSpan = spans[i]; var actualComment = actual[i]; var actualCommentSpan = new TypeScript.TextSpan(actualComment.position, actualComment.message.length); if (expectedSpan.start !== actualCommentSpan.start() || expectedSpan.end !== actualCommentSpan.end()) { throw new Error('verifyOutliningSpans failed - span ' + (i + 1) + ' expected: (' + expectedSpan.start + ',' + expectedSpan.end + '), actual: (' + actualCommentSpan.start() + ',' + actualCommentSpan.end() + ')'); } } } public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) { this.taoInvalidReason = 'verifyMatchingBracePosition NYI'; var actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition); if (actual.length !== 2) { throw new Error('verifyMatchingBracePosition failed - expected result to contain 2 spans, but it had ' + actual.length); } var actualMatchPosition = -1; if (bracePosition == actual[0].start()) { actualMatchPosition = actual[1].start(); } else if (bracePosition == actual[1].start()) { actualMatchPosition = actual[0].start(); } else { throw new Error('verifyMatchingBracePosition failed - could not find the brace position: ' + bracePosition + ' in the returned list: (' + actual[0].start() + ',' + actual[0].end() + ') and (' + actual[1].start() + ',' + actual[1].end() + ')'); } if (actualMatchPosition !== expectedMatchPosition) { throw new Error('verifyMatchingBracePosition failed - expected: ' + actualMatchPosition + ', actual: ' + expectedMatchPosition); } } public verifyNoMatchingBracePosition(bracePosition: number) { this.taoInvalidReason = 'verifyNoMatchingBracePosition NYI'; var actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition); if (actual.length !== 0) { throw new Error('verifyNoMatchingBracePosition failed - expected: 0 spans, actual: ' + actual.length); } } public verifyTypesAgainstFullCheckAtPositions(positions: number[]) { this.taoInvalidReason = 'verifyTypesAgainstFullCheckAtPositions impossible'; // Create a from-scratch LS to check against var referenceLanguageServiceShimHost = new Harness.TypeScriptLS(); var referenceLanguageServiceShim = referenceLanguageServiceShimHost.getLanguageService(); var referenceLanguageService = referenceLanguageServiceShim.languageService; // Add lib.d.ts to the reference language service referenceLanguageServiceShimHost.addScript('lib.d.ts', Harness.Compiler.libTextMinimal); for (var i = 0; i < this.testData.files.length; i++) { var file = this.testData.files[i]; var snapshot = this.languageServiceShimHost.getScriptSnapshot(file.fileName); var content = snapshot.getText(0, snapshot.getLength()); referenceLanguageServiceShimHost.addScript(this.testData.files[i].fileName, content); } for (i = 0; i < positions.length; i++) { var nameOf = (type: TypeScript.Services.TypeInfo) => type ? type.fullSymbolName : '(none)'; var pullName: string, refName: string; var anyFailed = false; var errMsg = ''; try { var pullType = this.languageService.getTypeAtPosition(this.activeFile.fileName, positions[i]); pullName = nameOf(pullType); } catch (err1) { errMsg = 'Failed to get pull type check. Exception: ' + err1 + '\r\n'; if (err1.stack) errMsg = errMsg + err1.stack; pullName = '(failed)'; anyFailed = true; } try { var referenceType = referenceLanguageService.getTypeAtPosition(this.activeFile.fileName, positions[i]); refName = nameOf(referenceType); } catch (err2) { errMsg = 'Failed to get full type check. Exception: ' + err2 + '\r\n'; if (err2.stack) errMsg = errMsg + err2.stack; refName = '(failed)'; anyFailed = true; } var failure = anyFailed || (refName !== pullName); if (failure) { snapshot = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName); content = snapshot.getText(0, snapshot.getLength()); var textAtPosition = content.substr(positions[i], 10); var positionDescription = 'Position ' + positions[i] + ' ("' + textAtPosition + '"...)'; if (anyFailed) { throw new Error('Exception thrown in language service for ' + positionDescription + '\r\n' + errMsg); } else if (refName !== pullName) { throw new Error('Pull/Full disagreement failed at ' + positionDescription + ' - expected full typecheck type "' + refName + '" to equal pull type "' + pullName + '".'); } } } } /// Check number of navigationItems which match both searchValue and matchKind. /// Report an error if expected value and actual value do not match. public verifyNavigationItemsCount(expected: number, searchValue: string, matchKind?: string) { this.taoInvalidReason = 'verifyNavigationItemsCount NYI'; var items = this.languageService.getNavigateToItems(searchValue); var actual = 0; var item: TypeScript.Services.NavigateToItem = null; // Count only the match that match the same MatchKind for (var i = 0; i < items.length; ++i) { item = items[i]; if (!matchKind || item.matchKind === matchKind) { actual++; } } if (expected != actual) { throw new Error('verifyNavigationItemsCount failed - found: ' + actual + ' navigation items, expected: ' + expected + '.'); } } /// Verify that returned navigationItems from getNavigateToItems have matched searchValue, matchKind, and kind. /// Report an error if getNavigateToItems does not find any matched searchValue. public verifyNavigationItemsListContains( name: string, kind: string, searchValue: string, matchKind: string, fileName?: string, parentName?: string) { this.taoInvalidReason = 'verifyNavigationItemsListContains NYI'; var items = this.languageService.getNavigateToItems(searchValue); if (!items || items.length === 0) { throw new Error('verifyNavigationItemsListContains failed - found 0 navigation items, expected at least one.'); } for (var i = 0; i < items.length; i++) { var item = items[i]; if (item && item.name === name && item.kind === kind && (matchKind === undefined || item.matchKind === matchKind) && (fileName === undefined || item.fileName === fileName) && (parentName === undefined || item.containerName === parentName)) { return; } } // if there was an explicit match kind specified, then it should be validated. if (matchKind !== undefined) { var missingItem = { name: name, kind: kind, searchValue: searchValue, matchKind: matchKind, fileName: fileName, parentName: parentName }; throw new Error('verifyNavigationItemsListContains failed - could not find the item: ' + JSON.stringify(missingItem) + ' in the returned list: (' + JSON.stringify(items) + ')'); } } public verifyGetScriptLexicalStructureListCount(expected: number) { this.taoInvalidReason = 'verifyNavigationItemsListContains impossible'; var items = this.languageService.getNavigationBarItems(this.activeFile.fileName); var actual = this.getNavigationBarItemsCount(items); if (expected != actual) { throw new Error('verifyGetScriptLexicalStructureListCount failed - found: ' + actual + ' navigation items, expected: ' + expected + '.'); } } private getNavigationBarItemsCount(items: TypeScript.Services.NavigationBarItem[]) { var result = 0; if (items) { for (var i = 0, n = items.length; i < n; i++) { result++; result += this.getNavigationBarItemsCount(items[i].childItems); } } return result; } public verifGetScriptLexicalStructureListContains( name: string, kind: string, markerPosition?: number) { this.taoInvalidReason = 'verifGetScriptLexicalStructureListContains impossible'; var items = this.languageService.getNavigationBarItems(this.activeFile.fileName); if (!items || items.length === 0) { throw new Error('verifyGetScriptLexicalStructureListContains failed - found 0 navigation items, expected at least one.'); } if (this.navigationBarItemsContains(items, name, kind)) { return; } var missingItem = { name: name, kind: kind }; throw new Error('verifyGetScriptLexicalStructureListContains failed - could not find the item: ' + JSON.stringify(missingItem) + ' in the returned list: (' + JSON.stringify(items) + ')'); } private navigationBarItemsContains(items: TypeScript.Services.NavigationBarItem[], name: string, kind: string) { if (items) { for (var i = 0; i < items.length; i++) { var item = items[i]; if (item && item.text === name && item.kind === kind) { return true; } if (this.navigationBarItemsContains(item.childItems, name, kind)) { return true; } } } return false; } public printNavigationItems(searchValue: string) { var items = this.languageService.getNavigateToItems(searchValue); var length = items && items.length; Harness.IO.log('NavigationItems list (' + length + ' items)'); for (var i = 0; i < length; i++) { var item = items[i]; Harness.IO.log('name: ' + item.name + ', kind: ' + item.kind + ', parentName: ' + item.containerName + ', fileName: ' + item.fileName); } } public printScriptLexicalStructureItems() { var items = this.languageService.getNavigationBarItems(this.activeFile.fileName); var length = items && items.length; Harness.IO.log('NavigationItems list (' + length + ' items)'); for (var i = 0; i < length; i++) { var item = items[i]; Harness.IO.log('name: ' + item.text + ', kind: ' + item.kind); } } private getOccurancesAtCurrentPosition() { return this.languageService.getOccurrencesAtPosition(this.activeFile.fileName, this.currentCaretPosition); } public verifyOccurrencesAtPositionListContains(fileName: string, start: number, end: number, isWriteAccess?: boolean) { this.taoInvalidReason = 'verifyOccurrencesAtPositionListContains NYI'; var occurances = this.getOccurancesAtCurrentPosition(); if (!occurances || occurances.length === 0) { throw new Error('verifyOccurancesAtPositionListContains failed - found 0 references, expected at least one.'); } for (var i = 0; i < occurances.length; i++) { var occurance = occurances[i]; if (occurance && occurance.fileName === fileName && occurance.textSpan.start() === start && occurance.textSpan.end() === end) { if (typeof isWriteAccess !== "undefined" && occurance.isWriteAccess !== isWriteAccess) { throw new Error('verifyOccurancesAtPositionListContains failed - item isWriteAccess value doe not match, actual: ' + occurance.isWriteAccess + ', expected: ' + isWriteAccess + '.'); } return; } } var missingItem = { fileName: fileName, start: start, end: end, isWriteAccess: isWriteAccess }; throw new Error('verifyOccurancesAtPositionListContains failed - could not find the item: ' + JSON.stringify(missingItem) + ' in the returned list: (' + JSON.stringify(occurances) + ')'); } public verifyOccurrencesAtPositionListCount(expectedCount: number) { this.taoInvalidReason = 'verifyOccurrencesAtPositionListCount NYI'; var occurances = this.getOccurancesAtCurrentPosition(); var actualCount = occurances ? occurances.length : 0; if (expectedCount !== actualCount) { throw new Error('verifyOccurrencesAtPositionListCount failed - actual: ' + actualCount + ', expected:' + expectedCount); } } private getBOF(): number { return 0; } private getEOF(): number { return this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName).getLength(); } // Get the text of the entire line the caret is currently at private getCurrentLineContent() { // The current caret position (in line/col terms) var line = this.getCurrentCaretFilePosition().line; // The line/col of the start of this line var pos = this.languageServiceShimHost.lineColToPosition(this.activeFile.fileName, line, 1); // The index of the current file // The text from the start of the line to the end of the file var snapshot = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName); var text = snapshot.getText(pos, snapshot.getLength()); // Truncate to the first newline var newlinePos = text.indexOf('\n'); if (newlinePos === -1) { return text; } else { if (text.charAt(newlinePos - 1) === '\r') { newlinePos--; } return text.substr(0, newlinePos); } } private getCurrentFileContent() { var snapshot = this.languageServiceShimHost.getScriptSnapshot(this.activeFile.fileName); return snapshot.getText(0, snapshot.getLength()); } private getCurrentCaretFilePosition() { var result = this.languageServiceShimHost.positionToZeroBasedLineCol(this.activeFile.fileName, this.currentCaretPosition); if (result.line >= 0) { result.line++; } if (result.character >= 0) { result.character++; } return result; } private assertItemInCompletionList(items: TypeScript.Services.CompletionEntry[], name: string, type?: string, docComment?: string, fullSymbolName?: string, kind?: string) { this.scenarioActions.push(''); this.scenarioActions.push(''); if (type || docComment || fullSymbolName || kind) { this.taoInvalidReason = 'assertItemInCompletionList only supports the "name" parameter'; } for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.name == name) { if (docComment != undefined || type !== undefined || fullSymbolName !== undefined) { var details = this.getCompletionEntryDetails(item.name); if (docComment != undefined) { assert.equal(details.docComment, docComment); } if (type !== undefined) { assert.equal(details.type, type); } if (fullSymbolName !== undefined) { assert.equal(details.fullSymbolName, fullSymbolName); } } if (kind !== undefined) { assert.equal(item.kind, kind); } return; } } var itemsString = items.map((item) => JSON.stringify({ name: item.name, kind: item.kind })).join(",\n"); throw new Error("Marker: " + currentTestState.lastKnownMarker + "\n" + 'Expected "' + JSON.stringify({ name: name, type: type, docComment: docComment, fullSymbolName: fullSymbolName, kind: kind }) + '" to be in list [' + itemsString + ']'); } private findFile(indexOrName: any) { var result: FourSlashFile = null; if (typeof indexOrName === 'number') { var index = indexOrName; if (index >= this.testData.files.length) { throw new Error('File index (' + index + ') in openFile was out of range. There are only ' + this.testData.files.length + ' files in this test.'); } else { result = this.testData.files[index]; } } else if (typeof indexOrName === 'string') { var name = indexOrName; // names are stored in the compiler with this relative path, this allows people to use goTo.file on just the filename name = name.indexOf('/') === -1 ? 'tests/cases/fourslash/' + name : name; var availableNames: string[] = []; var foundIt = false; for (var i = 0; i < this.testData.files.length; i++) { var fn = this.testData.files[i].fileName; if (fn) { if (fn === name) { result = this.testData.files[i]; foundIt = true; break; } availableNames.push(fn); } } if (!foundIt) { throw new Error('No test file named "' + name + '" exists. Available file names are:' + availableNames.join(', ')); } } else { throw new Error('Unknown argument type'); } return result; } private getCurrentLineNumberZeroBased() { return this.getCurrentLineNumberOneBased() - 1; } private getCurrentLineNumberOneBased() { return this.languageServiceShimHost.positionToZeroBasedLineCol(this.activeFile.fileName, this.currentCaretPosition).line + 1; } private getLineColStringAtCaret() { var pos = this.languageServiceShimHost.positionToZeroBasedLineCol(this.activeFile.fileName, this.currentCaretPosition); return 'line ' + (pos.line + 1) + ', col ' + pos.character; } private getMarkerByName(markerName: string) { var markerPos = this.testData.markerPositions[markerName]; if (markerPos === undefined) { var markerNames: string[] = []; for (var m in this.testData.markerPositions) markerNames.push(m); throw new Error('Unknown marker "' + markerName + '" Available markers: ' + markerNames.map(m => '"' + m + '"').join(', ')); } else { return markerPos; } } private static makeWhitespaceVisible(text: string) { return text.replace(/ /g, '\u00B7').replace(/\r/g, '\u00B6').replace(/\n/g, '\u2193\n').replace(/\t/g, '\u2192\ '); } public getTestXmlData(): TestXmlData { return { actions: this.scenarioActions, invalidReason: this.taoInvalidReason, originalName: '' }; } } // TOOD: should these just use the Harness's stdout/stderr? var fsOutput = new Harness.Compiler.WriterAggregator(); var fsErrors = new Harness.Compiler.WriterAggregator(); export var xmlData: TestXmlData[] = []; export function runFourSlashTest(fileName: string) { var content = Harness.IO.readFile(fileName); var xml = runFourSlashTestContent(content, fileName); xmlData.push(xml); } export function runFourSlashTestContent(content: string, fileName: string): TestXmlData { // Parse out the files and their metadata var testData = parseTestData(content, fileName); currentTestState = new TestState(testData); var result = ''; var tsFn = 'tests/cases/fourslash/fourslash.ts'; fsOutput.reset(); fsErrors.reset(); var harnessCompiler = Harness.Compiler.getCompiler(); harnessCompiler.reset(); var filesToAdd = [ { unitName: tsFn, content: Harness.IO.readFile(tsFn) }, { unitName: fileName, content: Harness.IO.readFile(fileName) } ]; harnessCompiler.addInputFiles(filesToAdd); harnessCompiler.compile(); var emitterIOHost: Harness.Compiler.IEmitterIOHost = { writeFile: (path: string, contents: string, writeByteOrderMark: boolean) => fsOutput.Write(contents), resolvePath: (s: string) => s } harnessCompiler.emitAll(emitterIOHost); fsOutput.Close(); fsErrors.Close(); if (fsErrors.lines.length > 0) { throw new Error('Error compiling ' + fileName + ': ' + fsErrors.lines.join('\r\n')); } result = fsOutput.lines.join('\r\n'); // Compile and execute the test try { eval(result); } catch (err) { // Debugging: FourSlash.currentTestState.printCurrentFileState(); throw err; } finally { harnessCompiler.reset(); } var xmlData = currentTestState.getTestXmlData(); xmlData.originalName = fileName; return xmlData; } function chompLeadingSpace(content: string) { var lines = content.split("\n"); for (var i = 0; i < lines.length; i++) { if ((lines[i].length !== 0) && (lines[i].charAt(0) !== ' ')) { return content; } } return lines.map(s => s.substr(1)).join('\n'); } function parseTestData(contents: string, fileName: string): FourSlashData { // Regex for parsing options in the format "@Alpha: Value of any sort" var optionRegex = /^\s*@(\w+): (.*)\s*/; // List of all the subfiles we've parsed out var files: FourSlashFile[] = []; // Global options var opts: { [s: string]: string; } = {}; // Marker positions // Split up the input file by line // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so // we have to string-based splitting instead and try to figure out the delimiting chars var lines = contents.split('\n'); var markerMap: MarkerMap = {}; var markers: Marker[] = []; var ranges: Range[] = []; // Stuff related to the subfile we're parsing var currentFileContent: string = null; var currentFileName = fileName; var currentFileOptions: { [s: string]: string } = {}; for (var i = 0; i < lines.length; i++) { var line = lines[i]; var lineLength = line.length; if (lineLength > 0 && line.charAt(lineLength - 1) === '\r') { line = line.substr(0, lineLength - 1); } if (line.substr(0, 4) === '////') { // Subfile content line // Append to the current subfile content, inserting a newline needed if (currentFileContent === null) { currentFileContent = ''; } else { // End-of-line currentFileContent = currentFileContent + '\n'; } currentFileContent = currentFileContent + line.substr(4); } else if (line.substr(0, 2) === '//') { // Comment line, check for global/file @options and record them var match = optionRegex.exec(line.substr(2)); if (match) { var globalNameIndex = globalMetadataNames.indexOf(match[1]); var fileNameIndex = fileMetadataNames.indexOf(match[1]); if (globalNameIndex === -1) { if (fileNameIndex === -1) { throw new Error('Unrecognized metadata name "' + match[1] + '". Available global metadata names are: ' + globalMetadataNames.join(', ') + '; file metadata names are: ' + fileMetadataNames.join(', ')); } else { // Found an @Filename directive, if this is not the first then create a new subfile if (currentFileContent) { var file = parseFileContent(currentFileContent, currentFileName, markerMap, markers, ranges); file.fileOptions = currentFileOptions; // Store result file files.push(file); // Reset local data currentFileContent = null; currentFileOptions = {}; currentFileName = fileName; } currentFileName = 'tests/cases/fourslash/' + match[2]; currentFileOptions[match[1]] = match[2]; } } else { opts[match[1]] = match[2]; } } } else if (line == '' || lineLength === 0) { // Previously blank lines between fourslash content caused it to be considered as 2 files, // Remove this behavior since it just causes errors now } else { // Empty line or code line, terminate current subfile if there is one if (currentFileContent) { var file = parseFileContent(currentFileContent, currentFileName, markerMap, markers, ranges); file.fileOptions = currentFileOptions; // Store result file files.push(file); // Reset local data currentFileContent = null; currentFileOptions = {}; currentFileName = fileName; } } } return { markerPositions: markerMap, markers: markers, globalOptions: opts, files: files, ranges: ranges } } enum State { none, inSlashStarMarker, inObjectMarker } function reportError(fileName: string, line: number, col: number, message: string) { var errorMessage = fileName + "(" + line + "," + col + "): " + message; throw new Error(errorMessage); } function recordObjectMarker(fileName: string, location: ILocationInformation, text: string, markerMap: MarkerMap, markers: Marker[]): Marker { var markerValue: any = undefined; try { // Attempt to parse the marker value as JSON markerValue = JSON.parse("{ " + text + " }"); } catch (e) { reportError(fileName, location.sourceLine, location.sourceColumn, "Unable to parse marker text " + e.message); } if (markerValue === undefined) { reportError(fileName, location.sourceLine, location.sourceColumn, "Object markers can not be empty"); return null; } var marker: Marker = { fileName: fileName, position: location.position, data: markerValue }; // Object markers can be anonymous if (markerValue.name) { markerMap[markerValue.name] = marker; } markers.push(marker); return marker; } function recordMarker(fileName: string, location: ILocationInformation, name: string, markerMap: MarkerMap, markers: Marker[]): Marker { var marker: Marker = { fileName: fileName, position: location.position }; // Verify markers for uniqueness if (markerMap[name] !== undefined) { var message = "Marker '" + name + "' is duplicated in the source file contents."; reportError(marker.fileName, location.sourceLine, location.sourceColumn, message); return null; } else { markerMap[name] = marker; markers.push(marker); return marker; } } function parseFileContent(content: string, fileName: string, markerMap: MarkerMap, markers: Marker[], ranges: Range[]): FourSlashFile { content = chompLeadingSpace(content); // Any slash-star comment with a character not in this string is not a marker. var validMarkerChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_'; /// The file content (minus metacharacters) so far var output: string = ""; /// The current marker (or maybe multi-line comment?) we're parsing, possibly var openMarker: ILocationInformation = null; /// A stack of the open range markers that are still unclosed var openRanges: IRangeLocationInformation[] = []; /// A list of ranges we've collected so far */ var localRanges: Range[] = []; /// The latest position of the start of an unflushed plaintext area var lastNormalCharPosition: number = 0; /// The total number of metacharacters removed from the file (so far) var difference: number = 0; /// The fourslash file state object we are generating var state: State = State.none; /// Current position data var line: number = 1; var column: number = 1; var flush = (lastSafeCharIndex: number) => { if (lastSafeCharIndex === undefined) { output = output + content.substr(lastNormalCharPosition); } else { output = output + content.substr(lastNormalCharPosition, lastSafeCharIndex - lastNormalCharPosition); } }; if (content.length > 0) { var previousChar = content.charAt(0); for (var i = 1; i < content.length; i++) { var currentChar = content.charAt(i); switch (state) { case State.none: if (previousChar === "[" && currentChar === "|") { // found a range start openRanges.push({ position: (i - 1) - difference, sourcePosition: i - 1, sourceLine: line, sourceColumn: column, }); // copy all text up to marker position flush(i - 1); lastNormalCharPosition = i + 1; difference += 2; } else if (previousChar === "|" && currentChar === "]") { // found a range end var rangeStart = openRanges.pop(); if (!rangeStart) { reportError(fileName, line, column, "Found range end with no matching start."); } var range: Range = { fileName: fileName, start: rangeStart.position, end: (i - 1) - difference, marker: rangeStart.marker }; localRanges.push(range); // copy all text up to range marker position flush(i - 1); lastNormalCharPosition = i + 1; difference += 2; } else if (previousChar === "/" && currentChar === "*") { // found a possible marker start state = State.inSlashStarMarker; openMarker = { position: (i - 1) - difference, sourcePosition: i - 1, sourceLine: line, sourceColumn: column, }; } else if (previousChar === "{" && currentChar === "|") { // found an object marker start state = State.inObjectMarker; openMarker = { position: (i - 1) - difference, sourcePosition: i - 1, sourceLine: line, sourceColumn: column, }; flush(i - 1); } break; case State.inObjectMarker: // Object markers are only ever terminated by |} and have no content restrictions if (previousChar === "|" && currentChar === "}") { // Record the marker var objectMarkerNameText = content.substring(openMarker.sourcePosition + 2, i - 1).trim(); var marker = recordObjectMarker(fileName, openMarker, objectMarkerNameText, markerMap, markers); if (openRanges.length > 0) { openRanges[openRanges.length - 1].marker = marker; } // Set the current start to point to the end of the current marker to ignore its text lastNormalCharPosition = i + 1; difference += i + 1 - openMarker.sourcePosition; // Reset the state openMarker = null; state = State.none; } break; case State.inSlashStarMarker: if (previousChar === "*" && currentChar === "/") { // Record the marker // start + 2 to ignore the */, -1 on the end to ignore the * (/ is next) var markerNameText = content.substring(openMarker.sourcePosition + 2, i - 1).trim(); var marker = recordMarker(fileName, openMarker, markerNameText, markerMap, markers); if (openRanges.length > 0) { openRanges[openRanges.length - 1].marker = marker; } // Set the current start to point to the end of the current marker to ignore its text flush(openMarker.sourcePosition); lastNormalCharPosition = i + 1; difference += i + 1 - openMarker.sourcePosition; // Reset the state openMarker = null; state = State.none; } else if (validMarkerChars.indexOf(currentChar) < 0) { if (currentChar === '*' && i < content.length - 1 && content.charAt(i + 1) === '/') { // The marker is about to be closed, ignore the 'invalid' char } else { // We've hit a non-valid marker character, so we were actually in a block comment // Bail out the text we've gathered so far back into the output flush(i); lastNormalCharPosition = i; openMarker = null; state = State.none; } } break; } if (currentChar === '\n' && previousChar === '\r') { // Ignore trailing \n after a \r continue; } else if (currentChar === '\n' || currentChar === '\r') { line++; column = 1; continue; } column++; previousChar = currentChar; } } // Add the remaining text flush(undefined); if (openRanges.length > 0) { var openRange = openRanges[0]; reportError(fileName, openRange.sourceLine, openRange.sourceColumn, "Unterminated range."); } if (openMarker !== null) { reportError(fileName, openMarker.sourceLine, openMarker.sourceColumn, "Unterminated marker."); } // put ranges in the correct order localRanges = localRanges.sort((a, b) => a.start < b.start ? -1 : 1); localRanges.forEach((r) => { ranges.push(r); }); return { content: output, fileOptions: {}, version: 0, fileName: fileName }; } }