Merge pull request #17669 from Microsoft/builder

Improvements to tsc --watch
This commit is contained in:
Sheetal Nandi 2017-08-30 17:35:18 -07:00 committed by GitHub
commit 9e570c375b
44 changed files with 5770 additions and 2002 deletions

View file

@ -91,6 +91,7 @@ var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, "
var harnessCoreSources = [
"harness.ts",
"virtualFileSystem.ts",
"virtualFileSystemWithWatch.ts",
"sourceMapRecorder.ts",
"harnessLanguageService.ts",
"fourslash.ts",
@ -128,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([
"convertCompilerOptionsFromJson.ts",
"convertTypeAcquisitionFromJson.ts",
"tsserverProjectSystem.ts",
"tscWatchMode.ts",
"compileOnSave.ts",
"typingsInstaller.ts",
"projectErrors.ts",

492
src/compiler/builder.ts Normal file
View file

@ -0,0 +1,492 @@
/// <reference path="program.ts" />
namespace ts {
export interface EmitOutput {
outputFiles: OutputFile[];
emitSkipped: boolean;
}
export interface EmitOutputDetailed extends EmitOutput {
diagnostics: Diagnostic[];
sourceMaps: SourceMapData[];
emittedSourceFiles: SourceFile[];
}
export interface OutputFile {
name: string;
writeByteOrderMark: boolean;
text: string;
}
export interface ChangedProgramFiles {
/** Minimal set of list of files that require emit */
readonly filesToEmit: ReadonlyArray<string>;
/** File paths of source files changed/added/removed or affected by changed files */
readonly changedFiles: ReadonlyArray<string>;
}
export interface Builder {
/**
* This is the callback when file infos in the builder are updated
*/
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
getFilesAffectedBy(program: Program, path: Path): string[];
emitFile(program: Program, path: Path): EmitOutput;
/** Emit the changed files and clear the cache of the changed files */
emitChangedFiles(program: Program): EmitOutputDetailed[];
/** Get the changed files since last query and then clear the cache of changed files */
getChangedProgramFiles(program: Program): ChangedProgramFiles;
/** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */
getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[];
/** Called to reset the status of the builder */
clear(): void;
}
interface EmitHandler {
addScriptInfo(program: Program, sourceFile: SourceFile): void;
removeScriptInfo(path: Path): void;
updateScriptInfo(program: Program, sourceFile: SourceFile): void;
/**
* Gets the files affected by the script info which has updated shape from the known one
*/
getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[];
}
export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean,
cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed {
const outputFiles: OutputFile[] = [];
let emittedSourceFiles: SourceFile[];
const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
if (!isDetailed) {
return { outputFiles, emitSkipped: emitResult.emitSkipped };
}
return {
outputFiles,
emitSkipped: emitResult.emitSkipped,
diagnostics: emitResult.diagnostics,
sourceMaps: emitResult.sourceMaps,
emittedSourceFiles
};
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) {
outputFiles.push({ name: fileName, writeByteOrderMark, text });
if (isDetailed) {
emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles);
}
}
}
export function createBuilder(
getCanonicalFileName: (fileName: string) => string,
getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed,
computeHash: (data: string) => string,
shouldEmitFile: (sourceFile: SourceFile) => boolean
): Builder {
let isModuleEmit: boolean | undefined;
// Last checked shape signature for the file info
type FileInfo = { fileName: string; version: string; signature: string; };
const fileInfos = createMap<FileInfo>();
const semanticDiagnosticsPerFile = createMap<Diagnostic[]>();
/** The map has key by source file's path that has been changed */
const changedFileNames = createMap<string>();
let emitHandler: EmitHandler;
return {
onProgramUpdateGraph,
getFilesAffectedBy,
emitFile,
emitChangedFiles,
getChangedProgramFiles,
getSemanticDiagnostics,
clear
};
function createProgramGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None;
if (isModuleEmit !== currentIsModuleEmit) {
isModuleEmit = currentIsModuleEmit;
emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler();
fileInfos.clear();
semanticDiagnosticsPerFile.clear();
}
mutateMap(
fileInfos,
arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path),
{
// Add new file info
createNewValue: (_path, sourceFile) => addNewFileInfo(program, sourceFile),
// Remove existing file info
onDeleteValue: removeExistingFileInfo,
// We will update in place instead of deleting existing value and adding new one
onExistingValue: (_key, existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution)
}
);
}
function registerChangedFile(path: Path, fileName: string) {
changedFileNames.set(path, fileName);
// All changed files need to re-evaluate its semantic diagnostics
semanticDiagnosticsPerFile.delete(path);
}
function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo {
registerChangedFile(sourceFile.path, sourceFile.fileName);
emitHandler.addScriptInfo(program, sourceFile);
return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined };
}
function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) {
registerChangedFile(path, existingFileInfo.fileName);
emitHandler.removeScriptInfo(path);
}
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
registerChangedFile(sourceFile.path, sourceFile.fileName);
existingInfo.version = sourceFile.version;
emitHandler.updateScriptInfo(program, sourceFile);
}
}
function ensureProgramGraph(program: Program) {
if (!emitHandler) {
createProgramGraph(program, returnFalse);
}
}
function onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
if (emitHandler) {
createProgramGraph(program, hasInvalidatedResolution);
}
}
function getFilesAffectedBy(program: Program, path: Path): string[] {
ensureProgramGraph(program);
const sourceFile = program.getSourceFile(path);
const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : [];
const info = fileInfos.get(path);
if (!info || !updateShapeSignature(program, sourceFile, info)) {
return singleFileResult;
}
Debug.assert(!!sourceFile);
return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult);
}
function emitFile(program: Program, path: Path) {
ensureProgramGraph(program);
if (!fileInfos.has(path)) {
return { outputFiles: [], emitSkipped: true };
}
return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false);
}
function enumerateChangedFilesSet(
program: Program,
onChangedFile: (fileName: string, path: Path) => void,
onAffectedFile: (fileName: string, sourceFile: SourceFile) => void
) {
changedFileNames.forEach((fileName, path) => {
onChangedFile(fileName, path as Path);
const affectedFiles = getFilesAffectedBy(program, path as Path);
for (const file of affectedFiles) {
onAffectedFile(file, program.getSourceFile(file));
}
});
}
function enumerateChangedFilesEmitOutput(
program: Program,
emitOnlyDtsFiles: boolean,
onChangedFile: (fileName: string, path: Path) => void,
onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void
) {
const seenFiles = createMap<SourceFile>();
enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => {
if (!seenFiles.has(fileName)) {
seenFiles.set(fileName, sourceFile);
if (sourceFile) {
// Any affected file shouldnt have the cached diagnostics
semanticDiagnosticsPerFile.delete(sourceFile.path);
const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed;
onEmitOutput(emitOutput, sourceFile);
// mark all the emitted source files as seen
if (emitOutput.emittedSourceFiles) {
for (const file of emitOutput.emittedSourceFiles) {
seenFiles.set(file.fileName, file);
}
}
}
}
});
}
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
ensureProgramGraph(program);
const result: EmitOutputDetailed[] = [];
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false,
/*onChangedFile*/ noop, emitOutput => result.push(emitOutput));
changedFileNames.clear();
return result;
}
function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] {
ensureProgramGraph(program);
// Ensure that changed files have cleared their respective
enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => {
if (sourceFile) {
semanticDiagnosticsPerFile.delete(sourceFile.path);
}
});
let diagnostics: Diagnostic[];
for (const sourceFile of program.getSourceFiles()) {
const path = sourceFile.path;
const cachedDiagnostics = semanticDiagnosticsPerFile.get(path);
// Report the semantic diagnostics from the cache if we already have those diagnostics present
if (cachedDiagnostics) {
diagnostics = concatenate(diagnostics, cachedDiagnostics);
}
else {
// Diagnostics werent cached, get them from program, and cache the result
const cachedDiagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken);
semanticDiagnosticsPerFile.set(path, cachedDiagnostics);
diagnostics = concatenate(diagnostics, cachedDiagnostics);
}
}
return diagnostics || emptyArray;
}
function getChangedProgramFiles(program: Program): ChangedProgramFiles {
ensureProgramGraph(program);
let filesToEmit: string[];
const changedFiles = createMap<string>();
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true,
// All the changed files are required to get diagnostics
(changedFileName, changedFilePath) => addFileForDiagnostics(changedFileName, changedFilePath),
// Emitted file is for emit as well as diagnostic
(_emitOutput, sourceFile) => {
(filesToEmit || (filesToEmit = [])).push(sourceFile.fileName);
addFileForDiagnostics(sourceFile.fileName, sourceFile.path);
});
changedFileNames.clear();
return {
filesToEmit: filesToEmit || emptyArray,
changedFiles: arrayFrom(changedFiles.values())
};
function addFileForDiagnostics(fileName: string, path: Path) {
changedFiles.set(path, fileName);
}
}
function clear() {
isModuleEmit = undefined;
emitHandler = undefined;
fileInfos.clear();
semanticDiagnosticsPerFile.clear();
changedFileNames.clear();
}
/**
* For script files that contains only ambient external modules, although they are not actually external module files,
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
* there are no point to rebuild all script files if these special files have changed. However, if any statement
* in the file is not ambient external module, we treat it as a regular script file.
*/
function containsOnlyAmbientModules(sourceFile: SourceFile) {
for (const statement of sourceFile.statements) {
if (!isModuleWithStringLiteralName(statement)) {
return false;
}
}
return true;
}
/**
* @return {boolean} indicates if the shape signature has changed since last update.
*/
function updateShapeSignature(program: Program, sourceFile: SourceFile, info: FileInfo) {
const prevSignature = info.signature;
let latestSignature: string;
if (sourceFile.isDeclarationFile) {
latestSignature = computeHash(sourceFile.text);
info.signature = latestSignature;
}
else {
const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false);
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
latestSignature = computeHash(emitOutput.outputFiles[0].text);
info.signature = latestSignature;
}
else {
latestSignature = prevSignature;
}
}
return !prevSignature || latestSignature !== prevSignature;
}
/**
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
*/
function getReferencedFiles(program: Program, sourceFile: SourceFile): Map<true> {
const referencedFiles = createMap<true>();
// We need to use a set here since the code can contain the same import twice,
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
if (sourceFile.imports && sourceFile.imports.length > 0) {
const checker: TypeChecker = program.getTypeChecker();
for (const importName of sourceFile.imports) {
const symbol = checker.getSymbolAtLocation(importName);
if (symbol && symbol.declarations && symbol.declarations[0]) {
const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]);
if (declarationSourceFile) {
referencedFiles.set(declarationSourceFile.path, true);
}
}
}
}
const sourceFileDirectory = getDirectoryPath(sourceFile.path);
// Handle triple slash references
if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
for (const referencedFile of sourceFile.referencedFiles) {
const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName);
referencedFiles.set(referencedPath, true);
}
}
// Handle type reference directives
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => {
if (!resolvedTypeReferenceDirective) {
return;
}
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName);
referencedFiles.set(typeFilePath, true);
});
}
return referencedFiles;
}
/**
* Gets all the emittable files from the program
*/
function getAllEmittableFiles(program: Program) {
const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions());
const sourceFiles = program.getSourceFiles();
const result: string[] = [];
for (const sourceFile of sourceFiles) {
if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && shouldEmitFile(sourceFile)) {
result.push(sourceFile.fileName);
}
}
return result;
}
function getNonModuleEmitHandler(): EmitHandler {
return {
addScriptInfo: noop,
removeScriptInfo: noop,
updateScriptInfo: noop,
getFilesAffectedByUpdatedShape
};
function getFilesAffectedByUpdatedShape(program: Program, _sourceFile: SourceFile, singleFileResult: string[]): string[] {
const options = program.getCompilerOptions();
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
// so returning the file itself is good enough.
if (options && (options.out || options.outFile)) {
return singleFileResult;
}
return getAllEmittableFiles(program);
}
}
function getModuleEmitHandler(): EmitHandler {
const references = createMap<Map<true>>();
return {
addScriptInfo: setReferences,
removeScriptInfo,
updateScriptInfo: setReferences,
getFilesAffectedByUpdatedShape
};
function setReferences(program: Program, sourceFile: SourceFile) {
references.set(sourceFile.path, getReferencedFiles(program, sourceFile));
}
function removeScriptInfo(removedFilePath: Path) {
// Remove existing references
references.forEach((referencesInFile, filePath) => {
if (referencesInFile.has(removedFilePath)) {
// add files referencing the removedFilePath, as changed files too
const referencedByInfo = fileInfos.get(filePath);
if (referencedByInfo) {
registerChangedFile(filePath as Path, referencedByInfo.fileName);
}
}
});
// Delete the entry for the removed file path
references.delete(removedFilePath);
}
function getReferencedByPaths(referencedFilePath: Path) {
return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) =>
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
);
}
function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] {
if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) {
return getAllEmittableFiles(program);
}
const options = program.getCompilerOptions();
if (options && (options.isolatedModules || options.out || options.outFile)) {
return singleFileResult;
}
// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
const seenFileNamesMap = createMap<string>();
const setSeenFileName = (path: Path, sourceFile: SourceFile) => {
seenFileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined);
};
// Start with the paths this file was referenced by
const path = sourceFile.path;
setSeenFileName(path, sourceFile);
const queue = getReferencedByPaths(path);
while (queue.length > 0) {
const currentPath = queue.pop();
if (!seenFileNamesMap.has(currentPath)) {
const currentSourceFile = program.getSourceFileByPath(currentPath);
if (currentSourceFile && updateShapeSignature(program, currentSourceFile, fileInfos.get(currentPath))) {
queue.push(...getReferencedByPaths(currentPath));
}
setSeenFileName(currentPath, currentSourceFile);
}
}
// Return array of values that needs emit
return arrayFrom(seenFileNamesMap.values());
}
}
}
}

View file

@ -1166,7 +1166,7 @@ namespace ts {
}
function diagnosticName(nameArg: __String | Identifier) {
return typeof nameArg === "string" ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
}
function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) {

View file

@ -885,7 +885,7 @@ namespace ts {
*/
export function readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { config?: any; error?: Diagnostic } {
const textOrDiagnostic = tryReadFile(fileName, readFile);
return typeof textOrDiagnostic === "string" ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
}
/**
@ -907,7 +907,7 @@ namespace ts {
*/
export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): JsonSourceFile {
const textOrDiagnostic = tryReadFile(fileName, readFile);
return typeof textOrDiagnostic === "string" ? parseJsonText(fileName, textOrDiagnostic) : <JsonSourceFile>{ parseDiagnostics: [textOrDiagnostic] };
return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : <JsonSourceFile>{ parseDiagnostics: [textOrDiagnostic] };
}
function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | Diagnostic {
@ -1111,9 +1111,9 @@ namespace ts {
if (!isDoubleQuotedString(valueExpression)) {
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected));
}
reportInvalidOptionValue(option && (typeof option.type === "string" && option.type !== "string"));
reportInvalidOptionValue(option && (isString(option.type) && option.type !== "string"));
const text = (<StringLiteral>valueExpression).text;
if (option && typeof option.type !== "string") {
if (option && !isString(option.type)) {
const customOption = <CommandLineOptionOfCustomType>option;
// Validate custom option type
if (!customOption.type.has(text.toLowerCase())) {
@ -1184,7 +1184,7 @@ namespace ts {
function getCompilerOptionValueTypeString(option: CommandLineOption) {
return option.type === "list" ?
"Array" :
typeof option.type === "string" ? option.type : "string";
isString(option.type) ? option.type : "string";
}
function isCompilerOptionsValue(option: CommandLineOption, value: any): value is CompilerOptionsValue {
@ -1192,7 +1192,7 @@ namespace ts {
if (option.type === "list") {
return isArray(value);
}
const expectedType = typeof option.type === "string" ? option.type : "string";
const expectedType = isString(option.type) ? option.type : "string";
return typeof value === expectedType;
}
}
@ -1578,7 +1578,7 @@ namespace ts {
let extendedConfigPath: Path;
if (json.extends) {
if (typeof json.extends !== "string") {
if (!isString(json.extends)) {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"));
}
else {
@ -1803,7 +1803,7 @@ namespace ts {
if (optType === "list" && isArray(value)) {
return convertJsonOptionOfListType(<CommandLineOptionOfListType>opt, value, basePath, errors);
}
else if (typeof optType !== "string") {
else if (!isString(optType)) {
return convertJsonOptionOfCustomType(<CommandLineOptionOfCustomType>opt, <string>value, errors);
}
return normalizeNonListOptionValue(opt, basePath, value);
@ -1816,13 +1816,13 @@ namespace ts {
function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue {
if (option.type === "list") {
const listOption = <CommandLineOptionOfListType>option;
if (listOption.element.isFilePath || typeof listOption.element.type !== "string") {
if (listOption.element.isFilePath || !isString(listOption.element.type)) {
return <CompilerOptionsValue>filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v);
}
return value;
}
else if (typeof option.type !== "string") {
return option.type.get(typeof value === "string" ? value.toLowerCase() : value);
else if (!isString(option.type)) {
return option.type.get(isString(value) ? value.toLowerCase() : value);
}
return normalizeNonListOptionValue(option, basePath, value);
}
@ -1984,7 +1984,7 @@ namespace ts {
* @param host The host used to resolve files and directories.
* @param extraFileExtensions optionaly file extra file extension information from host
*/
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo>): ExpandResult {
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo> = []): ExpandResult {
basePath = normalizePath(basePath);
const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper;

View file

@ -1202,6 +1202,13 @@ namespace ts {
return Array.isArray ? Array.isArray(value) : value instanceof Array;
}
/**
* Tests whether a value is string
*/
export function isString(text: any): text is string {
return typeof text === "string";
}
export function tryCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined {
return value !== undefined && test(value) ? value : undefined;
}
@ -1212,7 +1219,10 @@ namespace ts {
}
/** Does nothing. */
export function noop(): void {}
export function noop(): void { }
/** Do nothing and return false */
export function returnFalse(): false { return false; }
/** Throws an error because a function is not implemented. */
export function notImplemented(): never {
@ -1455,16 +1465,16 @@ namespace ts {
function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison {
while (text1 && text2) {
// We still have both chains.
const string1 = typeof text1 === "string" ? text1 : text1.messageText;
const string2 = typeof text2 === "string" ? text2 : text2.messageText;
const string1 = isString(text1) ? text1 : text1.messageText;
const string2 = isString(text2) ? text2 : text2.messageText;
const res = compareValues(string1, string2);
if (res) {
return res;
}
text1 = typeof text1 === "string" ? undefined : text1.next;
text2 = typeof text2 === "string" ? undefined : text2.next;
text1 = isString(text1) ? undefined : text1.next;
text2 = isString(text2) ? undefined : text2.next;
}
if (!text1 && !text2) {
@ -2066,8 +2076,8 @@ namespace ts {
}
export interface FileSystemEntries {
files: ReadonlyArray<string>;
directories: ReadonlyArray<string>;
readonly files: ReadonlyArray<string>;
readonly directories: ReadonlyArray<string>;
}
export interface FileMatcherPatterns {
@ -2620,4 +2630,204 @@ namespace ts {
export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs;
}
export interface HostForCaching extends PartialSystem {
useCaseSensitiveFileNames: boolean;
}
export interface CachedHost {
addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path): void;
addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void;
clearCache(): void;
}
export interface CachedPartialSystem extends PartialSystem, CachedHost {
}
interface MutableFileSystemEntries {
readonly files: string[];
readonly directories: string[];
}
export function createCachedPartialSystem(host: HostForCaching): CachedPartialSystem {
const cachedReadDirectoryResult = createMap<MutableFileSystemEntries>();
const getCurrentDirectory = memoize(() => host.getCurrentDirectory());
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
return {
writeFile,
fileExists,
directoryExists,
createDirectory,
getCurrentDirectory,
getDirectories,
readDirectory,
addOrDeleteFileOrFolder,
addOrDeleteFile,
clearCache
};
function toPath(fileName: string) {
return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName);
}
function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined {
return cachedReadDirectoryResult.get(rootDirPath);
}
function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined {
return getCachedFileSystemEntries(getDirectoryPath(path));
}
function getBaseNameOfFileName(fileName: string) {
return getBaseFileName(normalizePath(fileName));
}
function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) {
const resultFromHost: MutableFileSystemEntries = {
files: map(host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [],
directories: host.getDirectories(rootDir) || []
};
cachedReadDirectoryResult.set(rootDirPath, resultFromHost);
return resultFromHost;
}
/**
* If the readDirectory result was already cached, it returns that
* Otherwise gets result from host and caches it.
* The host request is done under try catch block to avoid caching incorrect result
*/
function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined {
const cachedResult = getCachedFileSystemEntries(rootDirPath);
if (cachedResult) {
return cachedResult;
}
try {
return createCachedFileSystemEntries(rootDir, rootDirPath);
}
catch (_e) {
// If there is exception to read directories, dont cache the result and direct the calls to host
Debug.assert(!cachedReadDirectoryResult.has(rootDirPath));
return undefined;
}
}
function fileNameEqual(name1: string, name2: string) {
return getCanonicalFileName(name1) === getCanonicalFileName(name2);
}
function hasEntry(entries: ReadonlyArray<string>, name: string) {
return some(entries, file => fileNameEqual(file, name));
}
function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) {
if (hasEntry(entries, baseName)) {
if (!isValid) {
return filterMutate(entries, entry => !fileNameEqual(entry, baseName));
}
}
else if (isValid) {
return entries.push(baseName);
}
}
function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void {
const path = toPath(fileName);
const result = getCachedFileSystemEntriesForBaseDir(path);
if (result) {
updateFilesOfFileSystemEntry(result, getBaseNameOfFileName(fileName), /*fileExists*/ true);
}
return host.writeFile(fileName, data, writeByteOrderMark);
}
function fileExists(fileName: string): boolean {
const path = toPath(fileName);
const result = getCachedFileSystemEntriesForBaseDir(path);
return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) ||
host.fileExists(fileName);
}
function directoryExists(dirPath: string): boolean {
const path = toPath(dirPath);
return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath);
}
function createDirectory(dirPath: string) {
const path = toPath(dirPath);
const result = getCachedFileSystemEntriesForBaseDir(path);
const baseFileName = getBaseNameOfFileName(dirPath);
if (result) {
updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true);
}
host.createDirectory(dirPath);
}
function getDirectories(rootDir: string): string[] {
const rootDirPath = toPath(rootDir);
const result = tryReadDirectory(rootDir, rootDirPath);
if (result) {
return result.directories.slice();
}
return host.getDirectories(rootDir);
}
function readDirectory(rootDir: string, extensions?: ReadonlyArray<string>, excludes?: ReadonlyArray<string>, includes?: ReadonlyArray<string>, depth?: number): string[] {
const rootDirPath = toPath(rootDir);
const result = tryReadDirectory(rootDir, rootDirPath);
if (result) {
return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, getFileSystemEntries);
}
return host.readDirectory(rootDir, extensions, excludes, includes, depth);
function getFileSystemEntries(dir: string) {
const path = toPath(dir);
if (path === rootDirPath) {
return result;
}
return getCachedFileSystemEntries(path) || createCachedFileSystemEntries(dir, path);
}
}
function addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path) {
const existingResult = getCachedFileSystemEntries(fileOrFolderPath);
if (existingResult) {
// This was a folder already present, remove it if this doesnt exist any more
if (!host.directoryExists(fileOrFolder)) {
cachedReadDirectoryResult.delete(fileOrFolderPath);
}
}
else {
// This was earlier a file (hence not in cached directory contents)
// or we never cached the directory containing it
const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrFolderPath);
if (parentResult) {
const baseName = getBaseNameOfFileName(fileOrFolder);
if (parentResult) {
updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrFolderPath));
updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrFolderPath));
}
}
}
}
function addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind) {
if (eventKind === FileWatcherEventKind.Changed) {
return;
}
const parentResult = getCachedFileSystemEntriesForBaseDir(filePath);
if (parentResult) {
updateFilesOfFileSystemEntry(parentResult, getBaseNameOfFileName(fileName), eventKind === FileWatcherEventKind.Created);
}
}
function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) {
updateFileSystemEntry(parentResult.files, baseName, fileExists);
}
function clearCache() {
cachedReadDirectoryResult.clear();
}
}
}

View file

@ -81,7 +81,7 @@ namespace ts {
if (typeof value === "boolean") {
return value ? createTrue() : createFalse();
}
if (typeof value === "string") {
if (isString(value)) {
return createStringLiteral(value);
}
return createLiteralFromNode(value);
@ -2130,7 +2130,7 @@ namespace ts {
export function createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block) {
const node = <CatchClause>createSynthesizedNode(SyntaxKind.CatchClause);
node.variableDeclaration = typeof variableDeclaration === "string" ? createVariableDeclaration(variableDeclaration) : variableDeclaration;
node.variableDeclaration = isString(variableDeclaration) ? createVariableDeclaration(variableDeclaration) : variableDeclaration;
node.block = block;
return node;
}
@ -2438,11 +2438,11 @@ namespace ts {
function asName(name: string | EntityName): EntityName;
function asName(name: string | Identifier | ThisTypeNode): Identifier | ThisTypeNode;
function asName(name: string | Identifier | BindingName | PropertyName | QualifiedName | ThisTypeNode) {
return typeof name === "string" ? createIdentifier(name) : name;
return isString(name) ? createIdentifier(name) : name;
}
function asExpression(value: string | number | Expression) {
return typeof value === "string" || typeof value === "number" ? createLiteral(value) : value;
return isString(value) || typeof value === "number" ? createLiteral(value) : value;
}
function asNodeArray<T extends Node>(array: ReadonlyArray<T> | undefined): NodeArray<T> | undefined {

View file

@ -94,7 +94,7 @@ namespace ts {
}
const fileName = jsonContent[fieldName];
if (typeof fileName !== "string") {
if (!isString(fileName)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof fileName);
}
@ -659,8 +659,8 @@ namespace ts {
}
if (matchedPattern) {
const matchedStar = typeof matchedPattern === "string" ? undefined : matchedText(matchedPattern, moduleName);
const matchedPatternText = typeof matchedPattern === "string" ? matchedPattern : patternText(matchedPattern);
const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName);
const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern);
if (state.traceEnabled) {
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText);
}

View file

@ -1,6 +1,7 @@
/// <reference path="sys.ts" />
/// <reference path="emitter.ts" />
/// <reference path="core.ts" />
/// <reference path="builder.ts" />
namespace ts {
const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/;
@ -227,19 +228,25 @@ namespace ts {
let output = "";
for (const diagnostic of diagnostics) {
if (diagnostic.file) {
const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
const fileName = diagnostic.file.fileName;
const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName));
output += `${relativeFileName}(${line + 1},${character + 1}): `;
}
const category = DiagnosticCategory[diagnostic.category].toLowerCase();
output += `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`;
output += formatDiagnostic(diagnostic, host);
}
return output;
}
export function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string {
const category = DiagnosticCategory[diagnostic.category].toLowerCase();
const errorMessage = `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`;
if (diagnostic.file) {
const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
const fileName = diagnostic.file.fileName;
const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName));
return `${relativeFileName}(${line + 1},${character + 1}): ` + errorMessage;
}
return errorMessage;
}
const redForegroundEscapeSequence = "\u001b[91m";
const yellowForegroundEscapeSequence = "\u001b[93m";
const blueForegroundEscapeSequence = "\u001b[93m";
@ -337,7 +344,7 @@ namespace ts {
}
export function flattenDiagnosticMessageText(messageText: string | DiagnosticMessageChain, newLine: string): string {
if (typeof messageText === "string") {
if (isString(messageText)) {
return messageText;
}
else {
@ -386,6 +393,142 @@ namespace ts {
allDiagnostics?: Diagnostic[];
}
export function isProgramUptoDate(program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions,
getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution): boolean {
// If we haven't create a program yet, then it is not up-to-date
if (!program) {
return false;
}
// If number of files in the program do not match, it is not up-to-date
if (program.getRootFileNames().length !== rootFileNames.length) {
return false;
}
// If any file is not up-to-date, then the whole program is not up-to-date
if (program.getSourceFiles().some(sourceFileNotUptoDate)) {
return false;
}
// If any of the missing file paths are now created
if (program.getMissingFilePaths().some(fileExists)) {
return false;
}
const currentOptions = program.getCompilerOptions();
// If the compilation settings do no match, then the program is not up-to-date
if (!compareDataObjects(currentOptions, newOptions)) {
return false;
}
// If everything matches but the text of config file is changed,
// error locations can change for program options, so update the program
if (currentOptions.configFile && newOptions.configFile) {
return currentOptions.configFile.text === newOptions.configFile.text;
}
return true;
function sourceFileNotUptoDate(sourceFile: SourceFile): boolean {
return sourceFile.version !== getSourceVersion(sourceFile.path) ||
hasInvalidatedResolution(sourceFile.path);
}
}
/**
* Determined if source file needs to be re-created even if its text hasnt changed
*/
function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) {
// If any of these options change, we cant reuse old source file even if version match
// The change in options like these could result in change in syntax tree change
const oldOptions = program && program.getCompilerOptions();
return oldOptions &&
(oldOptions.target !== newOptions.target ||
oldOptions.module !== newOptions.module ||
oldOptions.moduleResolution !== newOptions.moduleResolution ||
oldOptions.noResolve !== newOptions.noResolve ||
oldOptions.jsx !== newOptions.jsx ||
oldOptions.allowJs !== newOptions.allowJs ||
oldOptions.disableSizeLimit !== newOptions.disableSizeLimit ||
oldOptions.baseUrl !== newOptions.baseUrl ||
!equalOwnProperties(oldOptions.paths, newOptions.paths));
}
/**
* Updates the existing missing file watches with the new set of missing files after new program is created
*/
export function updateMissingFilePathsWatch(
program: Program,
missingFileWatches: Map<FileWatcher>,
createMissingFileWatch: (missingFilePath: Path) => FileWatcher,
closeExistingMissingFilePathFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void
) {
const missingFilePaths = program.getMissingFilePaths();
const newMissingFilePathMap = arrayToSet(missingFilePaths);
// Update the missing file paths watcher
mutateMap(
missingFileWatches,
newMissingFilePathMap,
{
// Watch the missing files
createNewValue: createMissingFileWatch,
// Files that are no longer missing (e.g. because they are no longer required)
// should no longer be watched.
onDeleteValue: closeExistingMissingFilePathFileWatcher
}
);
}
export interface WildcardDirectoryWatcher {
watcher: FileWatcher;
flags: WatchDirectoryFlags;
}
/**
* Updates the existing wild card directory watches with the new set of wild card directories from the config file
* after new program is created because the config file was reloaded or program was created first time from the config file
* Note that there is no need to call this function when the program is updated with additional files without reloading config files,
* as wildcard directories wont change unless reloading config file
*/
export function updateWatchingWildcardDirectories(
existingWatchedForWildcards: Map<WildcardDirectoryWatcher>,
wildcardDirectories: Map<WatchDirectoryFlags>,
watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher,
closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flagsChanged: boolean) => void
) {
mutateMap(
existingWatchedForWildcards,
wildcardDirectories,
{
// Create new watch and recursive info
createNewValue: createWildcardDirectoryWatcher,
// Close existing watch thats not needed any more
onDeleteValue: (directory, wildcardDirectoryWatcher) =>
closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ false),
// Close existing watch that doesnt match in the flags
onExistingValue: updateWildcardDirectoryWatcher
}
);
function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher {
// Create new watch and recursive info
return {
watcher: watchDirectory(directory, flags),
flags
};
}
function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags) {
// Watcher needs to be updated if the recursive flags dont match
if (wildcardDirectoryWatcher.flags === flags) {
return;
}
closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ true);
existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags));
}
}
/**
* Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions'
* that represent a compilation unit.
@ -446,6 +589,7 @@ namespace ts {
let moduleResolutionCache: ModuleResolutionCache;
let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[];
const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
if (host.resolveModuleNames) {
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile).map(resolved => {
// An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName.
@ -482,10 +626,12 @@ namespace ts {
let redirectTargetsSet = createMap<true>();
const filesByName = createMap<SourceFile | undefined>();
let missingFilePaths: ReadonlyArray<Path>;
// stores 'filename -> file association' ignoring case
// used to track cases when two file names differ only in casing
const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap<SourceFile>() : undefined;
const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
const structuralIsReused = tryReuseStructureFromOldProgram();
if (structuralIsReused !== StructureIsReused.Completely) {
forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false));
@ -520,13 +666,26 @@ namespace ts {
});
}
}
missingFilePaths = arrayFrom(filesByName.keys(), p => <Path>p).filter(p => !filesByName.get(p));
}
const missingFilePaths = arrayFrom(filesByName.keys(), p => <Path>p).filter(p => !filesByName.get(p));
Debug.assert(!!missingFilePaths);
// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
moduleResolutionCache = undefined;
// Release any files we have acquired in the old program but are
// not part of the new program.
if (oldProgram && host.onReleaseOldSourceFile) {
const oldSourceFiles = oldProgram.getSourceFiles();
for (const oldSourceFile of oldSourceFiles) {
if (!getSourceFile(oldSourceFile.path) || shouldCreateNewSourceFile) {
host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions());
}
}
}
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
oldProgram = undefined;
@ -682,7 +841,7 @@ namespace ts {
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
}
}
else {
else if (!hasInvalidatedResolution(oldProgramState.file.path)) {
resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
}
@ -784,14 +943,21 @@ namespace ts {
const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = [];
oldProgram.structureIsReused = StructureIsReused.Completely;
// If the missing file paths are now present, it can change the progam structure,
// and hence cant reuse the structure.
// This is same as how we dont reuse the structure if one of the file from old program is now missing
if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) {
return oldProgram.structureIsReused = StructureIsReused.Not;
}
const oldSourceFiles = oldProgram.getSourceFiles();
const enum SeenPackageName { Exists, Modified }
const seenPackageNames = createMap<SeenPackageName>();
for (const oldSourceFile of oldSourceFiles) {
let newSourceFile = host.getSourceFileByPath
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target)
: host.getSourceFile(oldSourceFile.fileName, options.target);
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target, /*onError*/ undefined, shouldCreateNewSourceFile)
: host.getSourceFile(oldSourceFile.fileName, options.target, /*onError*/ undefined, shouldCreateNewSourceFile);
if (!newSourceFile) {
return oldProgram.structureIsReused = StructureIsReused.Not;
@ -874,6 +1040,13 @@ namespace ts {
// tentatively approve the file
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
}
else if (hasInvalidatedResolution(oldSourceFile.path)) {
// 'module/types' references could have changed
oldProgram.structureIsReused = StructureIsReused.SafeModules;
// add file to the modified list so that we will resolve it later
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
}
// if file has passed all checks it should be safe to reuse it
newSourceFiles.push(newSourceFile);
@ -920,20 +1093,7 @@ namespace ts {
return oldProgram.structureIsReused;
}
// If a file has ceased to be missing, then we need to discard some of the old
// structure in order to pick it up.
// Caution: if the file has created and then deleted between since it was discovered to
// be missing, then the corresponding file watcher will have been closed and no new one
// will be created until we encounter a change that prevents complete structure reuse.
// During this interval, creation of the file will go unnoticed. We expect this to be
// both rare and low-impact.
if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) {
return oldProgram.structureIsReused = StructureIsReused.SafeModules;
}
for (const p of oldProgram.getMissingFilePaths()) {
filesByName.set(p, undefined);
}
missingFilePaths = oldProgram.getMissingFilePaths();
// update fileName -> file mapping
for (let i = 0; i < newSourceFiles.length; i++) {
@ -1670,7 +1830,7 @@ namespace ts {
else {
fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage));
}
});
}, shouldCreateNewSourceFile);
if (packageId) {
const packageIdKey = `${packageId.name}@${packageId.version}`;

View file

@ -0,0 +1,304 @@
/// <reference path="types.ts"/>
/// <reference path="core.ts"/>
/*@internal*/
namespace ts {
/** This is the cache of module/typedirectives resolution that can be retained across program */
export interface ResolutionCache {
setModuleResolutionHost(host: ModuleResolutionHost): void;
startRecordingFilesWithChangedResolutions(): void;
finishRecordingFilesWithChangedResolutions(): Path[];
resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[];
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
invalidateResolutionOfFile(filePath: Path): void;
invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path): void;
createHasInvalidatedResolution(): HasInvalidatedResolution;
clear(): void;
}
interface NameResolutionWithFailedLookupLocations {
readonly failedLookupLocations: ReadonlyArray<string>;
isInvalidated?: boolean;
}
interface FailedLookupLocationsWatcher {
fileWatcher: FileWatcher;
refCount: number;
}
export function createResolutionCache(
toPath: (fileName: string) => Path,
getCompilerOptions: () => CompilerOptions,
watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => FileWatcher,
log: (s: string) => void,
projectName?: string,
getGlobalCache?: () => string | undefined): ResolutionCache {
let host: ModuleResolutionHost;
let filesWithChangedSetOfUnresolvedImports: Path[] | undefined;
let filesWithInvalidatedResolutions: Map<true> | undefined;
// The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file.
// The key in the map is source file's path.
// The values are Map of resolutions with key being name lookedup.
const resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
const failedLookupLocationsWatches = createMap<FailedLookupLocationsWatcher>();
return {
setModuleResolutionHost,
startRecordingFilesWithChangedResolutions,
finishRecordingFilesWithChangedResolutions,
resolveModuleNames,
resolveTypeReferenceDirectives,
invalidateResolutionOfFile,
invalidateResolutionOfChangedFailedLookupLocation,
createHasInvalidatedResolution,
clear
};
function setModuleResolutionHost(updatedHost: ModuleResolutionHost) {
host = updatedHost;
}
function clear() {
// Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache
clearMap(failedLookupLocationsWatches, (failedLookupLocationPath, failedLookupLocationWatcher) => {
log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`);
failedLookupLocationWatcher.fileWatcher.close();
});
resolvedModuleNames.clear();
resolvedTypeReferenceDirectives.clear();
}
function startRecordingFilesWithChangedResolutions() {
filesWithChangedSetOfUnresolvedImports = [];
}
function finishRecordingFilesWithChangedResolutions() {
const collected = filesWithChangedSetOfUnresolvedImports;
filesWithChangedSetOfUnresolvedImports = undefined;
return collected;
}
function createHasInvalidatedResolution(): HasInvalidatedResolution {
const collected = filesWithInvalidatedResolutions;
filesWithInvalidatedResolutions = undefined;
return path => collected && collected.has(path);
}
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host);
// return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts
if (!getGlobalCache) {
return primaryResult;
}
// otherwise try to load typings from @types
const globalCache = getGlobalCache();
if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension))) {
// create different collection of failed lookup locations for second pass
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache);
if (resolvedModule) {
return { resolvedModule, failedLookupLocations: addRange(primaryResult.failedLookupLocations as Array<string>, failedLookupLocations) };
}
}
// Default return the result from the first pass
return primaryResult;
}
function resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
names: string[],
containingFile: string,
cache: Map<Map<T>>,
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined,
logChanges: boolean): R[] {
const path = toPath(containingFile);
const currentResolutionsInFile = cache.get(path);
const newResolutions: Map<T> = createMap<T>();
const resolvedModules: R[] = [];
const compilerOptions = getCompilerOptions();
for (const name of names) {
// check if this is a duplicate entry in the list
let resolution = newResolutions.get(name);
if (!resolution) {
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
if (moduleResolutionIsValid(existingResolution)) {
// ok, it is safe to use existing name resolution results
resolution = existingResolution;
}
else {
resolution = loader(name, containingFile, compilerOptions, host);
updateFailedLookupLocationWatches(containingFile, name, existingResolution && existingResolution.failedLookupLocations, resolution.failedLookupLocations);
}
newResolutions.set(name, resolution);
if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
filesWithChangedSetOfUnresolvedImports.push(path);
// reset log changes to avoid recording the same file multiple times
logChanges = false;
}
}
Debug.assert(resolution !== undefined);
resolvedModules.push(getResult(resolution));
}
// replace old results with a new one
cache.set(path, newResolutions);
return resolvedModules;
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
if (oldResolution === newResolution) {
return true;
}
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
return false;
}
const oldResult = getResult(oldResolution);
const newResult = getResult(newResolution);
if (oldResult === newResult) {
return true;
}
if (!oldResult || !newResult) {
return false;
}
return getResultFileName(oldResult) === getResultFileName(newResult);
}
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution || resolution.isInvalidated) {
return false;
}
const result = getResult(resolution);
if (result) {
return true;
}
// consider situation if we have no candidate locations as valid resolution.
// after all there is no point to invalidate it if we have no idea where to look for the module.
return resolution.failedLookupLocations.length === 0;
}
}
function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
}
function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] {
return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName,
m => m.resolvedModule, r => r.resolvedFileName, logChanges);
}
function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath);
if (failedLookupLocationWatcher) {
failedLookupLocationWatcher.refCount++;
log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name} refCount: ${failedLookupLocationWatcher.refCount}`);
}
else {
log(`Watcher: FailedLookupLocations: Status: new watch: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`);
const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name);
failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 });
}
}
function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath);
Debug.assert(!!failedLookupLocationWatcher);
failedLookupLocationWatcher.refCount--;
if (failedLookupLocationWatcher.refCount) {
log(`Watcher: FailedLookupLocations: Status: Removing existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}: refCount: ${failedLookupLocationWatcher.refCount}`);
}
else {
log(`Watcher: FailedLookupLocations: Status: Closing the file watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`);
failedLookupLocationWatcher.fileWatcher.close();
failedLookupLocationsWatches.delete(failedLookupLocationPath);
}
}
type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => void;
function withFailedLookupLocations(failedLookupLocations: ReadonlyArray<string>, containingFile: string, name: string, fn: FailedLookupLocationAction) {
forEach(failedLookupLocations, failedLookupLocation => {
fn(failedLookupLocation, toPath(failedLookupLocation), containingFile, name);
});
}
function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: ReadonlyArray<string> | undefined, failedLookupLocations: ReadonlyArray<string>) {
// Watch all the failed lookup locations
withFailedLookupLocations(failedLookupLocations, containingFile, name, watchFailedLookupLocation);
// Close existing watches for the failed locations
withFailedLookupLocations(existingFailedLookupLocations, containingFile, name, closeFailedLookupLocationWatcher);
}
function invalidateResolutionCacheOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
deletedFilePath: Path,
cache: Map<Map<T>>,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined) {
cache.forEach((value, path) => {
if (path === deletedFilePath) {
cache.delete(path);
value.forEach((resolution, name) => {
withFailedLookupLocations(resolution.failedLookupLocations, path, name, closeFailedLookupLocationWatcher);
});
}
else if (value) {
value.forEach(resolution => {
if (resolution && !resolution.isInvalidated) {
const result = getResult(resolution);
if (result) {
if (toPath(getResultFileName(result)) === deletedFilePath) {
resolution.isInvalidated = true;
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(path, true);
}
}
}
});
}
});
}
function invalidateResolutionCacheOfChangedFailedLookupLocation<T extends NameResolutionWithFailedLookupLocations>(
failedLookupLocationPath: Path,
cache: Map<Map<T>>) {
cache.forEach((value, containingFile) => {
if (value) {
value.forEach(resolution => {
if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, location => toPath(location) === failedLookupLocationPath)) {
// Mark the file as needing re-evaluation of module resolution instead of using it blindly.
resolution.isInvalidated = true;
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(containingFile, true);
}
});
}
});
}
function invalidateResolutionOfFile(filePath: Path) {
invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName);
invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
}
function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path) {
invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedModuleNames);
invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedTypeReferenceDirectives);
}
}
}

View file

@ -30,14 +30,26 @@ namespace ts {
mtime?: Date;
}
export interface System {
/**
* Partial interface of the System thats needed to support the caching of directory structure
*/
export interface PartialSystem {
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
fileExists(path: string): boolean;
directoryExists(path: string): boolean;
createDirectory(path: string): void;
getCurrentDirectory(): string;
getDirectories(path: string): string[];
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
}
export interface System extends PartialSystem {
args: string[];
newLine: string;
useCaseSensitiveFileNames: boolean;
write(s: string): void;
readFile(path: string, encoding?: string): string | undefined;
getFileSize?(path: string): number;
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
/**
* @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that
* use native OS file watching
@ -45,13 +57,7 @@ namespace ts {
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
resolvePath(path: string): string;
fileExists(path: string): boolean;
directoryExists(path: string): boolean;
createDirectory(path: string): void;
getExecutingFilePath(): string;
getCurrentDirectory(): string;
getDirectories(path: string): string[];
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
getModifiedTime?(path: string): Date;
/**
* This should be cryptographically secure.
@ -184,7 +190,7 @@ namespace ts {
function fileEventHandler(eventName: string, relativeFileName: string, baseDirPath: string) {
// When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined"
const fileName = typeof relativeFileName !== "string"
const fileName = !isString(relativeFileName)
? undefined
: ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath);
// Some applications save a working file via rename operations

View file

@ -1,48 +1,13 @@
/// <reference path="program.ts"/>
/// <reference path="watchedProgram.ts"/>
/// <reference path="commandLineParser.ts"/>
namespace ts {
export interface SourceFile {
fileWatcher?: FileWatcher;
}
interface Statistic {
name: string;
value: string;
}
const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = {
getCurrentDirectory: () => sys.getCurrentDirectory(),
getNewLine: () => sys.newLine,
getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames)
};
let reportDiagnosticWorker = reportDiagnosticSimply;
function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost) {
reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost);
}
function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): void {
for (const diagnostic of diagnostics) {
reportDiagnostic(diagnostic, host);
}
}
function reportEmittedFiles(files: string[]): void {
if (!files || files.length === 0) {
return;
}
const currentDir = sys.getCurrentDirectory();
for (const file of files) {
const filepath = getNormalizedAbsolutePath(file, currentDir);
sys.write(`TSFILE: ${filepath}${sys.newLine}`);
}
}
function countLines(program: Program): number {
let count = 0;
forEach(program.getSourceFiles(), file => {
@ -56,25 +21,11 @@ namespace ts {
return <string>diagnostic.messageText;
}
function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void {
sys.write(ts.formatDiagnostics([diagnostic], host));
}
function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void {
sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + sys.newLine);
}
function reportWatchDiagnostic(diagnostic: Diagnostic) {
let output = new Date().toLocaleTimeString() + " - ";
if (diagnostic.file) {
const loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `;
let reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticSimply);
function udpateReportDiagnostic(options: CompilerOptions) {
if (options.pretty) {
reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticWithColorAndContext);
}
output += `${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine + sys.newLine + sys.newLine }`;
sys.write(output);
}
function padLeft(s: string, length: number) {
@ -98,26 +49,12 @@ namespace ts {
export function executeCommandLine(args: string[]): void {
const commandLine = parseCommandLine(args);
let configFileName: string; // Configuration file name (if any)
let cachedConfigFileText: string; // Cached configuration file text, used for reparsing (if any)
let configFileWatcher: FileWatcher; // Configuration file watcher
let directoryWatcher: FileWatcher; // Directory watcher to monitor source file addition/removal
let cachedProgram: Program; // Program cached from last compilation
let rootFileNames: string[]; // Root fileNames for compilation
let compilerOptions: CompilerOptions; // Compiler options for compilation
let compilerHost: CompilerHost; // Compiler host
let hostGetSourceFile: typeof compilerHost.getSourceFile; // getSourceFile method from default host
let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation
let timerHandleForDirectoryChanges: any; // Handle for 0.25s wait timer to trigger directory change handler
// This map stores and reuses results of fileExists check that happen inside 'createProgram'
// This allows to save time in module resolution heavy scenarios when existence of the same file might be checked multiple times.
let cachedExistingFiles: Map<boolean>;
let hostFileExists: typeof compilerHost.fileExists;
// Configuration file name (if any)
let configFileName: string;
if (commandLine.options.locale) {
if (!isJSONSupported()) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"));
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors);
@ -126,7 +63,7 @@ namespace ts {
// If there are any errors due to command line parsing and/or
// setting up localization, report them and quit.
if (commandLine.errors.length > 0) {
reportDiagnostics(commandLine.errors, compilerHost);
reportDiagnostics(commandLine.errors, reportDiagnostic);
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
@ -148,11 +85,11 @@ namespace ts {
if (commandLine.options.project) {
if (!isJSONSupported()) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"));
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
if (commandLine.fileNames.length !== 0) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line));
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
@ -160,14 +97,14 @@ namespace ts {
if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) {
configFileName = combinePaths(fileOrDirectory, "tsconfig.json");
if (!sys.fileExists(configFileName)) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project));
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
}
else {
configFileName = fileOrDirectory;
if (!sys.fileExists(configFileName)) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project));
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
}
@ -183,249 +120,94 @@ namespace ts {
return sys.exit(ExitStatus.Success);
}
if (isWatchSet(commandLine.options)) {
if (!sys.watchFile) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined);
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
if (configFileName) {
configFileWatcher = sys.watchFile(configFileName, configFileChanged);
}
if (sys.watchDirectory && configFileName) {
const directory = ts.getDirectoryPath(configFileName);
directoryWatcher = sys.watchDirectory(
// When the configFileName is just "tsconfig.json", the watched directory should be
// the current directory; if there is a given "project" parameter, then the configFileName
// is an absolute file name.
directory === "" ? "." : directory,
watchedDirectoryChanged, /*recursive*/ true);
}
}
performCompilation();
function parseConfigFile(): ParsedCommandLine {
if (!cachedConfigFileText) {
try {
cachedConfigFileText = sys.readFile(configFileName);
}
catch (e) {
const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message);
reportWatchDiagnostic(error);
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
return;
}
}
if (!cachedConfigFileText) {
const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName);
reportDiagnostics([error], /* compilerHost */ undefined);
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
return;
}
const result = parseJsonText(configFileName, cachedConfigFileText);
reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined);
const cwd = sys.getCurrentDirectory();
const configParseResult = parseJsonSourceFileConfigFileContent(result, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd));
reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined);
const commandLineOptions = commandLine.options;
if (configFileName) {
const reportWatchDiagnostic = createWatchDiagnosticReporter();
const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys, reportDiagnostic, reportWatchDiagnostic);
udpateReportDiagnostic(configParseResult.options);
if (isWatchSet(configParseResult.options)) {
if (!sys.watchFile) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined);
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
if (!directoryWatcher && sys.watchDirectory && configFileName) {
const directory = ts.getDirectoryPath(configFileName);
directoryWatcher = sys.watchDirectory(
// When the configFileName is just "tsconfig.json", the watched directory should be
// the current directory; if there is a given "project" parameter, then the configFileName
// is an absolute file name.
directory === "" ? "." : directory,
watchedDirectoryChanged, /*recursive*/ true);
}
reportWatchModeWithoutSysSupport();
createWatchModeWithConfigFile(configParseResult, commandLineOptions, createWatchingSystemHost(reportWatchDiagnostic));
}
return configParseResult;
}
// Invoked to perform initial compilation or re-compilation in watch mode
function performCompilation() {
if (!cachedProgram) {
if (configFileName) {
const configParseResult = parseConfigFile();
rootFileNames = configParseResult.fileNames;
compilerOptions = configParseResult.options;
}
else {
rootFileNames = commandLine.fileNames;
compilerOptions = commandLine.options;
}
compilerHost = createCompilerHost(compilerOptions);
hostGetSourceFile = compilerHost.getSourceFile;
compilerHost.getSourceFile = getSourceFile;
hostFileExists = compilerHost.fileExists;
compilerHost.fileExists = cachedFileExists;
}
if (compilerOptions.pretty) {
reportDiagnosticWorker = reportDiagnosticWithColorAndContext;
}
// reset the cache of existing files
cachedExistingFiles = createMap<boolean>();
const compileResult = compile(rootFileNames, compilerOptions, compilerHost);
if (!isWatchSet(compilerOptions)) {
return sys.exit(compileResult.exitStatus);
}
setCachedProgram(compileResult.program);
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
const missingPaths = compileResult.program.getMissingFilePaths();
missingPaths.forEach(path => {
const fileWatcher = sys.watchFile(path, (_fileName, eventKind) => {
if (eventKind === FileWatcherEventKind.Created) {
fileWatcher.close();
startTimerForRecompilation();
}
});
});
}
function cachedFileExists(fileName: string): boolean {
let fileExists = cachedExistingFiles.get(fileName);
if (fileExists === undefined) {
cachedExistingFiles.set(fileName, fileExists = hostFileExists(fileName));
}
return fileExists;
}
function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void) {
// Return existing SourceFile object if one is available
if (cachedProgram) {
const sourceFile = cachedProgram.getSourceFile(fileName);
// A modified source file has no watcher and should not be reused
if (sourceFile && sourceFile.fileWatcher) {
return sourceFile;
}
}
// Use default host function
const sourceFile = hostGetSourceFile(fileName, languageVersion, onError);
if (sourceFile && isWatchSet(compilerOptions) && sys.watchFile) {
// Attach a file watcher
sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName, eventKind) => sourceFileChanged(sourceFile, eventKind));
}
return sourceFile;
}
// Change cached program to the given program
function setCachedProgram(program: Program) {
if (cachedProgram) {
const newSourceFiles = program ? program.getSourceFiles() : undefined;
forEach(cachedProgram.getSourceFiles(), sourceFile => {
if (!(newSourceFiles && contains(newSourceFiles, sourceFile))) {
if (sourceFile.fileWatcher) {
sourceFile.fileWatcher.close();
sourceFile.fileWatcher = undefined;
}
}
});
}
cachedProgram = program;
}
// If a source file changes, mark it as unwatched and start the recompilation timer
function sourceFileChanged(sourceFile: SourceFile, eventKind: FileWatcherEventKind) {
sourceFile.fileWatcher.close();
sourceFile.fileWatcher = undefined;
if (eventKind === FileWatcherEventKind.Deleted) {
unorderedRemoveItem(rootFileNames, sourceFile.fileName);
}
startTimerForRecompilation();
}
// If the configuration file changes, forget cached program and start the recompilation timer
function configFileChanged() {
setCachedProgram(undefined);
cachedConfigFileText = undefined;
startTimerForRecompilation();
}
function watchedDirectoryChanged(fileName: string) {
if (fileName && !ts.isSupportedSourceFileName(fileName, compilerOptions)) {
return;
}
startTimerForHandlingDirectoryChanges();
}
function startTimerForHandlingDirectoryChanges() {
if (!sys.setTimeout || !sys.clearTimeout) {
return;
}
if (timerHandleForDirectoryChanges) {
sys.clearTimeout(timerHandleForDirectoryChanges);
}
timerHandleForDirectoryChanges = sys.setTimeout(directoryChangeHandler, 250);
}
function directoryChangeHandler() {
const parsedCommandLine = parseConfigFile();
const newFileNames = ts.map(parsedCommandLine.fileNames, compilerHost.getCanonicalFileName);
const canonicalRootFileNames = ts.map(rootFileNames, compilerHost.getCanonicalFileName);
// We check if the project file list has changed. If so, we just throw away the old program and start fresh.
if (!arrayIsEqualTo(newFileNames && newFileNames.sort(), canonicalRootFileNames && canonicalRootFileNames.sort())) {
setCachedProgram(undefined);
startTimerForRecompilation();
else {
performCompilation(configParseResult.fileNames, configParseResult.options);
}
}
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
// operations (such as saving all modified files in an editor) a chance to complete before we kick
// off a new compilation.
function startTimerForRecompilation() {
if (!sys.setTimeout || !sys.clearTimeout) {
return;
else {
udpateReportDiagnostic(commandLineOptions);
if (isWatchSet(commandLineOptions)) {
reportWatchModeWithoutSysSupport();
createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, createWatchingSystemHost());
}
if (timerHandleForRecompilation) {
sys.clearTimeout(timerHandleForRecompilation);
else {
performCompilation(commandLine.fileNames, commandLineOptions);
}
timerHandleForRecompilation = sys.setTimeout(recompile, 250);
}
function recompile() {
timerHandleForRecompilation = undefined;
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation));
performCompilation();
}
}
function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost) {
const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics;
let statistics: Statistic[];
if (hasDiagnostics) {
function reportWatchModeWithoutSysSupport() {
if (!sys.watchFile || !sys.watchDirectory) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"));
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
}
function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) {
const compilerHost = createCompilerHost(compilerOptions);
enableStatistics(compilerOptions);
const program = createProgram(rootFileNames, compilerOptions, compilerHost);
const exitStatus = compileProgram(program);
reportStatistics(program);
return sys.exit(exitStatus);
}
function createWatchingSystemHost(reportWatchDiagnostic?: DiagnosticReporter) {
const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic);
watchingHost.beforeCompile = enableStatistics;
const afterCompile = watchingHost.afterCompile;
watchingHost.afterCompile = (host, program, builder) => {
afterCompile(host, program, builder);
reportStatistics(program);
};
return watchingHost;
}
function compileProgram(program: Program): ExitStatus {
let diagnostics: Diagnostic[];
// First get and report any syntactic errors.
diagnostics = program.getSyntacticDiagnostics();
// If we didn't have any syntactic errors, then also try getting the global and
// semantic errors.
if (diagnostics.length === 0) {
diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
if (diagnostics.length === 0) {
diagnostics = program.getSemanticDiagnostics();
}
}
// Emit and report any errors we ran into.
const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit();
diagnostics = diagnostics.concat(emitDiagnostics);
return handleEmitOutputAndReportErrors(sys, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic);
}
function enableStatistics(compilerOptions: CompilerOptions) {
if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) {
performance.enable();
}
}
function reportStatistics(program: Program) {
let statistics: Statistic[];
const compilerOptions = program.getCompilerOptions();
if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) {
statistics = [];
}
const program = createProgram(fileNames, compilerOptions, compilerHost);
const exitStatus = compileProgram();
if (compilerOptions.listFiles) {
forEach(program.getSourceFiles(), file => {
sys.write(file.fileName + sys.newLine);
});
}
if (hasDiagnostics) {
const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1;
reportCountStatistic("Files", program.getSourceFiles().length);
reportCountStatistic("Lines", countLines(program));
@ -463,44 +245,6 @@ namespace ts {
performance.disable();
}
return { program, exitStatus };
function compileProgram(): ExitStatus {
let diagnostics: Diagnostic[];
// First get and report any syntactic errors.
diagnostics = program.getSyntacticDiagnostics();
// If we didn't have any syntactic errors, then also try getting the global and
// semantic errors.
if (diagnostics.length === 0) {
diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
if (diagnostics.length === 0) {
diagnostics = program.getSemanticDiagnostics();
}
}
// Otherwise, emit and report any errors we ran into.
const emitOutput = program.emit();
diagnostics = diagnostics.concat(emitOutput.diagnostics);
reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost);
reportEmittedFiles(emitOutput.emittedFiles);
if (emitOutput.emitSkipped && diagnostics.length > 0) {
// If the emitter didn't emit anything, then pass that value along.
return ExitStatus.DiagnosticsPresent_OutputsSkipped;
}
else if (diagnostics.length > 0) {
// The emitter emitted something, inform the caller if that happened in the presence
// of diagnostics or not.
return ExitStatus.DiagnosticsPresent_OutputsGenerated;
}
return ExitStatus.Success;
}
function reportStatistics() {
let nameSize = 0;
let valueSize = 0;
@ -654,19 +398,17 @@ namespace ts {
const currentDirectory = sys.getCurrentDirectory();
const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json"));
if (sys.fileExists(file)) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file));
}
else {
sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine));
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file), /* host */ undefined);
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file));
}
return;
}
}
ts.setStackTraceLimit();
if (ts.Debug.isDebugging) {
ts.Debug.enableDebugInfo();
}
@ -674,5 +416,4 @@ if (ts.Debug.isDebugging) {
if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) {
ts.sys.tryEnableSourceMapsForHost();
}
ts.executeCommandLine(ts.sys.args);

View file

@ -36,6 +36,9 @@
"declarationEmitter.ts",
"emitter.ts",
"program.ts",
"builder.ts",
"resolutionCache.ts",
"watchedProgram.ts",
"commandLineParser.ts",
"tsc.ts",
"diagnosticInformationMap.generated.ts"

View file

@ -2347,6 +2347,7 @@ namespace ts {
/* @internal */ patternAmbientModules?: PatternAmbientModule[];
/* @internal */ ambientModuleNames: ReadonlyArray<string>;
/* @internal */ checkJsDirective: CheckJsDirective | undefined;
/* @internal */ version: string;
}
export interface Bundle extends Node {
@ -2410,7 +2411,7 @@ namespace ts {
* program source file but could not be located.
*/
/* @internal */
getMissingFilePaths(): Path[];
getMissingFilePaths(): ReadonlyArray<Path>;
/**
* Emits the JavaScript and declaration files. If targetSourceFile is not specified, then
@ -3570,6 +3571,7 @@ namespace ts {
charset?: string;
checkJs?: boolean;
/* @internal */ configFilePath?: string;
/** configFile is set as non enumerable property so as to avoid checking of json source files */
/* @internal */ readonly configFile?: JsonSourceFile;
declaration?: boolean;
declarationDir?: string;
@ -4014,7 +4016,7 @@ namespace ts {
export interface ResolvedModuleWithFailedLookupLocations {
readonly resolvedModule: ResolvedModuleFull | undefined;
/* @internal */
readonly failedLookupLocations: string[];
readonly failedLookupLocations: ReadonlyArray<string>;
/*@internal*/
isInvalidated?: boolean;
}
@ -4028,14 +4030,18 @@ namespace ts {
export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
readonly resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective;
readonly failedLookupLocations: string[];
readonly failedLookupLocations: ReadonlyArray<string>;
/*@internal*/
isInvalidated?: boolean;
}
export interface HasInvalidatedResolution {
(sourceFile: Path): boolean;
}
export interface CompilerHost extends ModuleResolutionHost {
getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile;
getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile;
getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile;
getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile;
getCancellationToken?(): CancellationToken;
getDefaultLibFileName(options: CompilerOptions): string;
getDefaultLibLocation?(): string;
@ -4059,6 +4065,8 @@ namespace ts {
*/
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
getEnvironmentVariable?(name: string): string;
onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void;
hasInvalidatedResolution?: HasInvalidatedResolution;
}
/* @internal */

View file

@ -366,7 +366,7 @@ namespace ts {
}
export function getTextOfConstantValue(value: string | number) {
return typeof value === "string" ? '"' + escapeNonAsciiString(value) + '"' : "" + value;
return isString(value) ? '"' + escapeNonAsciiString(value) + '"' : "" + value;
}
// Add an extra underscore to identifiers that start with two underscores to avoid issues with magic names like '__proto__'
@ -403,7 +403,10 @@ namespace ts {
((<ModuleDeclaration>node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(<ModuleDeclaration>node));
}
/* @internal */
export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration {
return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral;
}
export function isNonGlobalAmbientModule(node: Node): node is ModuleDeclaration & { name: StringLiteral } {
return isModuleDeclaration(node) && isStringLiteral(node.name);
}
@ -1416,8 +1419,8 @@ namespace ts {
if (node.kind === SyntaxKind.ExportDeclaration) {
return (<ExportDeclaration>node).moduleSpecifier;
}
if (node.kind === SyntaxKind.ModuleDeclaration && (<ModuleDeclaration>node).name.kind === SyntaxKind.StringLiteral) {
return (<ModuleDeclaration>node).name;
if (isModuleWithStringLiteralName(node)) {
return node.name;
}
}
@ -3202,17 +3205,14 @@ namespace ts {
const carriageReturnLineFeed = "\r\n";
const lineFeed = "\n";
export function getNewLineCharacter(options: CompilerOptions | PrinterOptions): string {
export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, system?: System): string {
switch (options.newLine) {
case NewLineKind.CarriageReturnLineFeed:
return carriageReturnLineFeed;
case NewLineKind.LineFeed:
return lineFeed;
}
if (sys) {
return sys.newLine;
}
return carriageReturnLineFeed;
return system ? system.newLine : sys ? sys.newLine : carriageReturnLineFeed;
}
/**
@ -3466,6 +3466,84 @@ namespace ts {
export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags {
return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags;
}
export function compareDataObjects(dst: any, src: any): boolean {
if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) {
return false;
}
for (const e in dst) {
if (typeof dst[e] === "object") {
if (!compareDataObjects(dst[e], src[e])) {
return false;
}
}
else if (typeof dst[e] !== "function") {
if (dst[e] !== src[e]) {
return false;
}
}
}
return true;
}
/**
* clears already present map by calling onDeleteExistingValue callback before deleting that key/value
*/
export function clearMap<T>(map: Map<T>, onDeleteValue: (key: string, existingValue: T) => void) {
// Remove all
map.forEach((existingValue, key) => {
onDeleteValue(key, existingValue);
});
map.clear();
}
export interface MutateMapOptions<T, U> {
createNewValue(key: string, valueInNewMap: U): T;
onDeleteValue(key: string, existingValue: T): void;
/**
* If present this is called with the key when there is value for that key both in new map as well as existing map provided
* Caller can then decide to update or remove this key.
* If the key is removed, caller will get callback of createNewValue for that key.
* If this callback is not provided, the value of such keys is not updated.
*/
onExistingValue?(key: string, existingValue: T, valueInNewMap: U): void;
}
/**
* Mutates the map with newMap such that keys in map will be same as newMap.
*/
export function mutateMap<T, U>(map: Map<T>, newMap: ReadonlyMap<U>, options: MutateMapOptions<T, U>) {
// If there are new values update them
if (newMap) {
const { createNewValue, onDeleteValue, onExistingValue } = options;
// Needs update
map.forEach((existingValue, key) => {
const valueInNewMap = newMap.get(key);
// Not present any more in new map, remove it
if (valueInNewMap === undefined) {
map.delete(key);
onDeleteValue(key, existingValue);
}
// If present notify about existing values
else if (onExistingValue) {
onExistingValue(key, existingValue, valueInNewMap);
}
});
// Add new values that are not already present
newMap.forEach((valueInNewMap, key) => {
if (!map.has(key)) {
// New values
map.set(key, createNewValue(key, valueInNewMap));
}
});
}
else {
clearMap(map, options.onDeleteValue);
}
}
}
namespace ts {

View file

@ -0,0 +1,712 @@
/// <reference path="program.ts" />
/// <reference path="builder.ts" />
/// <reference path="resolutionCache.ts"/>
namespace ts {
export type DiagnosticReporter = (diagnostic: Diagnostic) => void;
export type DiagnosticWorker = (diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System) => void;
export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine;
export interface WatchingSystemHost {
// FS system to use
system: System;
// parse config file
parseConfigFile: ParseConfigFile;
// Reporting errors
reportDiagnostic: DiagnosticReporter;
reportWatchDiagnostic: DiagnosticReporter;
// Callbacks to do custom action before creating program and after creating program
beforeCompile(compilerOptions: CompilerOptions): void;
afterCompile(host: System, program: Program, builder: Builder): void;
}
const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? {
getCurrentDirectory: () => sys.getCurrentDirectory(),
getNewLine: () => sys.newLine,
getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames)
} : undefined;
export function createDiagnosticReporter(system = sys, worker = reportDiagnosticSimply, formatDiagnosticsHost?: FormatDiagnosticsHost): DiagnosticReporter {
return diagnostic => worker(diagnostic, getFormatDiagnosticsHost(), system);
function getFormatDiagnosticsHost() {
return formatDiagnosticsHost || (formatDiagnosticsHost = system === sys ? defaultFormatDiagnosticsHost : {
getCurrentDirectory: () => system.getCurrentDirectory(),
getNewLine: () => system.newLine,
getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames),
});
}
}
export function createWatchDiagnosticReporter(system = sys): DiagnosticReporter {
return diagnostic => {
let output = new Date().toLocaleTimeString() + " - ";
output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine + system.newLine}`;
system.write(output);
};
}
export function reportDiagnostics(diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter): void {
for (const diagnostic of diagnostics) {
reportDiagnostic(diagnostic);
}
}
export function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void {
system.write(ts.formatDiagnostic(diagnostic, host));
}
export function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void {
system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine());
}
export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine {
let configFileText: string;
try {
configFileText = system.readFile(configFileName);
}
catch (e) {
const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message);
reportWatchDiagnostic(error);
system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
return;
}
if (!configFileText) {
const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName);
reportDiagnostics([error], reportDiagnostic);
system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
return;
}
const result = parseJsonText(configFileName, configFileText);
reportDiagnostics(result.parseDiagnostics, reportDiagnostic);
const cwd = system.getCurrentDirectory();
const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd));
reportDiagnostics(configParseResult.errors, reportDiagnostic);
return configParseResult;
}
function reportEmittedFiles(files: string[], system: System): void {
if (!files || files.length === 0) {
return;
}
const currentDir = system.getCurrentDirectory();
for (const file of files) {
const filepath = getNormalizedAbsolutePath(file, currentDir);
system.write(`TSFILE: ${filepath}${system.newLine}`);
}
}
export function handleEmitOutputAndReportErrors(system: System, program: Program,
emittedFiles: string[], emitSkipped: boolean,
diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter
): ExitStatus {
reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), reportDiagnostic);
reportEmittedFiles(emittedFiles, system);
if (program.getCompilerOptions().listFiles) {
forEach(program.getSourceFiles(), file => {
system.write(file.fileName + system.newLine);
});
}
if (emitSkipped && diagnostics.length > 0) {
// If the emitter didn't emit anything, then pass that value along.
return ExitStatus.DiagnosticsPresent_OutputsSkipped;
}
else if (diagnostics.length > 0) {
// The emitter emitted something, inform the caller if that happened in the presence
// of diagnostics or not.
return ExitStatus.DiagnosticsPresent_OutputsGenerated;
}
return ExitStatus.Success;
}
export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys,
parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter,
reportWatchDiagnostic?: DiagnosticReporter
): WatchingSystemHost {
reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply);
reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system);
parseConfigFile = parseConfigFile || ts.parseConfigFile;
return {
system,
parseConfigFile,
reportDiagnostic,
reportWatchDiagnostic,
beforeCompile: noop,
afterCompile: compileWatchedProgram,
};
function compileWatchedProgram(host: System, program: Program, builder: Builder) {
// First get and report any syntactic errors.
let diagnostics = program.getSyntacticDiagnostics();
let reportSemanticDiagnostics = false;
// If we didn't have any syntactic errors, then also try getting the global and
// semantic errors.
if (diagnostics.length === 0) {
diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
if (diagnostics.length === 0) {
reportSemanticDiagnostics = true;
}
}
// Emit and report any errors we ran into.
const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined;
let sourceMaps: SourceMapData[];
let emitSkipped: boolean;
const result = builder.emitChangedFiles(program);
if (result.length === 0) {
emitSkipped = true;
}
else {
for (const emitOutput of result) {
if (emitOutput.emitSkipped) {
emitSkipped = true;
}
diagnostics = concatenate(diagnostics, emitOutput.diagnostics);
sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps);
writeOutputFiles(emitOutput.outputFiles);
}
}
if (reportSemanticDiagnostics) {
diagnostics = diagnostics.concat(builder.getSemanticDiagnostics(program));
}
return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped,
diagnostics, reportDiagnostic);
function ensureDirectoriesExist(directoryPath: string) {
if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) {
const parentDirectory = getDirectoryPath(directoryPath);
ensureDirectoriesExist(parentDirectory);
host.createDirectory(directoryPath);
}
}
function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) {
try {
performance.mark("beforeIOWrite");
ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName)));
host.writeFile(fileName, data, writeByteOrderMark);
performance.mark("afterIOWrite");
performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite");
}
catch (e) {
return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e);
}
}
function writeOutputFiles(outputFiles: OutputFile[]) {
if (outputFiles) {
for (const outputFile of outputFiles) {
const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark);
if (error) {
diagnostics.push(error);
}
if (emittedFiles) {
emittedFiles.push(outputFile.name);
}
}
}
}
}
}
export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions = {}, watchingHost?: WatchingSystemHost) {
return createWatchMode(configParseResult.fileNames, configParseResult.options, watchingHost, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend);
}
export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost) {
return createWatchMode(rootFileNames, compilerOptions, watchingHost);
}
interface HostFileInfo {
version: number;
sourceFile: SourceFile;
fileWatcher: FileWatcher;
}
function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike<WatchDirectoryFlags>, optionsToExtendForConfigFile?: CompilerOptions) {
let program: Program;
let needsReload: boolean; // true if the config file changed and needs to reload it from the disk
let missingFilesMap: Map<FileWatcher>; // Map of file watchers for the missing files
let configFileWatcher: FileWatcher; // watcher for the config file
let watchedWildcardDirectories: Map<WildcardDirectoryWatcher>; // map of watchers for the wild card directories in the config file
let timerToUpdateProgram: any; // timer callback to recompile the program
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files
let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions
let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations
watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty);
const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost;
let host: System;
if (configFileName) {
host = createCachedSystem(system);
configFileWatcher = system.watchFile(configFileName, onConfigFileChanged);
}
else {
host = system;
}
const currentDirectory = host.getCurrentDirectory();
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
// Cache for the module resolution
const resolutionCache = createResolutionCache(
fileName => toPath(fileName),
() => compilerOptions,
watchFailedLookupLocation,
s => writeLog(s)
);
// There is no extra check needed since we can just rely on the program to decide emit
const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true);
synchronizeProgram();
// Update the wild card directory watch
watchConfigFileWildCardDirectories();
return () => program;
function synchronizeProgram() {
writeLog(`Synchronizing program`);
hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution();
if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) {
return;
}
// Create the compiler host
const compilerHost = createWatchedCompilerHost(compilerOptions);
resolutionCache.setModuleResolutionHost(compilerHost);
if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) {
resolutionCache.clear();
}
hasChangedCompilerOptions = false;
beforeCompile(compilerOptions);
// Compile the program
program = createProgram(rootFileNames, compilerOptions, compilerHost, program);
builder.onProgramUpdateGraph(program, hasInvalidatedResolution);
// Update watches
updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath, closeMissingFilePathWatcher);
if (missingFilePathsRequestedForRelease) {
// These are the paths that program creater told us as not in use any more but were missing on the disk.
// We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO,
// if there is already watcher for it (for missing files)
// At that point our watches were updated, hence now we know that these paths are not tracked and need to be removed
// so that at later time we have correct result of their presence
for (const missingFilePath of missingFilePathsRequestedForRelease) {
if (!missingFilesMap.has(missingFilePath)) {
sourceFilesCache.delete(missingFilePath);
}
}
missingFilePathsRequestedForRelease = undefined;
}
afterCompile(host, program, builder);
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
}
function createWatchedCompilerHost(options: CompilerOptions): CompilerHost {
const newLine = getNewLineCharacter(options, system);
const realpath = host.realpath && ((path: string) => host.realpath(path));
return {
getSourceFile: getVersionedSourceFile,
getSourceFileByPath: getVersionedSourceFileByPath,
getDefaultLibLocation,
getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)),
writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { },
getCurrentDirectory: memoize(() => host.getCurrentDirectory()),
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames,
getCanonicalFileName,
getNewLine: () => newLine,
fileExists,
readFile: fileName => host.readFile(fileName),
trace: (s: string) => host.write(s + newLine),
directoryExists: directoryName => host.directoryExists(directoryName),
getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "",
getDirectories: (path: string) => host.getDirectories(path),
realpath,
resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile),
resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false),
onReleaseOldSourceFile,
hasInvalidatedResolution
};
}
function toPath(fileName: string) {
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
}
function fileExists(fileName: string) {
const path = toPath(fileName);
const hostSourceFileInfo = sourceFilesCache.get(path);
if (hostSourceFileInfo !== undefined) {
return !isString(hostSourceFileInfo);
}
return host.fileExists(fileName);
}
function getDefaultLibLocation(): string {
return getDirectoryPath(normalizePath(host.getExecutingFilePath()));
}
function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile);
}
function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
const hostSourceFile = sourceFilesCache.get(path);
// No source file on the host
if (isString(hostSourceFile)) {
return undefined;
}
// Create new source file if requested or the versions dont match
if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) {
const sourceFile = getNewSourceFile();
if (hostSourceFile) {
if (shouldCreateNewSourceFile) {
hostSourceFile.version++;
}
if (sourceFile) {
hostSourceFile.sourceFile = sourceFile;
sourceFile.version = hostSourceFile.version.toString();
if (!hostSourceFile.fileWatcher) {
hostSourceFile.fileWatcher = watchSourceFileForChanges(path);
}
}
else {
// There is no source file on host any more, close the watch, missing file paths will track it
hostSourceFile.fileWatcher.close();
sourceFilesCache.set(path, hostSourceFile.version.toString());
}
}
else {
let fileWatcher: FileWatcher;
if (sourceFile) {
sourceFile.version = "0";
fileWatcher = watchSourceFileForChanges(path);
sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher });
}
else {
sourceFilesCache.set(path, "0");
}
}
return sourceFile;
}
return hostSourceFile.sourceFile;
function getNewSourceFile() {
let text: string;
try {
performance.mark("beforeIORead");
text = host.readFile(fileName, compilerOptions.charset);
performance.mark("afterIORead");
performance.measure("I/O Read", "beforeIORead", "afterIORead");
}
catch (e) {
if (onError) {
onError(e.message);
}
}
return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined;
}
}
function removeSourceFile(path: Path) {
const hostSourceFile = sourceFilesCache.get(path);
if (hostSourceFile !== undefined) {
if (!isString(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
resolutionCache.invalidateResolutionOfFile(path);
}
sourceFilesCache.delete(path);
}
}
function getSourceVersion(path: Path): string {
const hostSourceFile = sourceFilesCache.get(path);
return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString();
}
function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) {
const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path);
// If this is the source file thats in the cache and new program doesnt need it,
// remove the cached entry.
// Note we arent deleting entry if file became missing in new program or
// there was version update and new source file was created.
if (hostSourceFileInfo) {
// record the missing file paths so they can be removed later if watchers arent tracking them
if (isString(hostSourceFileInfo)) {
(missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path);
}
else if (hostSourceFileInfo.sourceFile === oldSourceFile) {
sourceFilesCache.delete(oldSourceFile.path);
resolutionCache.invalidateResolutionOfFile(oldSourceFile.path);
}
}
}
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
// operations (such as saving all modified files in an editor) a chance to complete before we kick
// off a new compilation.
function scheduleProgramUpdate() {
if (!system.setTimeout || !system.clearTimeout) {
return;
}
if (timerToUpdateProgram) {
system.clearTimeout(timerToUpdateProgram);
}
timerToUpdateProgram = system.setTimeout(updateProgram, 250);
}
function scheduleProgramReload() {
Debug.assert(!!configFileName);
needsReload = true;
scheduleProgramUpdate();
}
function updateProgram() {
timerToUpdateProgram = undefined;
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation));
if (needsReload) {
reloadConfigFile();
}
else {
synchronizeProgram();
}
}
function reloadConfigFile() {
writeLog(`Reloading config file: ${configFileName}`);
needsReload = false;
const cachedHost = host as CachedSystem;
cachedHost.clearCache();
const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic);
rootFileNames = configParseResult.fileNames;
compilerOptions = configParseResult.options;
hasChangedCompilerOptions = true;
configFileSpecs = configParseResult.configFileSpecs;
configFileWildCardDirectories = configParseResult.wildcardDirectories;
synchronizeProgram();
// Update the wild card directory watch
watchConfigFileWildCardDirectories();
}
function watchSourceFileForChanges(path: Path) {
return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind));
}
function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) {
writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`);
updateCachedSystemWithFile(fileName, path, eventKind);
const hostSourceFile = sourceFilesCache.get(path);
if (hostSourceFile) {
// Update the cache
if (eventKind === FileWatcherEventKind.Deleted) {
resolutionCache.invalidateResolutionOfFile(path);
if (!isString(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
sourceFilesCache.set(path, (hostSourceFile.version++).toString());
}
}
else {
// Deleted file created
if (isString(hostSourceFile)) {
sourceFilesCache.delete(path);
}
else {
// file changed - just update the version
hostSourceFile.version++;
}
}
}
// Update the program
scheduleProgramUpdate();
}
function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) {
if (configFileName) {
(host as CachedSystem).addOrDeleteFile(fileName, path, eventKind);
}
}
function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, failedLookupLocationPath, containingFile, name));
}
function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`);
updateCachedSystemWithFile(fileName, failedLookupLocationPath, eventKind);
resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath);
scheduleProgramUpdate();
}
function watchMissingFilePath(missingFilePath: Path) {
return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind));
}
function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) {
fileWatcher.close();
}
function onMissingFileChange(fileName: string, missingFilePath: Path, eventKind: FileWatcherEventKind) {
writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`);
updateCachedSystemWithFile(fileName, missingFilePath, eventKind);
if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) {
closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath));
missingFilesMap.delete(missingFilePath);
// Delete the entry in the source files cache so that new source file is created
removeSourceFile(missingFilePath);
// When a missing file is created, we should update the graph.
scheduleProgramUpdate();
}
}
function watchConfigFileWildCardDirectories() {
updateWatchingWildcardDirectories(
watchedWildcardDirectories || (watchedWildcardDirectories = createMap()),
createMapFromTemplate(configFileWildCardDirectories),
watchWildCardDirectory,
stopWatchingWildCardDirectory
);
}
function watchWildCardDirectory(directory: string, flags: WatchDirectoryFlags) {
return host.watchDirectory(directory, fileOrFolder =>
onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileOrFolder, directory)),
(flags & WatchDirectoryFlags.Recursive) !== 0);
}
function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatcher, _recursiveChanged: boolean) {
watcher.close();
}
function onFileAddOrRemoveInWatchedDirectory(fileOrFolder: string) {
Debug.assert(!!configFileName);
const fileOrFolderPath = toPath(fileOrFolder);
// Since the file existance changed, update the sourceFiles cache
(host as CachedSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath);
removeSourceFile(fileOrFolderPath);
// If a change was made inside "folder/file", node will trigger the callback twice:
// one with the fileName being "folder/file", and the other one with "folder".
// We don't respond to the second one.
if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) {
writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`);
return;
}
writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileOrFolder}`);
// Reload is pending, do the reload
if (!needsReload) {
const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host);
if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) {
reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName));
}
rootFileNames = result.fileNames;
// Schedule Update the program
scheduleProgramUpdate();
}
}
function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) {
writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`);
scheduleProgramReload();
}
function writeLog(s: string) {
const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics;
if (hasDiagnostics) {
host.write(s);
}
}
function computeHash(data: string) {
return system.createHash ? system.createHash(data) : data;
}
}
interface CachedSystem extends System, CachedHost {
}
function createCachedSystem(host: System): CachedSystem {
const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined;
const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined;
const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined;
const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined;
const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined;
const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined;
const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined;
const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined;
const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined;
const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined;
const cachedPartialSystem = createCachedPartialSystem(host);
return {
args: host.args,
newLine: host.newLine,
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames,
write: s => host.write(s),
readFile: (path, encoding?) => host.readFile(path, encoding),
getFileSize,
writeFile: (fileName, data, writeByteOrderMark?) => cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark),
watchFile,
watchDirectory,
resolvePath: path => host.resolvePath(path),
fileExists: fileName => cachedPartialSystem.fileExists(fileName),
directoryExists: dir => cachedPartialSystem.directoryExists(dir),
createDirectory: dir => cachedPartialSystem.createDirectory(dir),
getExecutingFilePath: () => host.getExecutingFilePath(),
getCurrentDirectory: () => cachedPartialSystem.getCurrentDirectory(),
getDirectories: dir => cachedPartialSystem.getDirectories(dir),
readDirectory: (path, extensions, excludes, includes, depth) => cachedPartialSystem.readDirectory(path, extensions, excludes, includes, depth),
getModifiedTime,
createHash,
getMemoryUsage,
exit: exitCode => host.exit(exitCode),
realpath,
getEnvironmentVariable: name => host.getEnvironmentVariable(name),
tryEnableSourceMapsForHost,
debugMode: host.debugMode,
setTimeout,
clearTimeout,
addOrDeleteFileOrFolder: (fileOrFolder, fileOrFolderPath) => cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath),
addOrDeleteFile: (file, filePath, eventKind) => cachedPartialSystem.addOrDeleteFile(file, filePath, eventKind),
clearCache: () => cachedPartialSystem.clearCache()
};
}
}

View file

@ -394,7 +394,7 @@ namespace FourSlash {
// Entry points from fourslash.ts
public goToMarker(name: string | Marker = "") {
const marker = typeof name === "string" ? this.getMarkerByName(name) : name;
const marker = ts.isString(name) ? this.getMarkerByName(name) : name;
if (this.activeFile.fileName !== marker.fileName) {
this.openFile(marker.fileName);
}
@ -403,7 +403,7 @@ namespace FourSlash {
if (marker.position === -1 || marker.position > content.length) {
throw new Error(`Marker "${name}" has been invalidated by unrecoverable edits to the file.`);
}
const mName = typeof name === "string" ? name : this.markerName(marker);
const mName = ts.isString(name) ? name : this.markerName(marker);
this.lastKnownMarker = mName;
this.goToPosition(marker.position);
}
@ -1026,7 +1026,7 @@ namespace FourSlash {
public verifyNoReferences(markerNameOrRange?: string | Range) {
if (markerNameOrRange) {
if (typeof markerNameOrRange === "string") {
if (ts.isString(markerNameOrRange)) {
this.goToMarker(markerNameOrRange);
}
else {
@ -1522,7 +1522,7 @@ namespace FourSlash {
resultString += "Diagnostics:" + Harness.IO.newLine();
const diagnostics = ts.getPreEmitDiagnostics(this.languageService.getProgram());
for (const diagnostic of diagnostics) {
if (typeof diagnostic.messageText !== "string") {
if (!ts.isString(diagnostic.messageText)) {
let chainedMessage = <ts.DiagnosticMessageChain>diagnostic.messageText;
let indentation = " ";
while (chainedMessage) {
@ -2913,7 +2913,7 @@ namespace FourSlash {
result = this.testData.files[index];
}
}
else if (typeof indexOrName === "string") {
else if (ts.isString(indexOrName)) {
let name = <string>indexOrName;
// names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName

View file

@ -225,7 +225,7 @@ namespace Utils {
return JSON.stringify(file, (_, v) => isNodeOrArray(v) ? serializeNode(v) : v, " ");
function getKindName(k: number | string): string {
if (typeof k === "string") {
if (ts.isString(k)) {
return k;
}
@ -754,6 +754,10 @@ namespace Harness {
}
}
export function mockHash(s: string): string {
return `hash-${s}`;
}
const environment = Utils.getExecutionEnvironment();
switch (environment) {
case Utils.ExecutionEnvironment.Node:

View file

@ -855,8 +855,4 @@ namespace Harness.LanguageService {
getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); }
getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); }
}
export function mockHash(s: string): string {
return `hash-${s}`;
}
}

View file

@ -254,7 +254,7 @@ class ProjectRunner extends RunnerBase {
if (option) {
const optType = option.type;
let value = <any>testCase[name];
if (typeof optType !== "string") {
if (!ts.isString(optType)) {
const key = value.toLowerCase();
const optTypeValue = optType.get(key);
if (optTypeValue) {

View file

@ -1,4 +1,4 @@
{
{
"extends": "../tsconfig-base",
"compilerOptions": {
"removeComments": false,
@ -44,6 +44,7 @@
"../compiler/declarationEmitter.ts",
"../compiler/emitter.ts",
"../compiler/program.ts",
"../compiler/builder.ts",
"../compiler/commandLineParser.ts",
"../compiler/diagnosticInformationMap.generated.ts",
"../services/breakpoints.ts",
@ -92,6 +93,7 @@
"rwcRunner.ts",
"test262Runner.ts",
"runner.ts",
"virtualFileSystemWithWatch.ts",
"../server/protocol.ts",
"../server/session.ts",
"../server/client.ts",
@ -115,6 +117,7 @@
"./unittests/convertCompilerOptionsFromJson.ts",
"./unittests/convertTypeAcquisitionFromJson.ts",
"./unittests/tsserverProjectSystem.ts",
"./unittests/tscWatchMode.ts",
"./unittests/matchFiles.ts",
"./unittests/initializeTSConfig.ts",
"./unittests/compileOnSave.ts",

View file

@ -47,7 +47,7 @@ namespace ts {
clearTimeout,
setImmediate: typeof setImmediate !== "undefined" ? setImmediate : action => setTimeout(action, 0),
clearImmediate: typeof clearImmediate !== "undefined" ? clearImmediate : clearTimeout,
createHash: Harness.LanguageService.mockHash,
createHash: Harness.mockHash,
};
}
@ -61,7 +61,7 @@ namespace ts {
typingsInstaller: undefined
};
const projectService = new server.ProjectService(svcOpts);
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined);
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, serverHost);
const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo);
project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } );
@ -188,7 +188,8 @@ namespace ts {
let diags = project.getLanguageService().getSemanticDiagnostics(root.name);
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");
assert.isTrue(diags.length === 1, "one diagnostic expected");
assert.isTrue(typeof diags[0].messageText === "string" && ((<string>diags[0].messageText).indexOf("Cannot find module") === 0), "should be 'cannot find module' message");
const messageText = diags[0].messageText;
assert.isTrue(isString(messageText) && messageText.indexOf("Cannot find module") === 0, "should be 'cannot find module' message");
fileMap.set(imported.name, imported);
fileExistsCalledForBar = false;

View file

@ -87,7 +87,7 @@ namespace ts {
"/dev/tests/scenarios/first.json": "",
"/dev/tests/baselines/first/output.ts": ""
});
const testContents = mapEntries(testContentsJson, (k, v) => [k, typeof v === "string" ? v : JSON.stringify(v)]);
const testContents = mapEntries(testContentsJson, (k, v) => [k, isString(v) ? v : JSON.stringify(v)]);
const caseInsensitiveBasePath = "c:/dev/";
const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, mapEntries(testContents, (key, content) => [`c:${key}`, content]));

View file

@ -1,6 +1,18 @@
/// <reference path="..\harness.ts" />
namespace ts {
function verifyMissingFilePaths(missingPaths: ReadonlyArray<Path>, expected: ReadonlyArray<string>) {
assert.isDefined(missingPaths);
const map = arrayToSet(expected) as Map<boolean>;
for (const missing of missingPaths) {
const value = map.get(missing);
assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`);
map.set(missing, false);
}
const notFound = mapDefinedIter(map.keys(), k => map.get(k) === true ? k : undefined);
assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`);
}
describe("Program.getMissingFilePaths", () => {
const options: CompilerOptions = {
@ -40,34 +52,31 @@ namespace ts {
it("handles no missing root files", () => {
const program = createProgram([emptyFileRelativePath], options, testCompilerHost);
const missing = program.getMissingFilePaths();
assert.isDefined(missing);
assert.deepEqual(missing, []);
verifyMissingFilePaths(missing, []);
});
it("handles missing root file", () => {
const program = createProgram(["./nonexistent.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
assert.isDefined(missing);
assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path
});
it("handles multiple missing root files", () => {
const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths().sort();
assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
});
it("handles a mix of present and missing root files", () => {
const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths().sort();
assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
});
it("handles repeatedly specified root files", () => {
const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
assert.isDefined(missing);
assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]);
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]);
});
it("normalizes file paths", () => {
@ -81,9 +90,8 @@ namespace ts {
it("handles missing triple slash references", () => {
const program = createProgram([referenceFileRelativePath], options, testCompilerHost);
const missing = program.getMissingFilePaths().sort();
assert.isDefined(missing);
assert.deepEqual(missing, [
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, [
// From absolute reference
"d:/imaginary/nonexistent1.ts",
@ -100,4 +108,4 @@ namespace ts {
]);
});
});
}
}

View file

@ -346,7 +346,7 @@ namespace ts {
const program_2 = updateProgram(program_1, ["a.ts"], options, noop, newTexts);
assert.deepEqual(emptyArray, program_2.getMissingFilePaths());
assert.equal(StructureIsReused.SafeModules, program_1.structureIsReused);
assert.equal(StructureIsReused.Not, program_1.structureIsReused);
});
it("resolution cache follows imports", () => {

View file

@ -25,7 +25,7 @@ namespace ts.server {
clearTimeout: noop,
setImmediate: () => 0,
clearImmediate: noop,
createHash: Harness.LanguageService.mockHash,
createHash: Harness.mockHash,
};
class TestSession extends Session {

View file

@ -53,7 +53,7 @@ namespace ts.projectSystem {
// TODO: Apparently compilerOptions is mutated, so have to repeat it here!
et.assertProjectInfoTelemetryEvent({
projectId: Harness.LanguageService.mockHash("/hunter2/foo.csproj"),
projectId: Harness.mockHash("/hunter2/foo.csproj"),
compilerOptions: { strict: true },
compileOnSave: true,
// These properties can't be present for an external project, so they are undefined instead of false.
@ -195,7 +195,7 @@ namespace ts.projectSystem {
const et = new EventTracker([jsconfig, file]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
projectId: Harness.LanguageService.mockHash("/jsconfig.json"),
projectId: Harness.mockHash("/jsconfig.json"),
fileStats: fileStats({ js: 1 }),
compilerOptions: autoJsCompilerOptions,
typeAcquisition: {
@ -215,7 +215,7 @@ namespace ts.projectSystem {
et.service.openClientFile(file.path);
et.getEvent<server.ProjectLanguageServiceStateEvent>(server.ProjectLanguageServiceStateEvent, /*mayBeMore*/ true);
et.assertProjectInfoTelemetryEvent({
projectId: Harness.LanguageService.mockHash("/jsconfig.json"),
projectId: Harness.mockHash("/jsconfig.json"),
fileStats: fileStats({ js: 1 }),
compilerOptions: autoJsCompilerOptions,
configFileName: "jsconfig.json",
@ -251,7 +251,7 @@ namespace ts.projectSystem {
assertProjectInfoTelemetryEvent(partial: Partial<server.ProjectInfoTelemetryEventData>): void {
assert.deepEqual(this.getEvent<server.ProjectInfoTelemetryEvent>(ts.server.ProjectInfoTelemetryEvent), {
projectId: Harness.LanguageService.mockHash("/tsconfig.json"),
projectId: Harness.mockHash("/tsconfig.json"),
fileStats: fileStats({ ts: 1 }),
compilerOptions: {},
extends: false,
@ -282,7 +282,7 @@ namespace ts.projectSystem {
}
function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder {
return { path, content: typeof content === "string" ? "" : JSON.stringify(content) };
return { path, content: isString(content) ? "" : JSON.stringify(content) };
}
function fileStats(nonZeroStats: Partial<server.FileStats>): server.FileStats {

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,550 @@
/// <reference path="harness.ts" />
namespace ts.TestFSWithWatch {
const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
export const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
};
export const safeList = {
path: <Path>"/safeList.json",
content: JSON.stringify({
commander: "commander",
express: "express",
jquery: "jquery",
lodash: "lodash",
moment: "moment",
chroma: "chroma-js"
})
};
function getExecutingFilePathFromLibFile(): string {
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
}
interface TestServerHostCreationParameters {
useCaseSensitiveFileNames?: boolean;
executingFilePath?: string;
currentDirectory?: string;
newLine?: string;
}
export function createWatchedSystem(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost {
if (!params) {
params = {};
}
const host = new TestServerHost(/*withSafelist*/ false,
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
params.executingFilePath || getExecutingFilePathFromLibFile(),
params.currentDirectory || "/",
fileOrFolderList,
params.newLine);
return host;
}
export function createServerHost(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost {
if (!params) {
params = {};
}
const host = new TestServerHost(/*withSafelist*/ true,
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
params.executingFilePath || getExecutingFilePathFromLibFile(),
params.currentDirectory || "/",
fileOrFolderList,
params.newLine);
return host;
}
export interface FileOrFolder {
path: string;
content?: string;
fileSize?: number;
}
interface FSEntry {
path: Path;
fullPath: string;
}
interface File extends FSEntry {
content: string;
fileSize?: number;
}
interface Folder extends FSEntry {
entries: FSEntry[];
}
function isFolder(s: FSEntry): s is Folder {
return s && isArray((<Folder>s).entries);
}
function isFile(s: FSEntry): s is File {
return s && isString((<File>s).content);
}
function invokeWatcherCallbacks<T>(callbacks: T[], invokeCallback: (cb: T) => void): void {
if (callbacks) {
// The array copy is made to ensure that even if one of the callback removes the callbacks,
// we dont miss any callbacks following it
const cbs = callbacks.slice();
for (const cb of cbs) {
invokeCallback(cb);
}
}
}
function checkMapKeys(caption: string, map: Map<any>, expectedKeys: string[]) {
assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`);
for (const name of expectedKeys) {
assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`);
}
}
export function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) {
assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`);
for (const f of expectedFileNames) {
assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`);
}
}
export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
}
export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) {
checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
}
export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray<string>) {
const mapExpected = arrayToSet(expected);
const mapSeen = createMap<true>();
for (const f of host.getOutput()) {
assert.isUndefined(mapSeen.get(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`);
if (mapExpected.has(f)) {
mapExpected.delete(f);
mapSeen.set(f, true);
}
}
assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`);
}
export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray<string>) {
const mapExpectedToBeAbsent = arrayToSet(expectedToBeAbsent);
for (const f of host.getOutput()) {
assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`);
}
}
class Callbacks {
private map: TimeOutCallback[] = [];
private nextId = 1;
register(cb: (...args: any[]) => void, args: any[]) {
const timeoutId = this.nextId;
this.nextId++;
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
return timeoutId;
}
unregister(id: any) {
if (typeof id === "number") {
delete this.map[id];
}
}
count() {
let n = 0;
for (const _ in this.map) {
n++;
}
return n;
}
invoke() {
// Note: invoking a callback may result in new callbacks been queued,
// so do not clear the entire callback list regardless. Only remove the
// ones we have invoked.
for (const key in this.map) {
this.map[key]();
delete this.map[key];
}
}
}
type TimeOutCallback = () => any;
export class TestServerHost implements server.ServerHost {
args: string[] = [];
private readonly output: string[] = [];
private fs: Map<FSEntry> = createMap<FSEntry>();
private getCanonicalFileName: (s: string) => string;
private toPath: (f: string) => Path;
private timeoutCallbacks = new Callbacks();
private immediateCallbacks = new Callbacks();
readonly watchedDirectories = createMultiMap<DirectoryWatcherCallback>();
readonly watchedDirectoriesRecursive = createMultiMap<DirectoryWatcherCallback>();
readonly watchedFiles = createMultiMap<FileWatcherCallback>();
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[], public readonly newLine = "\n") {
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
this.reloadFS(fileOrFolderList);
}
toNormalizedAbsolutePath(s: string) {
return getNormalizedAbsolutePath(s, this.currentDirectory);
}
toFullPath(s: string) {
return this.toPath(this.toNormalizedAbsolutePath(s));
}
reloadFS(fileOrFolderList: FileOrFolder[]) {
const mapNewLeaves = createMap<true>();
const isNewFs = this.fs.size === 0;
// always inject safelist file in the list of files
for (const fileOrFolder of fileOrFolderList.concat(this.withSafeList ? safeList : [])) {
const path = this.toFullPath(fileOrFolder.path);
mapNewLeaves.set(path, true);
// If its a change
const currentEntry = this.fs.get(path);
if (currentEntry) {
if (isFile(currentEntry)) {
if (isString(fileOrFolder.content)) {
// Update file
if (currentEntry.content !== fileOrFolder.content) {
currentEntry.content = fileOrFolder.content;
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
}
}
else {
// TODO: Changing from file => folder
}
}
else {
// Folder
if (isString(fileOrFolder.content)) {
// TODO: Changing from folder => file
}
else {
// Folder update: Nothing to do.
}
}
}
else {
this.ensureFileOrFolder(fileOrFolder);
}
}
if (!isNewFs) {
this.fs.forEach((fileOrFolder, path) => {
// If this entry is not from the new file or folder
if (!mapNewLeaves.get(path)) {
// Leaf entries that arent in new list => remove these
if (isFile(fileOrFolder) || isFolder(fileOrFolder) && fileOrFolder.entries.length === 0) {
this.removeFileOrFolder(fileOrFolder, folder => !mapNewLeaves.get(folder.path));
}
}
});
}
}
ensureFileOrFolder(fileOrFolder: FileOrFolder) {
if (isString(fileOrFolder.content)) {
const file = this.toFile(fileOrFolder);
Debug.assert(!this.fs.get(file.path));
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath));
this.addFileOrFolderInFolder(baseFolder, file);
}
else {
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
this.ensureFolder(fullPath);
}
}
private ensureFolder(fullPath: string): Folder {
const path = this.toPath(fullPath);
let folder = this.fs.get(path) as Folder;
if (!folder) {
folder = this.toFolder(fullPath);
const baseFullPath = getDirectoryPath(fullPath);
if (fullPath !== baseFullPath) {
// Add folder in the base folder
const baseFolder = this.ensureFolder(baseFullPath);
this.addFileOrFolderInFolder(baseFolder, folder);
}
else {
// root folder
Debug.assert(this.fs.size === 0);
this.fs.set(path, folder);
}
}
Debug.assert(isFolder(folder));
return folder;
}
private addFileOrFolderInFolder(folder: Folder, fileOrFolder: File | Folder) {
folder.entries.push(fileOrFolder);
this.fs.set(fileOrFolder.path, fileOrFolder);
if (isFile(fileOrFolder)) {
this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Created);
}
this.invokeDirectoryWatcher(folder.fullPath, fileOrFolder.fullPath);
}
private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) {
const basePath = getDirectoryPath(fileOrFolder.path);
const baseFolder = this.fs.get(basePath) as Folder;
if (basePath !== fileOrFolder.path) {
Debug.assert(!!baseFolder);
filterMutate(baseFolder.entries, entry => entry !== fileOrFolder);
}
this.fs.delete(fileOrFolder.path);
if (isFile(fileOrFolder)) {
this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Deleted);
}
else {
Debug.assert(fileOrFolder.entries.length === 0);
const relativePath = this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath);
// Invoke directory and recursive directory watcher for the folder
// Here we arent invoking recursive directory watchers for the base folders
// since that is something we would want to do for both file as well as folder we are deleting
invokeWatcherCallbacks(this.watchedDirectories.get(fileOrFolder.path), cb => cb(relativePath));
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrFolder.path), cb => cb(relativePath));
}
if (basePath !== fileOrFolder.path) {
if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) {
this.removeFileOrFolder(baseFolder, isRemovableLeafFolder);
}
else {
this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrFolder.fullPath);
}
}
}
private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) {
const callbacks = this.watchedFiles.get(this.toPath(fileFullPath));
const fileName = getBaseFileName(fileFullPath);
invokeWatcherCallbacks(callbacks, cb => cb(fileName, eventKind));
}
private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) {
return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
}
/**
* This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders
*/
private invokeDirectoryWatcher(folderFullPath: string, fileName: string) {
const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName);
invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => cb(relativePath));
this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName);
}
/**
* This will call the recursive directory watcher for this directory as well as all the base directories
*/
private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) {
const relativePath = this.getRelativePathToDirectory(fullPath, fileName);
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => cb(relativePath));
const basePath = getDirectoryPath(fullPath);
if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
this.invokeRecursiveDirectoryWatcher(basePath, fileName);
}
}
private toFile(fileOrFolder: FileOrFolder): File {
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
return {
path: this.toPath(fullPath),
content: fileOrFolder.content,
fullPath,
fileSize: fileOrFolder.fileSize
};
}
private toFolder(path: string): Folder {
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
return {
path: this.toPath(fullPath),
entries: [],
fullPath
};
}
fileExists(s: string) {
const path = this.toFullPath(s);
return isFile(this.fs.get(path));
}
readFile(s: string) {
const fsEntry = this.fs.get(this.toFullPath(s));
return isFile(fsEntry) ? fsEntry.content : undefined;
}
getFileSize(s: string) {
const path = this.toFullPath(s);
const entry = this.fs.get(path);
if (isFile(entry)) {
return entry.fileSize ? entry.fileSize : entry.content.length;
}
return undefined;
}
directoryExists(s: string) {
const path = this.toFullPath(s);
return isFolder(this.fs.get(path));
}
getDirectories(s: string) {
const path = this.toFullPath(s);
const folder = this.fs.get(path);
if (isFolder(folder)) {
return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
}
Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder");
return [];
}
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[] {
return ts.matchFiles(this.toNormalizedAbsolutePath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => {
const directories: string[] = [];
const files: string[] = [];
const dirEntry = this.fs.get(this.toPath(dir));
if (isFolder(dirEntry)) {
dirEntry.entries.forEach((entry) => {
if (isFolder(entry)) {
directories.push(getBaseFileName(entry.fullPath));
}
else if (isFile(entry)) {
files.push(getBaseFileName(entry.fullPath));
}
else {
Debug.fail("Unknown entry");
}
});
}
return { directories, files };
});
}
watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher {
const path = this.toFullPath(directoryName);
const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories;
map.add(path, callback);
return {
referenceCount: 0,
directoryName,
close: () => map.remove(path, callback)
};
}
createHash(s: string): string {
return Harness.mockHash(s);
}
watchFile(fileName: string, callback: FileWatcherCallback) {
const path = this.toFullPath(fileName);
this.watchedFiles.add(path, callback);
return { close: () => this.watchedFiles.remove(path, callback) };
}
// TOOD: record and invoke callbacks to simulate timer events
setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) {
return this.timeoutCallbacks.register(callback, args);
}
clearTimeout(timeoutId: any): void {
this.timeoutCallbacks.unregister(timeoutId);
}
checkTimeoutQueueLengthAndRun(expected: number) {
this.checkTimeoutQueueLength(expected);
this.runQueuedTimeoutCallbacks();
}
checkTimeoutQueueLength(expected: number) {
const callbacksCount = this.timeoutCallbacks.count();
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
}
runQueuedTimeoutCallbacks() {
try {
this.timeoutCallbacks.invoke();
}
catch (e) {
if (e.message === this.existMessage) {
return;
}
throw e;
}
}
runQueuedImmediateCallbacks() {
this.immediateCallbacks.invoke();
}
setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) {
return this.immediateCallbacks.register(callback, args);
}
clearImmediate(timeoutId: any): void {
this.immediateCallbacks.unregister(timeoutId);
}
createDirectory(directoryName: string): void {
const folder = this.toFolder(directoryName);
// base folder has to be present
const base = getDirectoryPath(folder.fullPath);
const baseFolder = this.fs.get(base) as Folder;
Debug.assert(isFolder(baseFolder));
Debug.assert(!this.fs.get(folder.path));
this.addFileOrFolderInFolder(baseFolder, folder);
}
writeFile(path: string, content: string): void {
const file = this.toFile({ path, content });
// base folder has to be present
const base = getDirectoryPath(file.fullPath);
const folder = this.fs.get(base) as Folder;
Debug.assert(isFolder(folder));
this.addFileOrFolderInFolder(folder, file);
}
write(message: string) {
this.output.push(message);
}
getOutput(): ReadonlyArray<string> {
return this.output;
}
clearOutput() {
clear(this.output);
}
readonly existMessage = "System Exit";
exitCode: number;
readonly resolvePath = (s: string) => s;
readonly getExecutingFilePath = () => this.executingFilePath;
readonly getCurrentDirectory = () => this.currentDirectory;
exit(exitCode?: number) {
this.exitCode = exitCode;
throw new Error(this.existMessage);
}
readonly getEnvironmentVariable = notImplemented;
}
}

View file

@ -1,359 +0,0 @@
/// <reference path="..\compiler\commandLineParser.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="session.ts" />
namespace ts.server {
export function shouldEmitFile(scriptInfo: ScriptInfo) {
return !scriptInfo.hasMixedContent;
}
/**
* An abstract file info that maintains a shape signature.
*/
export class BuilderFileInfo {
private lastCheckedShapeSignature: string;
constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) {
}
public isExternalModuleOrHasOnlyAmbientExternalModules() {
const sourceFile = this.getSourceFile();
return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile);
}
/**
* For script files that contains only ambient external modules, although they are not actually external module files,
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
* there are no point to rebuild all script files if these special files have changed. However, if any statement
* in the file is not ambient external module, we treat it as a regular script file.
*/
private containsOnlyAmbientModules(sourceFile: SourceFile) {
for (const statement of sourceFile.statements) {
if (statement.kind !== SyntaxKind.ModuleDeclaration || (<ModuleDeclaration>statement).name.kind !== SyntaxKind.StringLiteral) {
return false;
}
}
return true;
}
private computeHash(text: string): string {
return this.project.projectService.host.createHash(text);
}
private getSourceFile(): SourceFile {
return this.project.getSourceFile(this.scriptInfo.path);
}
/**
* @return {boolean} indicates if the shape signature has changed since last update.
*/
public updateShapeSignature() {
const sourceFile = this.getSourceFile();
if (!sourceFile) {
return true;
}
const lastSignature = this.lastCheckedShapeSignature;
if (sourceFile.isDeclarationFile) {
this.lastCheckedShapeSignature = this.computeHash(sourceFile.text);
}
else {
const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true);
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text);
}
}
return !lastSignature || this.lastCheckedShapeSignature !== lastSignature;
}
}
export interface Builder {
readonly project: Project;
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
onProjectUpdateGraph(): void;
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
clear(): void;
}
abstract class AbstractBuilder<T extends BuilderFileInfo> implements Builder {
/**
* stores set of files from the project.
* NOTE: this field is created on demand and should not be accessed directly.
* Use 'getFileInfos' instead.
*/
private fileInfos_doNotAccessDirectly: Map<T>;
constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
}
private getFileInfos() {
return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createMap<T>());
}
protected hasFileInfos() {
return !!this.fileInfos_doNotAccessDirectly;
}
public clear() {
// drop the existing list - it will be re-created as necessary
this.fileInfos_doNotAccessDirectly = undefined;
}
protected getFileInfo(path: Path): T {
return this.getFileInfos().get(path);
}
protected getOrCreateFileInfo(path: Path): T {
let fileInfo = this.getFileInfo(path);
if (!fileInfo) {
const scriptInfo = this.project.getScriptInfo(path);
fileInfo = new this.ctor(scriptInfo, this.project);
this.setFileInfo(path, fileInfo);
}
return fileInfo;
}
protected getFileInfoPaths(): Path[] {
return arrayFrom(this.getFileInfos().keys() as Iterator<Path>);
}
protected setFileInfo(path: Path, info: T) {
this.getFileInfos().set(path, info);
}
protected removeFileInfo(path: Path) {
this.getFileInfos().delete(path);
}
protected forEachFileInfo(action: (fileInfo: T) => any) {
this.getFileInfos().forEach(action);
}
abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
abstract onProjectUpdateGraph(): void;
protected abstract ensureFileInfoIfInProject(scriptInfo: ScriptInfo): void;
/**
* @returns {boolean} whether the emit was conducted or not
*/
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
this.ensureFileInfoIfInProject(scriptInfo);
const fileInfo = this.getFileInfo(scriptInfo.path);
if (!fileInfo) {
return false;
}
const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false);
if (!emitSkipped) {
const projectRootPath = this.project.getProjectRootPath();
for (const outputFile of outputFiles) {
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName));
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
}
}
return !emitSkipped;
}
}
class NonModuleBuilder extends AbstractBuilder<BuilderFileInfo> {
constructor(public readonly project: Project) {
super(project, BuilderFileInfo);
}
protected ensureFileInfoIfInProject(scriptInfo: ScriptInfo) {
if (this.project.containsScriptInfo(scriptInfo)) {
this.getOrCreateFileInfo(scriptInfo.path);
}
}
onProjectUpdateGraph() {
if (this.hasFileInfos()) {
this.forEachFileInfo(fileInfo => {
if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
// This file was deleted from this project
this.removeFileInfo(fileInfo.scriptInfo.path);
}
});
}
}
/**
* Note: didn't use path as parameter because the returned file names will be directly
* consumed by the API user, which will use it to interact with file systems. Path
* should only be used internally, because the case sensitivity is not trustable.
*/
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
const info = this.getOrCreateFileInfo(scriptInfo.path);
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
if (info.updateShapeSignature()) {
const options = this.project.getCompilerOptions();
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
// so returning the file itself is good enough.
if (options && (options.out || options.outFile)) {
return singleFileResult;
}
return this.project.getAllEmittableFiles();
}
return singleFileResult;
}
}
class ModuleBuilderFileInfo extends BuilderFileInfo {
references = createSortedArray<ModuleBuilderFileInfo>();
readonly referencedBy = createSortedArray<ModuleBuilderFileInfo>();
scriptVersionForReferences: string;
static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): Comparison {
return compareStrings(lf.scriptInfo.fileName, rf.scriptInfo.fileName);
}
addReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
insertSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
}
removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
removeSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
}
removeFileReferences() {
for (const reference of this.references) {
reference.removeReferencedBy(this);
}
clear(this.references);
}
}
class ModuleBuilder extends AbstractBuilder<ModuleBuilderFileInfo> {
constructor(public readonly project: Project) {
super(project, ModuleBuilderFileInfo);
}
private projectVersionForDependencyGraph: string;
public clear() {
this.projectVersionForDependencyGraph = undefined;
super.clear();
}
private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): SortedArray<ModuleBuilderFileInfo> {
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return createSortedArray();
}
const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path);
return toSortedArray(referencedFilePaths.map(f => this.getOrCreateFileInfo(f)), ModuleBuilderFileInfo.compareFileInfos);
}
protected ensureFileInfoIfInProject(_scriptInfo: ScriptInfo) {
this.ensureProjectDependencyGraphUpToDate();
}
onProjectUpdateGraph() {
// Update the graph only if we have computed graph earlier
if (this.hasFileInfos()) {
this.ensureProjectDependencyGraphUpToDate();
}
}
private ensureProjectDependencyGraphUpToDate() {
if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) {
const currentScriptInfos = this.project.getScriptInfos();
for (const scriptInfo of currentScriptInfos) {
const fileInfo = this.getOrCreateFileInfo(scriptInfo.path);
this.updateFileReferences(fileInfo);
}
this.forEachFileInfo(fileInfo => {
if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
// This file was deleted from this project
fileInfo.removeFileReferences();
this.removeFileInfo(fileInfo.scriptInfo.path);
}
});
this.projectVersionForDependencyGraph = this.project.getProjectVersion();
}
}
private updateFileReferences(fileInfo: ModuleBuilderFileInfo) {
// Only need to update if the content of the file changed.
if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) {
return;
}
const newReferences = this.getReferencedFileInfos(fileInfo);
const oldReferences = fileInfo.references;
enumerateInsertsAndDeletes(newReferences, oldReferences,
/*inserted*/ newReference => newReference.addReferencedBy(fileInfo),
/*deleted*/ oldReference => {
// New reference is greater then current reference. That means
// the current reference doesn't exist anymore after parsing. So delete
// references.
oldReference.removeReferencedBy(fileInfo);
},
/*compare*/ ModuleBuilderFileInfo.compareFileInfos);
fileInfo.references = newReferences;
fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion();
}
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
this.ensureProjectDependencyGraphUpToDate();
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
const fileInfo = this.getFileInfo(scriptInfo.path);
if (!fileInfo || !fileInfo.updateShapeSignature()) {
return singleFileResult;
}
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return this.project.getAllEmittableFiles();
}
const options = this.project.getCompilerOptions();
if (options && (options.isolatedModules || options.out || options.outFile)) {
return singleFileResult;
}
// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
// Use slice to clone the array to avoid manipulating in place
const queue = fileInfo.referencedBy.slice(0);
const fileNameSet = createMap<ScriptInfo>();
fileNameSet.set(scriptInfo.fileName, scriptInfo);
while (queue.length > 0) {
const processingFileInfo = queue.pop();
if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) {
for (const potentialFileInfo of processingFileInfo.referencedBy) {
if (!fileNameSet.has(potentialFileInfo.scriptInfo.fileName)) {
queue.push(potentialFileInfo);
}
}
}
fileNameSet.set(processingFileInfo.scriptInfo.fileName, processingFileInfo.scriptInfo);
}
const result: string[] = [];
fileNameSet.forEach((scriptInfo, fileName) => {
if (shouldEmitFile(scriptInfo)) {
result.push(fileName);
}
});
return result;
}
}
export function createBuilder(project: Project): Builder {
const moduleKind = project.getCompilerOptions().module;
switch (moduleKind) {
case ModuleKind.None:
return new NonModuleBuilder(project);
default:
return new ModuleBuilder(project);
}
}
}

View file

@ -342,7 +342,7 @@ namespace ts.server {
convertDiagnostic(entry: protocol.DiagnosticWithLinePosition, _fileName: string): Diagnostic {
let category: DiagnosticCategory;
for (const id in DiagnosticCategory) {
if (typeof id === "string" && entry.category === id.toLowerCase()) {
if (isString(id) && entry.category === id.toLowerCase()) {
category = (<any>DiagnosticCategory)[id];
}
}

View file

@ -10,14 +10,14 @@
namespace ts.server {
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
export const ContextEvent = "context";
export const ProjectChangedEvent = "projectChanged";
export const ConfigFileDiagEvent = "configFileDiag";
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
export const ProjectInfoTelemetryEvent = "projectInfo";
export interface ContextEvent {
eventName: typeof ContextEvent;
data: { project: Project; fileName: NormalizedPath };
export interface ProjectChangedEvent {
eventName: typeof ProjectChangedEvent;
data: { project: Project; filesToEmit: string[]; changedFiles: string[] };
}
export interface ConfigFileDiagEvent {
@ -77,7 +77,7 @@ namespace ts.server {
readonly dts: number;
}
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
export type ProjectServiceEvent = ProjectChangedEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
export interface ProjectServiceEventHandler {
(event: ProjectServiceEvent): void;
@ -159,7 +159,7 @@ namespace ts.server {
};
export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings {
if (typeof protocolOptions.indentStyle === "string") {
if (isString(protocolOptions.indentStyle)) {
protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase());
Debug.assert(protocolOptions.indentStyle !== undefined);
}
@ -169,7 +169,7 @@ namespace ts.server {
export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin {
compilerOptionConverters.forEach((mappedValues, id) => {
const propertyValue = protocolOptions[id];
if (typeof propertyValue === "string") {
if (isString(propertyValue)) {
protocolOptions[id] = mappedValues.get(propertyValue.toLowerCase());
}
});
@ -177,9 +177,7 @@ namespace ts.server {
}
export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind {
return typeof scriptKindName === "string"
? convertScriptKindName(scriptKindName)
: scriptKindName;
return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName;
}
export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) {
@ -249,7 +247,8 @@ namespace ts.server {
WildcardDirectories = "Wild card directory",
TypeRoot = "Type root of the project",
ClosedScriptInfo = "Closed Script info",
ConfigFileForInferredRoot = "Config file for the inferred project root"
ConfigFileForInferredRoot = "Config file for the inferred project root",
FailedLookupLocation = "Failed lookup locations in module resolution"
}
/* @internal */
@ -493,9 +492,32 @@ namespace ts.server {
if (this.pendingProjectUpdates.delete(projectName)) {
project.updateGraph();
}
// Send the update event to notify about the project changes
this.sendProjectChangedEvent(project);
});
}
private sendProjectChangedEvent(project: Project) {
if (project.isClosed() || !this.eventHandler || !project.languageServiceEnabled) {
return;
}
const { filesToEmit, changedFiles } = project.getChangedFiles();
if (changedFiles.length === 0) {
return;
}
const event: ProjectChangedEvent = {
eventName: ProjectChangedEvent,
data: {
project,
filesToEmit: filesToEmit as string[],
changedFiles: changedFiles as string[]
}
};
this.eventHandler(event);
}
/* @internal */
delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) {
this.delayUpdateProjectGraph(project);
@ -567,6 +589,11 @@ namespace ts.server {
return scriptInfo && !scriptInfo.isOrphan() && scriptInfo.getDefaultProject();
}
getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) {
this.ensureProjectStructuresUptoDate();
return this.getScriptInfo(uncheckedFileName);
}
/**
* Ensures the project structures are upto date
* This means,
@ -674,25 +701,12 @@ namespace ts.server {
// update projects to make sure that set of referenced files is correct
this.delayUpdateProjectGraphs(containingProjects);
// TODO: (sheetalkamat) Someway to send this event so that error checks are updated?
// if (!this.eventHandler) {
// return;
// }
// for (const openFile of this.openFiles) {
// const event: ContextEvent = {
// eventName: ContextEvent,
// data: { project: openFile.getDefaultProject(), fileName: openFile.fileName }
// };
// this.eventHandler(event);
// }
}
}
/* @internal */
onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) {
project.getCachedServerHost().addOrDeleteFileOrFolder(fileName);
onTypeRootFileChanged(project: ConfiguredProject, fileOrFolder: NormalizedPath) {
project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder));
project.updateTypes();
this.delayUpdateProjectGraphAndInferredProjectsRefresh(project);
}
@ -703,15 +717,15 @@ namespace ts.server {
* @param fileName the absolute file name that changed in watched directory
*/
/* @internal */
onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: NormalizedPath) {
project.getCachedServerHost().addOrDeleteFileOrFolder(fileName);
onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileOrFolder: NormalizedPath) {
project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder));
const configFilename = project.getConfigFilePath();
// If a change was made inside "folder/file", node will trigger the callback twice:
// one with the fileName being "folder/file", and the other one with "folder".
// We don't respond to the second one.
if (fileName && !isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) {
this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileName}`);
if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) {
this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrFolder}`);
return;
}
@ -1019,7 +1033,7 @@ namespace ts.server {
if (this.configuredProjects.has(canonicalConfigFilePath)) {
watches.push(WatchType.ConfigFilePath);
}
this.logger.info(`ConfigFilePresence:: Current Watches: ['${watches.join("','")}']:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`);
this.logger.info(`ConfigFilePresence:: Current Watches: ${watches}:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`);
}
/**
@ -1371,7 +1385,7 @@ namespace ts.server {
}
private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) {
const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName);
const cachedServerHost = new CachedServerHost(this.host);
const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost);
this.logger.info(`Opened configuration file ${configFileName}`);
const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader);
@ -1425,7 +1439,7 @@ namespace ts.server {
else {
const scriptKind = propertyReader.getScriptKind(f);
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent);
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.lsHost.host);
path = scriptInfo.path;
// If this script info is not already a root add it
if (!project.isRoot(scriptInfo)) {
@ -1579,9 +1593,12 @@ namespace ts.server {
* @param uncheckedFileName is absolute pathname
* @param fileContent is a known version of the file content that is more up to date than the one on disk
*/
getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) {
return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind);
/*@internal*/
getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: ServerHost) {
return this.getOrCreateScriptInfoForNormalizedPath(
toNormalizedPath(uncheckedFileName), openedByClient, /*fileContent*/ undefined,
/*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn
);
}
getScriptInfo(uncheckedFileName: string) {
@ -1606,11 +1623,11 @@ namespace ts.server {
}
}
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) {
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: ServerHost) {
const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
let info = this.getScriptInfoForPath(path);
if (!info) {
if (openedByClient || this.host.fileExists(fileName)) {
if (openedByClient || (hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path);
this.filenameToScriptInfo.set(info.path, info);
@ -2063,7 +2080,7 @@ namespace ts.server {
// RegExp group numbers are 1-based, but the first element in groups
// is actually the original string, so it all works out in the end.
if (typeof groupNumberOrString === "number") {
if (typeof groups[groupNumberOrString] !== "string") {
if (!isString(groups[groupNumberOrString])) {
// Specification was wrong - exclude nothing!
this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`);
// * can't appear in a filename; escape it because it's feeding into a RegExp

View file

@ -1,20 +1,21 @@
/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts" />
/// <reference path="scriptInfo.ts" />
/// <reference path="..\compiler\resolutionCache.ts" />
namespace ts.server {
/*@internal*/
export class CachedServerHost implements ServerHost {
args: string[];
newLine: string;
useCaseSensitiveFileNames: boolean;
private readonly cachedPartialSystem: CachedPartialSystem;
readonly trace: (s: string) => void;
readonly realpath?: (path: string) => string;
private cachedReadDirectoryResult = createMap<FileSystemEntries>();
private readonly currentDirectory: string;
constructor(private readonly host: ServerHost, private getCanonicalFileName: (fileName: string) => string) {
constructor(private readonly host: ServerHost) {
this.args = host.args;
this.newLine = host.newLine;
this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames;
@ -24,41 +25,7 @@ namespace ts.server {
if (this.host.realpath) {
this.realpath = path => this.host.realpath(path);
}
this.currentDirectory = this.host.getCurrentDirectory();
}
private toPath(fileName: string) {
return toPath(fileName, this.currentDirectory, this.getCanonicalFileName);
}
private getFileSystemEntries(rootDir: string) {
const path = this.toPath(rootDir);
const cachedResult = this.cachedReadDirectoryResult.get(path);
if (cachedResult) {
return cachedResult;
}
const resultFromHost: FileSystemEntries = {
files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [],
directories: this.host.getDirectories(rootDir) || []
};
this.cachedReadDirectoryResult.set(path, resultFromHost);
return resultFromHost;
}
private canWorkWithCacheForDir(rootDir: string) {
// Some of the hosts might not be able to handle read directory or getDirectories
const path = this.toPath(rootDir);
if (this.cachedReadDirectoryResult.get(path)) {
return true;
}
try {
return this.getFileSystemEntries(rootDir);
}
catch (_e) {
return false;
}
this.cachedPartialSystem = createCachedPartialSystem(host);
}
write(s: string) {
@ -66,13 +33,7 @@ namespace ts.server {
}
writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) {
const path = this.toPath(fileName);
const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path));
const baseFileName = getBaseFileName(toNormalizedPath(fileName));
if (result) {
result.files = this.updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true);
}
return this.host.writeFile(fileName, data, writeByteOrderMark);
this.cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark);
}
resolvePath(path: string) {
@ -88,7 +49,7 @@ namespace ts.server {
}
getCurrentDirectory() {
return this.currentDirectory;
return this.cachedPartialSystem.getCurrentDirectory();
}
exit(exitCode?: number) {
@ -101,78 +62,35 @@ namespace ts.server {
}
getDirectories(rootDir: string) {
if (this.canWorkWithCacheForDir(rootDir)) {
return this.getFileSystemEntries(rootDir).directories.slice();
}
return this.host.getDirectories(rootDir);
return this.cachedPartialSystem.getDirectories(rootDir);
}
readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
if (this.canWorkWithCacheForDir(rootDir)) {
return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path));
}
return this.host.readDirectory(rootDir, extensions, excludes, includes, depth);
return this.cachedPartialSystem.readDirectory(rootDir, extensions, excludes, includes, depth);
}
fileExists(fileName: string): boolean {
const path = this.toPath(fileName);
const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path));
const baseName = getBaseFileName(toNormalizedPath(fileName));
return (result && this.hasEntry(result.files, baseName)) || this.host.fileExists(fileName);
return this.cachedPartialSystem.fileExists(fileName);
}
directoryExists(dirPath: string) {
const path = this.toPath(dirPath);
return this.cachedReadDirectoryResult.has(path) || this.host.directoryExists(dirPath);
return this.cachedPartialSystem.directoryExists(dirPath);
}
readFile(path: string, encoding?: string): string {
return this.host.readFile(path, encoding);
}
private fileNameEqual(name1: string, name2: string) {
return this.getCanonicalFileName(name1) === this.getCanonicalFileName(name2);
addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath, fileOrFolderPath: Path) {
return this.cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath);
}
private hasEntry(entries: ReadonlyArray<string>, name: string) {
return some(entries, file => this.fileNameEqual(file, name));
}
private updateFileSystemEntry(entries: ReadonlyArray<string>, baseName: string, isValid: boolean) {
if (this.hasEntry(entries, baseName)) {
if (!isValid) {
return filter(entries, entry => !this.fileNameEqual(entry, baseName));
}
}
else if (isValid) {
return entries.concat(baseName);
}
return entries;
}
addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) {
const path = this.toPath(fileOrFolder);
const existingResult = this.cachedReadDirectoryResult.get(path);
if (existingResult) {
// This was a folder already present, remove it if this doesnt exist any more
if (!this.host.directoryExists(fileOrFolder)) {
this.cachedReadDirectoryResult.delete(path);
}
}
else {
// This was earlier a file (hence not in cached directory contents)
// or we never cached the directory containing it
const parentResult = this.cachedReadDirectoryResult.get(getDirectoryPath(path));
if (parentResult) {
const baseName = getBaseFileName(fileOrFolder);
parentResult.files = this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path));
parentResult.directories = this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path));
}
}
addOrDeleteFile(file: string, path: Path, eventKind: FileWatcherEventKind) {
return this.cachedPartialSystem.addOrDeleteFile(file, path, eventKind);
}
clearCache() {
this.cachedReadDirectoryResult = createMap<FileSystemEntries>();
return this.cachedPartialSystem.clearCache();
}
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) {
@ -190,17 +108,16 @@ namespace ts.server {
}
type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean };
export class LSHost implements LanguageServiceHost, ModuleResolutionHost {
private compilationSettings: CompilerOptions;
private readonly resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
private readonly resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
/*@internal*/
compilationSettings: CompilerOptions;
private filesWithChangedSetOfUnresolvedImports: Path[];
private resolveModuleName: typeof resolveModuleName;
readonly trace: (s: string) => void;
readonly realpath?: (path: string) => string;
/*@internal*/
hasInvalidatedResolution: HasInvalidatedResolution;
/**
* This is the host that is associated with the project. This is normally same as projectService's host
* except in Configured projects where it is CachedServerHost so that we can cache the results of the
@ -217,25 +134,6 @@ namespace ts.server {
this.trace = s => host.trace(s);
}
this.resolveModuleName = (moduleName, containingFile, compilerOptions, host) => {
const globalCache = this.project.getTypeAcquisition().enable
? this.project.projectService.typingsInstaller.globalTypingsCacheLocation
: undefined;
const primaryResult = resolveModuleName(moduleName, containingFile, compilerOptions, host);
// return result immediately only if it is .ts, .tsx or .d.ts
if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) {
// otherwise try to load typings from @types
// create different collection of failed lookup locations for second pass
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, this.project.getProjectName(), compilerOptions, host, globalCache);
if (resolvedModule) {
return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) };
}
}
return primaryResult;
};
if (this.host.realpath) {
this.realpath = path => this.host.realpath(path);
}
@ -243,99 +141,9 @@ namespace ts.server {
dispose() {
this.project = undefined;
this.resolveModuleName = undefined;
this.host = undefined;
}
public startRecordingFilesWithChangedResolutions() {
this.filesWithChangedSetOfUnresolvedImports = [];
}
public finishRecordingFilesWithChangedResolutions() {
const collected = this.filesWithChangedSetOfUnresolvedImports;
this.filesWithChangedSetOfUnresolvedImports = undefined;
return collected;
}
private resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
names: string[],
containingFile: string,
cache: Map<Map<T>>,
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined,
logChanges: boolean): R[] {
const path = this.project.projectService.toPath(containingFile);
const currentResolutionsInFile = cache.get(path);
const newResolutions: Map<T> = createMap<T>();
const resolvedModules: R[] = [];
const compilerOptions = this.getCompilationSettings();
for (const name of names) {
// check if this is a duplicate entry in the list
let resolution = newResolutions.get(name);
if (!resolution) {
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
if (moduleResolutionIsValid(existingResolution)) {
// ok, it is safe to use existing name resolution results
resolution = existingResolution;
}
else {
resolution = loader(name, containingFile, compilerOptions, this);
newResolutions.set(name, resolution);
}
if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
this.filesWithChangedSetOfUnresolvedImports.push(path);
// reset log changes to avoid recording the same file multiple times
logChanges = false;
}
}
Debug.assert(resolution !== undefined);
resolvedModules.push(getResult(resolution));
}
// replace old results with a new one
cache.set(path, newResolutions);
return resolvedModules;
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
if (oldResolution === newResolution) {
return true;
}
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
return false;
}
const oldResult = getResult(oldResolution);
const newResult = getResult(newResolution);
if (oldResult === newResult) {
return true;
}
if (!oldResult || !newResult) {
return false;
}
return getResultFileName(oldResult) === getResultFileName(newResult);
}
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution || resolution.isInvalidated) {
return false;
}
const result = getResult(resolution);
if (result) {
return true;
}
// consider situation if we have no candidate locations as valid resolution.
// after all there is no point to invalidate it if we have no idea where to look for the module.
return resolution.failedLookupLocations.length === 0;
}
}
getNewLine() {
return this.host.newLine;
}
@ -357,13 +165,11 @@ namespace ts.server {
}
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
return this.project.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile);
}
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] {
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName,
m => m.resolvedModule, r => r.resolvedFileName, /*logChanges*/ true);
return this.project.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true);
}
getDefaultLibFileName() {
@ -426,44 +232,5 @@ namespace ts.server {
getDirectories(path: string): string[] {
return this.host.getDirectories(path);
}
notifyFileRemoved(info: ScriptInfo) {
this.invalidateResolutionOfDeletedFile(info, this.resolvedModuleNames,
m => m.resolvedModule, r => r.resolvedFileName);
this.invalidateResolutionOfDeletedFile(info, this.resolvedTypeReferenceDirectives,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
}
private invalidateResolutionOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
deletedInfo: ScriptInfo,
cache: Map<Map<T>>,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined) {
cache.forEach((value, path) => {
if (path === deletedInfo.path) {
cache.delete(path);
}
else if (value) {
value.forEach((resolution) => {
if (resolution && !resolution.isInvalidated) {
const result = getResult(resolution);
if (result) {
if (getResultFileName(result) === deletedInfo.path) {
resolution.isInvalidated = true;
}
}
}
});
}
});
}
setCompilationSettings(opt: CompilerOptions) {
if (changesAffectModuleResolution(this.compilationSettings, opt)) {
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
this.compilationSettings = opt;
}
}
}

View file

@ -3,7 +3,7 @@
/// <reference path="scriptInfo.ts"/>
/// <reference path="lsHost.ts"/>
/// <reference path="typingsCache.ts"/>
/// <reference path="builder.ts"/>
/// <reference path="..\compiler\builder.ts"/>
namespace ts.server {
@ -127,10 +127,13 @@ namespace ts.server {
public languageServiceEnabled = true;
/*@internal*/
resolutionCache: ResolutionCache;
/*@internal*/
lsHost: LSHost;
builder: Builder;
private builder: Builder;
/**
* Set of files names that were updated since the last call to getChangesSinceVersion.
*/
@ -210,7 +213,16 @@ namespace ts.server {
this.setInternalCompilerOptionsForEmittingJsFiles();
this.lsHost = new LSHost(host, this, this.projectService.cancellationToken);
this.lsHost.setCompilationSettings(this.compilerOptions);
this.resolutionCache = createResolutionCache(
fileName => this.projectService.toPath(fileName),
() => this.compilerOptions,
(failedLookupLocation, failedLookupLocationPath, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name),
s => this.projectService.logger.info(s),
this.getProjectName(),
() => this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined
);
this.lsHost.compilationSettings = this.compilerOptions;
this.resolutionCache.setModuleResolutionHost(this.lsHost);
this.languageService = createLanguageService(this.lsHost, this.documentRegistry);
@ -218,10 +230,22 @@ namespace ts.server {
this.disableLanguageService();
}
this.builder = createBuilder(this);
this.markAsDirty();
}
private watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) {
// There is some kind of change in the failed lookup location, update the program
return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (fileName, eventKind) => {
this.projectService.logger.info(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`);
if (this.projectKind === ProjectKind.Configured) {
(this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind);
}
this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath);
this.markAsDirty();
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
});
}
private setInternalCompilerOptionsForEmittingJsFiles() {
if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) {
this.compilerOptions.noEmitForJsFiles = true;
@ -246,12 +270,47 @@ namespace ts.server {
return this.languageService;
}
private ensureBuilder() {
if (!this.builder) {
this.builder = createBuilder(
this.projectService.toCanonicalFileName,
(_program, sourceFile, emitOnlyDts, isDetailed) => this.getFileEmitOutput(sourceFile, emitOnlyDts, isDetailed),
data => this.projectService.host.createHash(data),
sourceFile => !this.projectService.getScriptInfoForPath(sourceFile.path).hasMixedContent
);
}
}
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
if (!this.languageServiceEnabled) {
return [];
}
this.updateGraph();
return this.builder.getFilesAffectedBy(scriptInfo);
this.ensureBuilder();
return this.builder.getFilesAffectedBy(this.program, scriptInfo.path);
}
/**
* Returns true if emit was conducted
*/
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
this.ensureBuilder();
const { emitSkipped, outputFiles } = this.builder.emitFile(this.program, scriptInfo.path);
if (!emitSkipped) {
const projectRootPath = this.getProjectRootPath();
for (const outputFile of outputFiles) {
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName));
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
}
}
return !emitSkipped;
}
getChangedFiles() {
Debug.assert(this.languageServiceEnabled);
this.ensureBuilder();
return this.builder.getChangedProgramFiles(this.program);
}
getProjectVersion() {
@ -320,6 +379,8 @@ namespace ts.server {
this.rootFilesMap = undefined;
this.program = undefined;
this.builder = undefined;
this.resolutionCache.clear();
this.resolutionCache = undefined;
this.cachedUnresolvedImportsPerFile = undefined;
this.lsHost.dispose();
this.lsHost = undefined;
@ -337,6 +398,10 @@ namespace ts.server {
this.languageService = undefined;
}
isClosed() {
return this.lsHost === undefined;
}
getCompilerOptions() {
return this.compilerOptions;
}
@ -391,11 +456,11 @@ namespace ts.server {
});
}
getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) {
private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) {
if (!this.languageServiceEnabled) {
return undefined;
}
return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed);
}
getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) {
@ -454,21 +519,6 @@ namespace ts.server {
return false;
}
getAllEmittableFiles() {
if (!this.languageServiceEnabled) {
return [];
}
const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions);
const infos = this.getScriptInfos();
const result: string[] = [];
for (const info of infos) {
if (getBaseFileName(info.fileName) !== defaultLibraryFileName && shouldEmitFile(info)) {
result.push(info.fileName);
}
}
return result;
}
containsScriptInfo(info: ScriptInfo): boolean {
return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined);
}
@ -505,7 +555,7 @@ namespace ts.server {
if (this.isRoot(info)) {
this.removeRoot(info);
}
this.lsHost.notifyFileRemoved(info);
this.resolutionCache.invalidateResolutionOfFile(info.path);
this.cachedUnresolvedImportsPerFile.remove(info.path);
if (detachFromProject) {
@ -560,11 +610,12 @@ namespace ts.server {
* @returns: true if set of files in the project stays the same and false - otherwise.
*/
updateGraph(): boolean {
this.lsHost.startRecordingFilesWithChangedResolutions();
this.resolutionCache.startRecordingFilesWithChangedResolutions();
this.lsHost.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution();
let hasChanges = this.updateGraphWorker();
const changedFiles: ReadonlyArray<Path> = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray;
const changedFiles: ReadonlyArray<Path> = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray;
for (const file of changedFiles) {
// delete cached information for changed files
@ -594,11 +645,14 @@ namespace ts.server {
// update builder only if language service is enabled
// otherwise tell it to drop its internal state
if (this.languageServiceEnabled) {
this.builder.onProjectUpdateGraph();
}
else {
this.builder.clear();
// Note we are retaining builder so we can send events for project change
if (this.builder) {
if (this.languageServiceEnabled) {
this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution);
}
else {
this.builder.clear();
}
}
if (hasChanges) {
@ -639,41 +693,15 @@ namespace ts.server {
}
}
const missingFilePaths = this.program.getMissingFilePaths();
const newMissingFilePathMap = arrayToSet(missingFilePaths);
// Update the missing file paths watcher
mutateMap(
updateMissingFilePathsWatch(
this.program,
this.missingFilesMap || (this.missingFilesMap = createMap()),
newMissingFilePathMap,
{
// Watch the missing files
createNewValue: missingFilePath => {
const fileWatcher = this.projectService.addFileWatcher(
WatchType.MissingFilePath, this, missingFilePath,
(filename, eventKind) => {
if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) {
this.missingFilesMap.delete(missingFilePath);
this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated);
if (this.projectKind === ProjectKind.Configured) {
const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath));
(this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath));
}
// When a missing file is created, we should update the graph.
this.markAsDirty();
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
}
}
);
return fileWatcher;
},
// Files that are no longer missing (e.g. because they are no longer required)
// should no longer be watched.
onDeleteExistingValue: (missingFilePath, fileWatcher) => {
this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded);
}
}
// Watch the missing files
missingFilePath => this.addMissingFileWatcher(missingFilePath),
// Files that are no longer missing (e.g. because they are no longer required)
// should no longer be watched.
(missingFilePath, fileWatcher) => this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded)
);
}
@ -684,7 +712,7 @@ namespace ts.server {
// by the LSHost for files in the program when the program is retrieved above but
// the program doesn't contain external files so this must be done explicitly.
inserted => {
const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false);
const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.lsHost.host);
scriptInfo.attachToProject(this);
},
removed => {
@ -697,12 +725,37 @@ namespace ts.server {
return hasChanges;
}
private addMissingFileWatcher(missingFilePath: Path) {
const fileWatcher = this.projectService.addFileWatcher(
WatchType.MissingFilePath, this, missingFilePath,
(fileName, eventKind) => {
if (this.projectKind === ProjectKind.Configured) {
(this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, missingFilePath, eventKind);
}
if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) {
this.missingFilesMap.delete(missingFilePath);
this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.FileCreated);
// When a missing file is created, we should update the graph.
this.markAsDirty();
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
}
}
);
return fileWatcher;
}
private closeMissingFileWatcher(missingFilePath: Path, fileWatcher: FileWatcher, reason: WatcherCloseReason) {
this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, reason);
}
isWatchedMissingFile(path: Path) {
return this.missingFilesMap && this.missingFilesMap.has(path);
}
getScriptInfoLSHost(fileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.lsHost.host);
if (scriptInfo) {
const existingValue = this.rootFilesMap.get(scriptInfo.path);
if (existingValue !== undefined && existingValue !== scriptInfo) {
@ -716,7 +769,10 @@ namespace ts.server {
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(
fileName, /*openedByClient*/ false, /*fileContent*/ undefined,
/*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.lsHost.host
);
if (scriptInfo && !scriptInfo.isAttached(this)) {
return Errors.ThrowProjectDoesNotContainDocument(fileName, this);
}
@ -746,9 +802,13 @@ namespace ts.server {
this.cachedUnresolvedImportsPerFile.clear();
this.lastCachedUnresolvedImportsList = undefined;
}
const oldOptions = this.compilerOptions;
this.compilerOptions = compilerOptions;
this.setInternalCompilerOptionsForEmittingJsFiles();
this.lsHost.setCompilationSettings(compilerOptions);
if (changesAffectModuleResolution(oldOptions, compilerOptions)) {
this.resolutionCache.clear();
}
this.lsHost.compilationSettings = this.compilerOptions;
this.markAsDirty();
}
@ -814,58 +874,6 @@ namespace ts.server {
}
}
getReferencedFiles(path: Path): Path[] {
if (!this.languageServiceEnabled) {
return [];
}
const sourceFile = this.getSourceFile(path);
if (!sourceFile) {
return [];
}
// We need to use a set here since the code can contain the same import twice,
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
const referencedFiles = createMap<true>();
if (sourceFile.imports && sourceFile.imports.length > 0) {
const checker: TypeChecker = this.program.getTypeChecker();
for (const importName of sourceFile.imports) {
const symbol = checker.getSymbolAtLocation(importName);
if (symbol && symbol.declarations && symbol.declarations[0]) {
const declarationSourceFile = symbol.declarations[0].getSourceFile();
if (declarationSourceFile) {
referencedFiles.set(declarationSourceFile.path, true);
}
}
}
}
const currentDirectory = getDirectoryPath(path);
// Handle triple slash references
if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
for (const referencedFile of sourceFile.referencedFiles) {
const referencedPath = this.projectService.toPath(referencedFile.fileName, currentDirectory);
referencedFiles.set(referencedPath, true);
}
}
// Handle type reference directives
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => {
if (!resolvedTypeReferenceDirective) {
return;
}
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
const typeFilePath = this.projectService.toPath(fileName, currentDirectory);
referencedFiles.set(typeFilePath, true);
});
}
const allFileNames = arrayFrom(referencedFiles.keys()) as Path[];
return filter(allFileNames, file => this.lsHost.host.fileExists(file));
}
// remove a root file from project
protected removeRoot(info: ScriptInfo): void {
orderedRemoveItem(this.rootFiles, info);
@ -973,11 +981,6 @@ namespace ts.server {
}
}
interface WildcardDirectoryWatcher {
watcher: FileWatcher;
flags: WatchDirectoryFlags;
}
/**
* If a file is opened, the server will look for a tsconfig (or jsconfig)
* and if successfull create a ConfiguredProject for it.
@ -1160,30 +1163,19 @@ namespace ts.server {
/*@internal*/
watchWildcards(wildcardDirectories: Map<WatchDirectoryFlags>) {
mutateMap(
updateWatchingWildcardDirectories(
this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()),
wildcardDirectories,
{
// Watcher is same if the recursive flags match
isSameValue: ({ flags: existingFlags }, flags) => existingFlags !== flags,
// Create new watch and recursive info
createNewValue: (directory, flags) => {
return {
watcher: this.projectService.addDirectoryWatcher(
WatchType.WildcardDirectories, this, directory,
path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path),
flags
),
flags
};
},
// Close existing watch thats not needed any more
onDeleteExistingValue: (directory, wildcardDirectoryWatcher) =>
this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, WatcherCloseReason.NotNeeded),
// Close existing watch that doesnt match in the recursive flags
onDeleteExistingMismatchValue: (directory, wildcardDirectoryWatcher) =>
this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, WatcherCloseReason.RecursiveChanged),
}
// Create new directory watcher
(directory, flags) => this.projectService.addDirectoryWatcher(
WatchType.WildcardDirectories, this, directory,
path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path),
flags
),
// Close directory watcher
(directory, wildcardDirectoryWatcher, flagsChanged) => this.closeWildcardDirectoryWatcher(
directory, wildcardDirectoryWatcher, flagsChanged ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded
)
);
}
@ -1214,7 +1206,7 @@ namespace ts.server {
path => this.projectService.onTypeRootFileChanged(this, path), WatchDirectoryFlags.None
),
// Close existing watch thats not needed any more
onDeleteExistingValue: (directory, watcher) => this.projectService.closeDirectoryWatcher(
onDeleteValue: (directory, watcher) => this.projectService.closeDirectoryWatcher(
WatchType.TypeRoot, this, directory, watcher, WatchDirectoryFlags.None, WatcherCloseReason.NotNeeded
)
}

View file

@ -2040,6 +2040,29 @@ namespace ts.server.protocol {
languageServiceEnabled: boolean;
}
export type ProjectChangedEventName = "projectChanged";
export interface ProjectStructureChangedEvent extends Event {
event: ProjectChangedEventName;
body: ProjectChangedEventBody;
}
export interface ProjectChangedEventBody {
/**
* Project name that has changes
*/
projectName: string;
/**
* Minimum set of file names to emit
*/
fileNamesToEmit: string[];
/**
* List of files that have changed/added/removed or could have been affected by the changed files
*/
changedFiles: string[];
}
/**
* Arguments for reload request.
*/

View file

@ -237,7 +237,7 @@ namespace ts.server {
detachAllProjects() {
for (const p of this.containingProjects) {
if (p.projectKind === ProjectKind.Configured) {
(p.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(this.fileName);
(p.lsHost.host as CachedServerHost).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted);
}
const isInfoRoot = p.isRoot(this);
// detach is unnecessary since we'll clean the list of containing projects anyways

View file

@ -538,7 +538,15 @@ namespace ts.server {
fs.stat(watchedFile.fileName, (err: any, stats: any) => {
if (err) {
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed);
if (err.code === "ENOENT") {
if (watchedFile.mtime.getTime() !== 0) {
watchedFile.mtime = new Date(0);
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted);
}
}
else {
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed);
}
}
else {
const oldTime = watchedFile.mtime.getTime();

View file

@ -332,14 +332,18 @@ namespace ts.server {
private defaultEventHandler(event: ProjectServiceEvent) {
switch (event.eventName) {
case ContextEvent:
const { project, fileName } = event.data;
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
this.errorCheck.startNew(next => this.updateErrorCheck(next, [{ fileName, project }], 100));
case ProjectChangedEvent:
const { project, filesToEmit, changedFiles } = event.data;
this.projectChangedEvent(project, filesToEmit, changedFiles);
break;
case ConfigFileDiagEvent:
const { triggerFile, configFileName, diagnostics } = event.data;
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
this.event<protocol.ConfigFileDiagnosticEventBody>({
triggerFile,
configFile,
diagnostics: bakedDiags
}, "configFileDiag");
break;
case ProjectLanguageServiceStateEvent: {
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
@ -360,6 +364,24 @@ namespace ts.server {
}
}
private projectChangedEvent(project: Project, fileNamesToEmit: string[], changedFiles: string[]): void {
this.projectService.logger.info(`got project changed event, updating diagnostics for ${changedFiles}`);
if (changedFiles.length) {
const checkList = this.createCheckList(changedFiles, project);
// For now only queue error checking for open files. We can change this to include non open files as well
this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true));
// Send project changed event
this.event<protocol.ProjectChangedEventBody>({
projectName: project.getProjectName(),
changedFiles,
fileNamesToEmit
}, "projectChanged");
}
}
public logError(err: Error, cmd: string) {
let msg = "Exception on executing command " + cmd;
if (err.message) {
@ -381,21 +403,6 @@ namespace ts.server {
this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine));
}
public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ReadonlyArray<Diagnostic>) {
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
const ev: protocol.ConfigFileDiagnosticEvent = {
seq: 0,
type: "event",
event: "configFileDiag",
body: {
triggerFile,
configFile,
diagnostics: bakedDiags
}
};
this.send(ev);
}
public event<T>(info: T, eventName: string) {
const ev: protocol.Event = {
seq: 0,
@ -1197,13 +1204,13 @@ namespace ts.server {
}
private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): ReadonlyArray<protocol.CompileOnSaveAffectedFileListSingleProject> {
const info = this.projectService.getScriptInfo(args.file);
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
const info = this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file);
if (!info) {
return emptyArray;
}
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
// if specified a project, we only return affected file list in this project
const projectsToSearch = args.projectFileName ? [this.projectService.findProject(args.projectFileName)] : info.containingProjects;
for (const project of projectsToSearch) {
@ -1227,7 +1234,7 @@ namespace ts.server {
return false;
}
const scriptInfo = project.getScriptInfo(file);
return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
return project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
}
private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems {
@ -1257,13 +1264,16 @@ namespace ts.server {
}
}
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
const checkList = mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] {
return mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
const fileName = toNormalizedPath(uncheckedFileName);
const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
return project && { fileName, project };
});
}
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
const checkList = this.createCheckList(fileNames);
if (checkList.length > 0) {
this.updateErrorCheck(next, checkList, delay);
}

View file

@ -304,59 +304,4 @@ namespace ts.server {
deleted(oldItems[oldIndex++]);
}
}
/**
* clears already present map by calling onDeleteExistingValue callback before deleting that key/value
*/
export function clearMap<T>(map: Map<T>, onDeleteExistingValue: (key: string, existingValue: T) => void) {
// Remove all
map.forEach((existingValue, key) => {
onDeleteExistingValue(key, existingValue);
});
map.clear();
}
export interface MutateMapOptions<T, U> {
createNewValue(key: string, valueInNewMap: U): T;
onDeleteExistingValue(key: string, existingValue: T, isNotSame?: boolean): void;
isSameValue?(existingValue: T, valueInNewMap: U): boolean;
onDeleteExistingMismatchValue?(key: string, existingValue: T): void;
}
/**
* Mutates the map with newMap such that keys in map will be same as newMap.
*/
export function mutateMap<T, U>(map: Map<T>, newMap: ReadonlyMap<U>, options: MutateMapOptions<T, U>) {
// If there are new values update them
if (newMap) {
const { isSameValue, createNewValue, onDeleteExistingValue, onDeleteExistingMismatchValue } = options;
// Needs update
map.forEach((existingValue, key) => {
const valueInNewMap = newMap.get(key);
// Not present any more in new map, remove it
if (valueInNewMap === undefined) {
map.delete(key);
onDeleteExistingValue(key, existingValue);
}
// different value - remove it
else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) {
Debug.assert(!!onDeleteExistingMismatchValue);
map.delete(key);
onDeleteExistingMismatchValue(key, existingValue);
}
});
// Add new values that are not already present
newMap.forEach((valueInNewMap, key) => {
if (!map.has(key)) {
// New values
map.set(key, createNewValue(key, valueInNewMap));
}
});
}
else {
clearMap(map, options.onDeleteExistingValue);
}
}
}

View file

@ -814,18 +814,21 @@ namespace ts {
return codefix.getSupportedErrorCodes();
}
// Either it will be file name if host doesnt have file or it will be the host's file information
type CachedHostFileInformation = HostFileInformation | string;
// Cache host information about script Should be refreshed
// at each language service public entry point, since we don't know when
// the set of scripts handled by the host changes.
class HostCache {
private fileNameToEntry: Map<HostFileInformation>;
private fileNameToEntry: Map<CachedHostFileInformation>;
private _compilationSettings: CompilerOptions;
private currentDirectory: string;
constructor(private host: LanguageServiceHost, getCanonicalFileName: (fileName: string) => string) {
// script id => script index
this.currentDirectory = host.getCurrentDirectory();
this.fileNameToEntry = createMap<HostFileInformation>();
this.fileNameToEntry = createMap<CachedHostFileInformation>();
// Initialize the list with the root file names
const rootFileNames = host.getScriptFileNames();
@ -842,7 +845,7 @@ namespace ts {
}
private createEntry(fileName: string, path: Path) {
let entry: HostFileInformation;
let entry: CachedHostFileInformation;
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
if (scriptSnapshot) {
entry = {
@ -852,36 +855,41 @@ namespace ts {
scriptKind: getScriptKind(fileName, this.host)
};
}
else {
entry = fileName;
}
this.fileNameToEntry.set(path, entry);
return entry;
}
public getEntryByPath(path: Path): HostFileInformation {
public getEntryByPath(path: Path): CachedHostFileInformation | undefined {
return this.fileNameToEntry.get(path);
}
public containsEntryByPath(path: Path): boolean {
return this.fileNameToEntry.has(path);
public getHostFileInformation(path: Path): HostFileInformation | undefined {
const entry = this.fileNameToEntry.get(path);
return !isString(entry) ? entry : undefined;
}
public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
return this.containsEntryByPath(path)
? this.getEntryByPath(path)
: this.createEntry(fileName, path);
const info = this.getEntryByPath(path) || this.createEntry(fileName, path);
return isString(info) ? undefined : info;
}
public getRootFileNames(): string[] {
return this.host.getScriptFileNames();
return arrayFrom(this.fileNameToEntry.values(), entry => {
return isString(entry) ? entry : entry.hostFileName;
});
}
public getVersion(path: Path): string {
const file = this.getEntryByPath(path);
const file = this.getHostFileInformation(path);
return file && file.version;
}
public getScriptSnapshot(path: Path): IScriptSnapshot {
const file = this.getEntryByPath(path);
const file = this.getHostFileInformation(path);
return file && file.scriptSnapshot;
}
}
@ -1108,9 +1116,12 @@ namespace ts {
// Get a fresh cache of the host information
let hostCache = new HostCache(host, getCanonicalFileName);
const rootFileNames = hostCache.getRootFileNames();
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
// If the program is already up-to-date, we can reuse it
if (programUpToDate()) {
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) {
return;
}
@ -1120,18 +1131,7 @@ namespace ts {
// the program points to old source files that have been invalidated because of
// incremental parsing.
const oldSettings = program && program.getCompilerOptions();
const newSettings = hostCache.compilationSettings();
const shouldCreateNewSourceFiles = oldSettings &&
(oldSettings.target !== newSettings.target ||
oldSettings.module !== newSettings.module ||
oldSettings.moduleResolution !== newSettings.moduleResolution ||
oldSettings.noResolve !== newSettings.noResolve ||
oldSettings.jsx !== newSettings.jsx ||
oldSettings.allowJs !== newSettings.allowJs ||
oldSettings.disableSizeLimit !== oldSettings.disableSizeLimit ||
oldSettings.baseUrl !== newSettings.baseUrl ||
!equalOwnProperties(oldSettings.paths, newSettings.paths));
// Now create a new compiler
const compilerHost: CompilerHost = {
@ -1144,19 +1144,13 @@ namespace ts {
getDefaultLibFileName: (options) => host.getDefaultLibFileName(options),
writeFile: noop,
getCurrentDirectory: () => currentDirectory,
fileExists: (fileName): boolean => {
// stub missing host functionality
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
return hostCache.containsEntryByPath(path) ?
!!hostCache.getEntryByPath(path) :
(host.fileExists && host.fileExists(fileName));
},
fileExists,
readFile(fileName) {
// stub missing host functionality
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
if (hostCache.containsEntryByPath(path)) {
const entry = hostCache.getEntryByPath(path);
return entry && entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength());
const entry = hostCache.getEntryByPath(path);
if (entry) {
return isString(entry) ? undefined : entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength());
}
return host.readFile && host.readFile(fileName);
},
@ -1165,7 +1159,9 @@ namespace ts {
},
getDirectories: path => {
return host.getDirectories ? host.getDirectories(path) : [];
}
},
onReleaseOldSourceFile,
hasInvalidatedResolution
};
if (host.trace) {
compilerHost.trace = message => host.trace(message);
@ -1181,36 +1177,37 @@ namespace ts {
}
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program);
// Release any files we have acquired in the old program but are
// not part of the new program.
if (program) {
const oldSourceFiles = program.getSourceFiles();
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldSettings);
for (const oldSourceFile of oldSourceFiles) {
if (!newProgram.getSourceFile(oldSourceFile.fileName) || shouldCreateNewSourceFiles) {
documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey);
}
}
}
program = createProgram(rootFileNames, newSettings, compilerHost, program);
// hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point.
// It needs to be cleared to allow all collected snapshots to be released
hostCache = undefined;
program = newProgram;
// Make sure all the nodes in the program are both bound, and have their parent
// pointers set property.
program.getTypeChecker();
return;
function getOrCreateSourceFile(fileName: string): SourceFile {
return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName));
function fileExists(fileName: string) {
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache.getEntryByPath(path);
return entry ?
!isString(entry) :
(host.fileExists && host.fileExists(fileName));
}
function getOrCreateSourceFileByPath(fileName: string, path: Path): SourceFile {
// Release any files we have acquired in the old program but are
// not part of the new program.
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions);
documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey);
}
function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile);
}
function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
Debug.assert(hostCache !== undefined);
// The program is asking for this file, check first if the host can locate it.
// If the host can not locate the file, then it does not exist. return undefined
@ -1223,7 +1220,7 @@ namespace ts {
// Check if the language version has changed since we last created a program; if they are the same,
// it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile
// can not be reused. we have to dump all syntax trees and create new ones.
if (!shouldCreateNewSourceFiles) {
if (!shouldCreateNewSourceFile) {
// Check if the old program had this file already
const oldSourceFile = program && program.getSourceFileByPath(path);
if (oldSourceFile) {
@ -1263,49 +1260,6 @@ namespace ts {
// Could not find this file in the old program, create a new SourceFile for it.
return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
}
function sourceFileUpToDate(sourceFile: SourceFile): boolean {
if (!sourceFile) {
return false;
}
const path = sourceFile.path || toPath(sourceFile.fileName, currentDirectory, getCanonicalFileName);
return sourceFile.version === hostCache.getVersion(path);
}
function programUpToDate(): boolean {
// If we haven't create a program yet, then it is not up-to-date
if (!program) {
return false;
}
// If number of files in the program do not match, it is not up-to-date
const rootFileNames = hostCache.getRootFileNames();
if (program.getSourceFiles().length !== rootFileNames.length) {
return false;
}
// If any file is not up-to-date, then the whole program is not up-to-date
for (const fileName of rootFileNames) {
if (!sourceFileUpToDate(program.getSourceFile(fileName))) {
return false;
}
}
const currentOptions = program.getCompilerOptions();
const newOptions = hostCache.compilationSettings();
// If the compilation settings do no match, then the program is not up-to-date
if (!compareDataObjects(currentOptions, newOptions)) {
return false;
}
// If everything matches but the text of config file is changed,
// error locations can change for program options, so update the program
if (currentOptions.configFile && newOptions.configFile) {
return currentOptions.configFile.text === newOptions.configFile.text;
}
return true;
}
}
function getProgram(): Program {
@ -1520,23 +1474,12 @@ namespace ts {
return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles);
}
function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput {
function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean) {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const outputFiles: OutputFile[] = [];
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) {
outputFiles.push({ name: fileName, writeByteOrderMark, text });
}
const customTransformers = host.getCustomTransformers && host.getCustomTransformers();
const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
return {
outputFiles,
emitSkipped: emitOutput.emitSkipped
};
return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, isDetailed, cancellationToken, customTransformers);
}
// Signature help

View file

@ -527,7 +527,7 @@ namespace ts {
if (logPerformance) {
const end = timestamp();
logger.log(`${actionDescription} completed in ${end - start} msec`);
if (typeof result === "string") {
if (isString(result)) {
let str = result;
if (str.length > 128) {
str = str.substring(0, 128) + "...";

View file

@ -139,7 +139,7 @@ namespace ts {
const value = options[opt.name];
// Value should be a key of opt.type
if (typeof value === "string") {
if (isString(value)) {
// If value is not a string, this will fail
options[opt.name] = parseCustomTypeOption(opt, value, diagnostics);
}

View file

@ -185,6 +185,7 @@ namespace ts {
*/
resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[];
resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
hasInvalidatedResolution?: HasInvalidatedResolution;
directoryExists?(directoryName: string): boolean;
/*
@ -276,7 +277,7 @@ namespace ts {
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed;
getProgram(): Program;
@ -694,23 +695,12 @@ namespace ts {
autoCollapse: boolean;
}
export interface EmitOutput {
outputFiles: OutputFile[];
emitSkipped: boolean;
}
export const enum OutputFileType {
JavaScript,
SourceMap,
Declaration
}
export interface OutputFile {
name: string;
writeByteOrderMark: boolean;
text: string;
}
export const enum EndOfLineState {
None,
InMultiLineCommentTrivia,

View file

@ -979,26 +979,6 @@ namespace ts {
return result;
}
export function compareDataObjects(dst: any, src: any): boolean {
if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) {
return false;
}
for (const e in dst) {
if (typeof dst[e] === "object") {
if (!compareDataObjects(dst[e], src[e])) {
return false;
}
}
else if (typeof dst[e] !== "function") {
if (dst[e] !== src[e]) {
return false;
}
}
}
return true;
}
export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: Node) {
if (node.kind === SyntaxKind.ArrayLiteralExpression ||
node.kind === SyntaxKind.ObjectLiteralExpression) {