diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d6c1f1f977..5b201982d2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1436,10 +1436,10 @@ namespace ts { return resolveExternalModuleSymbol(node.parent.symbol, dontResolveAlias); } - function getTargetOfExportSpecifier(node: ExportSpecifier, dontResolveAlias?: boolean): Symbol { - return (node.parent.parent).moduleSpecifier ? - getExternalModuleMember(node.parent.parent, node, dontResolveAlias) : - resolveEntityName(node.propertyName || node.name, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); + function getTargetOfExportSpecifier(node: ExportSpecifier, meaning: SymbolFlags, dontResolveAlias?: boolean) { + return node.parent.parent.moduleSpecifier ? + getExternalModuleMember(node.parent.parent, node, dontResolveAlias) : + resolveEntityName(node.propertyName || node.name, meaning, /*ignoreErrors*/ false, dontResolveAlias); } function getTargetOfExportAssignment(node: ExportAssignment, dontResolveAlias: boolean): Symbol { @@ -1457,7 +1457,7 @@ namespace ts { case SyntaxKind.ImportSpecifier: return getTargetOfImportSpecifier(node, dontRecursivelyResolve); case SyntaxKind.ExportSpecifier: - return getTargetOfExportSpecifier(node, dontRecursivelyResolve); + return getTargetOfExportSpecifier(node, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, dontRecursivelyResolve); case SyntaxKind.ExportAssignment: return getTargetOfExportAssignment(node, dontRecursivelyResolve); case SyntaxKind.NamespaceExportDeclaration: @@ -3757,10 +3757,7 @@ namespace ts { exportSymbol = resolveName(node.parent, node.text, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, Diagnostics.Cannot_find_name_0, node); } else if (node.parent.kind === SyntaxKind.ExportSpecifier) { - const exportSpecifier = node.parent; - exportSymbol = (exportSpecifier.parent.parent).moduleSpecifier ? - getExternalModuleMember(exportSpecifier.parent.parent, exportSpecifier) : - resolveEntityName(exportSpecifier.propertyName || exportSpecifier.name, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); + exportSymbol = getTargetOfExportSpecifier(node.parent, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); } const result: Node[] = []; if (exportSymbol) { @@ -7293,7 +7290,7 @@ namespace ts { accessExpression && checkThatExpressionIsProperSymbolReference(accessExpression.argumentExpression, indexType, /*reportError*/ false) ? getPropertyNameForKnownSymbolName(((accessExpression.argumentExpression).name).text) : undefined; - if (propName) { + if (propName !== undefined) { const prop = getPropertyOfType(objectType, propName); if (prop) { if (accessExpression) { @@ -9264,25 +9261,39 @@ namespace ts { let result = Ternary.True; const saveErrorInfo = errorInfo; - outer: for (const t of targetSignatures) { - // Only elaborate errors from the first failure - let shouldElaborateErrors = reportErrors; - for (const s of sourceSignatures) { - const related = signatureRelatedTo(s, t, shouldElaborateErrors); - if (related) { - result &= related; - errorInfo = saveErrorInfo; - continue outer; + if (getObjectFlags(source) & ObjectFlags.Instantiated && getObjectFlags(target) & ObjectFlags.Instantiated && source.symbol === target.symbol) { + // We instantiations of the same anonymous type (which typically will be the type of a method). + // Simply do a pairwise comparison of the signatures in the two signature lists instead of the + // much more expensive N * M comparison matrix we explore below. + for (let i = 0; i < targetSignatures.length; i++) { + const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], reportErrors); + if (!related) { + return Ternary.False; } - shouldElaborateErrors = false; + result &= related; } + } + else { + outer: for (const t of targetSignatures) { + // Only elaborate errors from the first failure + let shouldElaborateErrors = reportErrors; + for (const s of sourceSignatures) { + const related = signatureRelatedTo(s, t, shouldElaborateErrors); + if (related) { + result &= related; + errorInfo = saveErrorInfo; + continue outer; + } + shouldElaborateErrors = false; + } - if (shouldElaborateErrors) { - reportError(Diagnostics.Type_0_provides_no_match_for_the_signature_1, - typeToString(source), - signatureToString(t, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, kind)); + if (shouldElaborateErrors) { + reportError(Diagnostics.Type_0_provides_no_match_for_the_signature_1, + typeToString(source), + signatureToString(t, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, kind)); + } + return Ternary.False; } - return Ternary.False; } return result; } @@ -13273,6 +13284,8 @@ namespace ts { let attributesTable = createMap(); let spread: Type = emptyObjectType; let attributesArray: Symbol[] = []; + let hasSpreadAnyType = false; + for (const attributeDecl of attributes.properties) { const member = attributeDecl.symbol; if (isJsxAttribute(attributeDecl)) { @@ -13301,31 +13314,33 @@ namespace ts { const exprType = checkExpression(attributeDecl.expression); if (!isValidSpreadType(exprType)) { error(attributeDecl, Diagnostics.Spread_types_may_only_be_created_from_object_types); - return anyType; + hasSpreadAnyType = true; } if (isTypeAny(exprType)) { - return anyType; + hasSpreadAnyType = true; } spread = getSpreadType(spread, exprType); } } - if (spread !== emptyObjectType) { - if (attributesArray.length > 0) { - spread = getSpreadType(spread, createJsxAttributesType(attributes.symbol, attributesTable)); - attributesArray = []; - attributesTable = createMap(); - } - attributesArray = getPropertiesOfType(spread); - } - - attributesTable = createMap(); - if (attributesArray) { - forEach(attributesArray, (attr) => { - if (!filter || filter(attr)) { - attributesTable.set(attr.name, attr); + if (!hasSpreadAnyType) { + if (spread !== emptyObjectType) { + if (attributesArray.length > 0) { + spread = getSpreadType(spread, createJsxAttributesType(attributes.symbol, attributesTable)); + attributesArray = []; + attributesTable = createMap(); } - }); + attributesArray = getPropertiesOfType(spread); + } + + attributesTable = createMap(); + if (attributesArray) { + forEach(attributesArray, (attr) => { + if (!filter || filter(attr)) { + attributesTable.set(attr.name, attr); + } + }); + } } // Handle children attribute @@ -13349,7 +13364,7 @@ namespace ts { // Error if there is a attribute named "children" and children element. // This is because children element will overwrite the value from attributes const jsxChildrenPropertyName = getJsxElementChildrenPropertyname(); - if (jsxChildrenPropertyName && jsxChildrenPropertyName !== "") { + if (!hasSpreadAnyType && jsxChildrenPropertyName && jsxChildrenPropertyName !== "") { if (attributesTable.has(jsxChildrenPropertyName)) { error(attributes, Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, jsxChildrenPropertyName); } @@ -13363,7 +13378,7 @@ namespace ts { } } - return createJsxAttributesType(attributes.symbol, attributesTable); + return hasSpreadAnyType ? anyType : createJsxAttributesType(attributes.symbol, attributesTable); /** * Create anonymous type from given attributes symbol table. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index c2a646b105..321cc06f76 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -251,6 +251,15 @@ namespace ts { } } + export function zipToMap(keys: string[], values: T[]): Map { + Debug.assert(keys.length === values.length); + const map = createMap(); + for (let i = 0; i < keys.length; ++i) { + map.set(keys[i], values[i]); + } + return map; + } + /** * Iterates through `array` by index and performs the callback on each element of array until the callback * returns a falsey value, then returns false. diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index b205cb85cb..c520290acb 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3211,9 +3211,16 @@ }, "Scoped package detected, looking in '{0}'": { "category": "Message", - "code": "6182" + "code": 6182 + }, + "Reusing resolution of module '{0}' to file '{1}' from old program.": { + "category": "Message", + "code": 6183 + }, + "Reusing module resolutions originating in '{0}' since resolutions are unchanged from old program.": { + "category": "Message", + "code": 6184 }, - "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", "code": 7005 diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index dbaaffdc62..cce95c23c2 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1440,6 +1440,44 @@ namespace ts { : node; } + export function createInterfaceDeclaration(decorators: Decorator[] | undefined, modifiers: Modifier[] | undefined, name: string | Identifier, typeParameters: TypeParameterDeclaration[] | undefined, heritageClauses: HeritageClause[] | undefined, members: TypeElement[]) { + const node = createSynthesizedNode(SyntaxKind.InterfaceDeclaration); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.heritageClauses = asNodeArray(heritageClauses); + node.members = createNodeArray(members); + return node; + } + + export function updateInterfaceDeclaration(node: InterfaceDeclaration, decorators: Decorator[] | undefined, modifiers: Modifier[] | undefined, name: Identifier, typeParameters: TypeParameterDeclaration[] | undefined, heritageClauses: HeritageClause[] | undefined, members: TypeElement[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? updateNode(createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) + : node; + } + + export function createTypeAliasDeclaration(name: string | Identifier, typeParameters: TypeParameterDeclaration[] | undefined, type: TypeNode) { + const node = createSynthesizedNode(SyntaxKind.TypeAliasDeclaration); + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.type = type; + return node; + } + + export function updateTypeAliasDeclaration(node: TypeAliasDeclaration, name: Identifier, typeParameters: TypeParameterDeclaration[] | undefined, type: TypeNode) { + return node.name !== name + || node.typeParameters !== typeParameters + || node.type !== type + ? updateNode(createTypeAliasDeclaration(name, typeParameters, type), node) + : node; + } + export function createEnumDeclaration(decorators: Decorator[] | undefined, modifiers: Modifier[] | undefined, name: string | Identifier, members: EnumMember[]) { const node = createSynthesizedNode(SyntaxKind.EnumDeclaration); node.decorators = asNodeArray(decorators); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 347e6e97eb..7893fee651 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -298,6 +298,7 @@ namespace ts { let diagnosticsProducingTypeChecker: TypeChecker; let noDiagnosticsTypeChecker: TypeChecker; let classifiableNames: Map; + let modifiedFilePaths: Path[] | undefined; const cachedSemanticDiagnosticsForFile: DiagnosticCache = {}; const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; @@ -367,7 +368,8 @@ namespace ts { // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap(fileName => fileName.toLowerCase()) : undefined; - if (!tryReuseStructureFromOldProgram()) { + const structuralIsReused = tryReuseStructureFromOldProgram(); + if (structuralIsReused !== StructureIsReused.Completely) { forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false)); // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders @@ -476,89 +478,114 @@ namespace ts { } interface OldProgramState { - program: Program; + program: Program | undefined; file: SourceFile; + /** The collection of paths modified *since* the old program. */ modifiedFilePaths: Path[]; } - function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile, oldProgramState?: OldProgramState) { - if (!oldProgramState && !file.ambientModuleNames.length) { - // if old program state is not supplied and file does not contain locally defined ambient modules - // then the best we can do is fallback to the default logic + function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile, oldProgramState: OldProgramState) { + if (structuralIsReused === StructureIsReused.Not && !file.ambientModuleNames.length) { + // If the old program state does not permit reusing resolutions and `file` does not contain locally defined ambient modules, + // the best we can do is fallback to the default logic. return resolveModuleNamesWorker(moduleNames, containingFile); } - // at this point we know that either + const oldSourceFile = oldProgramState.program && oldProgramState.program.getSourceFile(containingFile); + if (oldSourceFile !== file && file.resolvedModules) { + // `file` was created for the new program. + // + // We only set `file.resolvedModules` via work from the current function, + // so it is defined iff we already called the current function on `file`. + // That call happened no later than the creation of the `file` object, + // which per above occured during the current program creation. + // Since we assume the filesystem does not change during program creation, + // it is safe to reuse resolutions from the earlier call. + const result: ResolvedModuleFull[] = []; + for (const moduleName of moduleNames) { + const resolvedModule = file.resolvedModules.get(moduleName); + result.push(resolvedModule); + } + return result; + } + // At this point, we know at least one of the following hold: // - file has local declarations for ambient modules - // OR // - old program state is available - // OR - // - both of items above - // With this it is possible that we can tell how some module names from the initial list will be resolved - // without doing actual resolution (in particular if some name was resolved to ambient module). - // Such names should be excluded from the list of module names that will be provided to `resolveModuleNamesWorker` - // since we don't want to resolve them again. + // With this information, we can infer some module resolutions without performing resolution. - // this is a list of modules for which we cannot predict resolution so they should be actually resolved + /** An ordered list of module names for which we cannot recover the resolution. */ let unknownModuleNames: string[]; - // this is a list of combined results assembles from predicted and resolved results. - // Order in this list matches the order in the original list of module names `moduleNames` which is important - // so later we can split results to resolutions of modules and resolutions of module augmentations. + /** + * The indexing of elements in this list matches that of `moduleNames`. + * + * Before combining results, result[i] is in one of the following states: + * * undefined: needs to be recomputed, + * * predictedToResolveToAmbientModuleMarker: known to be an ambient module. + * Needs to be reset to undefined before returning, + * * ResolvedModuleFull instance: can be reused. + */ let result: ResolvedModuleFull[]; - // a transient placeholder that is used to mark predicted resolution in the result list + /** A transient placeholder used to mark predicted resolution in the result list. */ const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = {}; + for (let i = 0; i < moduleNames.length; i++) { const moduleName = moduleNames[i]; - // module name is known to be resolved to ambient module if - // - module name is contained in the list of ambient modules that are locally declared in the file - // - in the old program module name was resolved to ambient module whose declaration is in non-modified file + // If we want to reuse resolutions more aggressively, we can refine this to check for whether the + // text of the corresponding modulenames has changed. + if (file === oldSourceFile) { + const oldResolvedModule = oldSourceFile && oldSourceFile.resolvedModules.get(moduleName); + if (oldResolvedModule) { + if (isTraceEnabled(options, host)) { + trace(host, Diagnostics.Reusing_resolution_of_module_0_to_file_1_from_old_program, moduleName, containingFile); + } + (result || (result = new Array(moduleNames.length)))[i] = oldResolvedModule; + continue; + } + } + // We know moduleName resolves to an ambient module provided that moduleName: + // - is in the list of ambient modules locally declared in the current source file. + // - resolved to an ambient module in the old program whose declaration is in an unmodified file // (so the same module declaration will land in the new program) - let isKnownToResolveToAmbientModule = false; + let resolvesToAmbientModuleInNonModifiedFile = false; if (contains(file.ambientModuleNames, moduleName)) { - isKnownToResolveToAmbientModule = true; + resolvesToAmbientModuleInNonModifiedFile = true; if (isTraceEnabled(options, host)) { trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile); } } else { - isKnownToResolveToAmbientModule = checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName, oldProgramState); + resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState); } - if (isKnownToResolveToAmbientModule) { - if (!unknownModuleNames) { - // found a first module name for which result can be prediced - // this means that this module name should not be passed to `resolveModuleNamesWorker`. - // We'll use a separate list for module names that are definitely unknown. - result = new Array(moduleNames.length); - // copy all module names that appear before the current one in the list - // since they are known to be unknown - unknownModuleNames = moduleNames.slice(0, i); - } - // mark prediced resolution in the result list - result[i] = predictedToResolveToAmbientModuleMarker; + if (resolvesToAmbientModuleInNonModifiedFile) { + (result || (result = new Array(moduleNames.length)))[i] = predictedToResolveToAmbientModuleMarker; } - else if (unknownModuleNames) { - // found unknown module name and we are already using separate list for those - add it to the list - unknownModuleNames.push(moduleName); + else { + // Resolution failed in the old program, or resolved to an ambient module for which we can't reuse the result. + (unknownModuleNames || (unknownModuleNames = [])).push(moduleName); } } - if (!unknownModuleNames) { - // we've looked throught the list but have not seen any predicted resolution - // use default logic - return resolveModuleNamesWorker(moduleNames, containingFile); - } - - const resolutions = unknownModuleNames.length + const resolutions = unknownModuleNames && unknownModuleNames.length ? resolveModuleNamesWorker(unknownModuleNames, containingFile) : emptyArray; - // combine results of resolutions and predicted results + // Combine results of resolutions and predicted results + if (!result) { + // There were no unresolved/ambient resolutions. + Debug.assert(resolutions.length === moduleNames.length); + return resolutions; + } + let j = 0; for (let i = 0; i < result.length; i++) { - if (result[i] === predictedToResolveToAmbientModuleMarker) { - result[i] = undefined; + if (result[i]) { + // `result[i]` is either a `ResolvedModuleFull` or a marker. + // If it is the former, we can leave it as is. + if (result[i] === predictedToResolveToAmbientModuleMarker) { + result[i] = undefined; + } } else { result[i] = resolutions[j]; @@ -566,18 +593,18 @@ namespace ts { } } Debug.assert(j === resolutions.length); + return result; - function checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState?: OldProgramState): boolean { - if (!oldProgramState) { - return false; - } + // If we change our policy of rechecking failed lookups on each program create, + // we should adjust the value returned here. + function moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState: OldProgramState): boolean { const resolutionToFile = getResolvedModule(oldProgramState.file, moduleName); if (resolutionToFile) { // module used to be resolved to file - ignore it return false; } - const ambientModule = oldProgram.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(moduleName); + const ambientModule = oldProgramState.program && oldProgramState.program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(moduleName); if (!(ambientModule && ambientModule.declarations)) { return false; } @@ -599,99 +626,107 @@ namespace ts { } } - function tryReuseStructureFromOldProgram(): boolean { + function tryReuseStructureFromOldProgram(): StructureIsReused { if (!oldProgram) { - return false; + return StructureIsReused.Not; } // check properties that can affect structure of the program or module resolution strategy // if any of these properties has changed - structure cannot be reused const oldOptions = oldProgram.getCompilerOptions(); if (changesAffectModuleResolution(oldOptions, options)) { - return false; + return oldProgram.structureIsReused = StructureIsReused.Not; } - Debug.assert(!oldProgram.structureIsReused); + Debug.assert(!(oldProgram.structureIsReused & (StructureIsReused.Completely | StructureIsReused.SafeModules))); // there is an old program, check if we can reuse its structure const oldRootNames = oldProgram.getRootFileNames(); if (!arrayIsEqualTo(oldRootNames, rootNames)) { - return false; + return oldProgram.structureIsReused = StructureIsReused.Not; } if (!arrayIsEqualTo(options.types, oldOptions.types)) { - return false; + return oldProgram.structureIsReused = StructureIsReused.Not; } // check if program source files has changed in the way that can affect structure of the program const newSourceFiles: SourceFile[] = []; const filePaths: Path[] = []; const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = []; + oldProgram.structureIsReused = StructureIsReused.Completely; for (const oldSourceFile of oldProgram.getSourceFiles()) { - let newSourceFile = host.getSourceFileByPath + const newSourceFile = host.getSourceFileByPath ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target) : host.getSourceFile(oldSourceFile.fileName, options.target); if (!newSourceFile) { - return false; + return oldProgram.structureIsReused = StructureIsReused.Not; } newSourceFile.path = oldSourceFile.path; filePaths.push(newSourceFile.path); if (oldSourceFile !== newSourceFile) { + // The `newSourceFile` object was created for the new program. + if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { // value of no-default-lib has changed // this will affect if default library is injected into the list of files - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } // check tripleslash references if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { // tripleslash references has changed - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } // check imports and module augmentations collectExternalModuleReferences(newSourceFile); if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { // imports has changed - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } if (!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { // moduleAugmentations has changed - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { // 'types' references has changed - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } // tentatively approve the file modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); } - else { - // file has no changes - use it as is - newSourceFile = oldSourceFile; - } // if file has passed all checks it should be safe to reuse it newSourceFiles.push(newSourceFile); } - const modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path); + if (oldProgram.structureIsReused !== StructureIsReused.Completely) { + return oldProgram.structureIsReused; + } + + modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path); // try to verify results of module resolution for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory); if (resolveModuleNamesWorker) { const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile, { file: oldSourceFile, program: oldProgram, modifiedFilePaths }); + const oldProgramState = { program: oldProgram, file: oldSourceFile, modifiedFilePaths }; + const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile, oldProgramState); // ensure that module resolution results are still correct const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo); if (resolutionsChanged) { - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; + newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions); + } + else { + newSourceFile.resolvedModules = oldSourceFile.resolvedModules; } } if (resolveTypeReferenceDirectiveNamesWorker) { @@ -700,12 +735,17 @@ namespace ts { // ensure that types resolutions are still correct const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo); if (resolutionsChanged) { - return false; + oldProgram.structureIsReused = StructureIsReused.SafeModules; + newSourceFile.resolvedTypeReferenceDirectiveNames = zipToMap(typesReferenceDirectives, resolutions); + } + else { + newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; } } - // pass the cache of module/types resolutions from the old source file - newSourceFile.resolvedModules = oldSourceFile.resolvedModules; - newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; + } + + if (oldProgram.structureIsReused !== StructureIsReused.Completely) { + return oldProgram.structureIsReused; } // update fileName -> file mapping @@ -720,9 +760,8 @@ namespace ts { fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile); } resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); - oldProgram.structureIsReused = true; - return true; + return oldProgram.structureIsReused = StructureIsReused.Completely; } function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { @@ -1528,11 +1567,11 @@ namespace ts { function processImportedModules(file: SourceFile) { collectExternalModuleReferences(file); if (file.imports.length || file.moduleAugmentations.length) { - file.resolvedModules = createMap(); // Because global augmentation doesn't have string literal name, we can check for global augmentation as such. const nonGlobalAugmentation = filter(file.moduleAugmentations, (moduleAugmentation) => moduleAugmentation.kind === SyntaxKind.StringLiteral); const moduleNames = map(concatenate(file.imports, nonGlobalAugmentation), getTextOfLiteral); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file); + const oldProgramState = { program: oldProgram, file, modifiedFilePaths }; + const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file, oldProgramState); Debug.assert(resolutions.length === moduleNames.length); for (let i = 0; i < moduleNames.length; i++) { const resolution = resolutions[i]; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a923a8fff7..a9d4027c6f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2408,7 +2408,14 @@ namespace ts { /* @internal */ getResolvedTypeReferenceDirectives(): Map; /* @internal */ isSourceFileFromExternalLibrary(file: SourceFile): boolean; // For testing purposes only. - /* @internal */ structureIsReused?: boolean; + /* @internal */ structureIsReused?: StructureIsReused; + } + + /* @internal */ + export const enum StructureIsReused { + Not = 0, + SafeModules = 1 << 0, + Completely = 1 << 1, } export interface CustomTransformers { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 02acacf9e7..6c1a664e8b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -122,9 +122,8 @@ namespace ts { /* @internal */ export function hasChangesInResolutions(names: string[], newResolutions: T[], oldResolutions: Map, comparer: (oldResolution: T, newResolution: T) => boolean): boolean { - if (names.length !== newResolutions.length) { - return false; - } + Debug.assert(names.length === newResolutions.length); + for (let i = 0; i < names.length; i++) { const newResolution = newResolutions[i]; const oldResolution = oldResolutions && oldResolutions.get(names[i]); diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 0725bf36a3..1b87fc848b 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -201,14 +201,13 @@ namespace ts { assert.isTrue(diags.length === 1, "one diagnostic expected"); assert.isTrue(typeof diags[0].messageText === "string" && ((diags[0].messageText).indexOf("Cannot find module") === 0), "should be 'cannot find module' message"); - // assert that import will success once file appear on disk fileMap.set(imported.name, imported); fileExistsCalledForBar = false; rootScriptInfo.editContent(0, root.content.length, `import {y} from "bar"`); diags = project.getLanguageService().getSemanticDiagnostics(root.name); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - assert.isTrue(diags.length === 0); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + assert.isTrue(diags.length === 0, "The import should succeed once the imported file appears on disk."); }); }); } diff --git a/src/harness/unittests/moduleResolution.ts b/src/harness/unittests/moduleResolution.ts index 14e13b6045..a4220f5032 100644 --- a/src/harness/unittests/moduleResolution.ts +++ b/src/harness/unittests/moduleResolution.ts @@ -1042,7 +1042,7 @@ import b = require("./moduleB"); assert.equal(diagnostics1.length, 1, "expected one diagnostic"); createProgram(names, {}, compilerHost, program1); - assert.isTrue(program1.structureIsReused); + assert.isTrue(program1.structureIsReused === StructureIsReused.Completely); const diagnostics2 = program1.getFileProcessingDiagnostics().getDiagnostics(); assert.equal(diagnostics2.length, 1, "expected one diagnostic"); assert.equal(diagnostics1[0].messageText, diagnostics2[0].messageText, "expected one diagnostic"); diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 43014f25af..2eb3d1f089 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -159,12 +159,14 @@ namespace ts { return program; } - function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void) { - const texts: NamedSourceText[] = (oldProgram).sourceTexts.slice(0); - updater(texts); - const host = createTestCompilerHost(texts, options.target, oldProgram); + function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[]) { + if (!newTexts) { + newTexts = (oldProgram).sourceTexts.slice(0); + } + updater(newTexts); + const host = createTestCompilerHost(newTexts, options.target, oldProgram); const program = createProgram(rootNames, options, host, oldProgram); - program.sourceTexts = texts; + program.sourceTexts = newTexts; program.host = host; return program; } @@ -217,13 +219,15 @@ namespace ts { describe("Reuse program structure", () => { const target = ScriptTarget.Latest; - const files = [ - { name: "a.ts", text: SourceText.New( - ` + const files: NamedSourceText[] = [ + { + name: "a.ts", text: SourceText.New( + ` /// /// /// -`, "", `var x = 1`) }, +`, "", `var x = 1`) + }, { name: "b.ts", text: SourceText.New(`/// `, "", `var y = 2`) }, { name: "c.ts", text: SourceText.New("", "", `var z = 1;`) }, { name: "types/typerefs/index.d.ts", text: SourceText.New("", "", `declare let z: number;`) }, @@ -234,7 +238,7 @@ namespace ts { const program_2 = updateProgram(program_1, ["a.ts"], { target }, files => { files[0].text = files[0].text.updateProgram("var x = 100"); }); - assert.isTrue(program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely); const program1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); const program2Diagnostics = program_2.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); assert.equal(program1Diagnostics.length, program2Diagnostics.length); @@ -245,7 +249,7 @@ namespace ts { const program_2 = updateProgram(program_1, ["a.ts"], { target }, files => { files[0].text = files[0].text.updateProgram("var x = 100"); }); - assert.isTrue(program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely); const program1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); const program2Diagnostics = program_2.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); assert.equal(program1Diagnostics.length, program2Diagnostics.length); @@ -259,19 +263,19 @@ namespace ts { `; files[0].text = files[0].text.updateReferences(newReferences); }); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules); }); it("fails if change affects type references", () => { const program_1 = newProgram(files, ["a.ts"], { types: ["a"] }); updateProgram(program_1, ["a.ts"], { types: ["b"] }, noop); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Not); }); it("succeeds if change doesn't affect type references", () => { const program_1 = newProgram(files, ["a.ts"], { types: ["a"] }); updateProgram(program_1, ["a.ts"], { types: ["a"] }, noop); - assert.isTrue(program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely); }); it("fails if change affects imports", () => { @@ -279,7 +283,7 @@ namespace ts { updateProgram(program_1, ["a.ts"], { target }, files => { files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); }); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules); }); it("fails if change affects type directives", () => { @@ -291,25 +295,25 @@ namespace ts { /// `; files[0].text = files[0].text.updateReferences(newReferences); }); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules); }); it("fails if module kind changes", () => { const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS }); updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.AMD }, noop); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Not); }); it("fails if rootdir changes", () => { const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/b" }); updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/c" }, noop); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Not); }); it("fails if config path changes", () => { const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" }); updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, noop); - assert.isTrue(!program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Not); }); it("resolution cache follows imports", () => { @@ -328,7 +332,7 @@ namespace ts { const program_2 = updateProgram(program_1, ["a.ts"], options, files => { files[0].text = files[0].text.updateProgram("var x = 2"); }); - assert.isTrue(program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely); // content of resolution cache should not change checkResolvedModulesCache(program_1, "a.ts", createMapFromTemplate({ "b": createResolvedModule("b.ts") })); @@ -338,7 +342,7 @@ namespace ts { const program_3 = updateProgram(program_2, ["a.ts"], options, files => { files[0].text = files[0].text.updateImportsAndExports(""); }); - assert.isTrue(!program_2.structureIsReused); + assert.isTrue(program_2.structureIsReused === StructureIsReused.SafeModules); checkResolvedModulesCache(program_3, "a.ts", /*expectedContent*/ undefined); const program_4 = updateProgram(program_3, ["a.ts"], options, files => { @@ -347,7 +351,7 @@ namespace ts { `; files[0].text = files[0].text.updateImportsAndExports(newImports); }); - assert.isTrue(!program_3.structureIsReused); + assert.isTrue(program_3.structureIsReused === StructureIsReused.SafeModules); checkResolvedModulesCache(program_4, "a.ts", createMapFromTemplate({ "b": createResolvedModule("b.ts"), "c": undefined })); }); @@ -365,7 +369,7 @@ namespace ts { const program_2 = updateProgram(program_1, ["/a.ts"], options, files => { files[0].text = files[0].text.updateProgram("var x = 2"); }); - assert.isTrue(program_1.structureIsReused); + assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely); // content of resolution cache should not change checkResolvedTypeDirectivesCache(program_1, "/a.ts", createMapFromTemplate({ "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); @@ -376,7 +380,7 @@ namespace ts { files[0].text = files[0].text.updateReferences(""); }); - assert.isTrue(!program_2.structureIsReused); + assert.isTrue(program_2.structureIsReused === StructureIsReused.SafeModules); checkResolvedTypeDirectivesCache(program_3, "/a.ts", /*expectedContent*/ undefined); updateProgram(program_3, ["/a.ts"], options, files => { @@ -385,10 +389,74 @@ namespace ts { `; files[0].text = files[0].text.updateReferences(newReferences); }); - assert.isTrue(!program_3.structureIsReused); + assert.isTrue(program_3.structureIsReused === StructureIsReused.SafeModules); checkResolvedTypeDirectivesCache(program_1, "/a.ts", createMapFromTemplate({ "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); }); + it("fetches imports after npm install", () => { + const file1Ts = { name: "file1.ts", text: SourceText.New("", `import * as a from "a";`, "const myX: number = a.x;") }; + const file2Ts = { name: "file2.ts", text: SourceText.New("", "", "") }; + const indexDTS = { name: "node_modules/a/index.d.ts", text: SourceText.New("", "export declare let x: number;", "") }; + const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.NodeJs }; + const rootFiles = [file1Ts, file2Ts]; + const filesAfterNpmInstall = [file1Ts, file2Ts, indexDTS]; + + const initialProgram = newProgram(rootFiles, rootFiles.map(f => f.name), options); + { + assert.deepEqual(initialProgram.host.getTrace(), + [ + "======== Resolving module 'a' from 'file1.ts'. ========", + "Explicitly specified module resolution kind: 'NodeJs'.", + "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.", + "File 'node_modules/a.ts' does not exist.", + "File 'node_modules/a.tsx' does not exist.", + "File 'node_modules/a.d.ts' does not exist.", + "File 'node_modules/a/package.json' does not exist.", + "File 'node_modules/a/index.ts' does not exist.", + "File 'node_modules/a/index.tsx' does not exist.", + "File 'node_modules/a/index.d.ts' does not exist.", + "File 'node_modules/@types/a.d.ts' does not exist.", + "File 'node_modules/@types/a/package.json' does not exist.", + "File 'node_modules/@types/a/index.d.ts' does not exist.", + "Loading module 'a' from 'node_modules' folder, target file type 'JavaScript'.", + "File 'node_modules/a.js' does not exist.", + "File 'node_modules/a.jsx' does not exist.", + "File 'node_modules/a/package.json' does not exist.", + "File 'node_modules/a/index.js' does not exist.", + "File 'node_modules/a/index.jsx' does not exist.", + "======== Module name 'a' was not resolved. ========" + ], + "initialProgram: execute module resolution normally."); + + const initialProgramDiagnostics = initialProgram.getSemanticDiagnostics(initialProgram.getSourceFile("file1.ts")); + assert(initialProgramDiagnostics.length === 1, `initialProgram: import should fail.`); + } + + const afterNpmInstallProgram = updateProgram(initialProgram, rootFiles.map(f => f.name), options, f => { + f[1].text = f[1].text.updateReferences(`/// `); + }, filesAfterNpmInstall); + { + assert.deepEqual(afterNpmInstallProgram.host.getTrace(), + [ + "======== Resolving module 'a' from 'file1.ts'. ========", + "Explicitly specified module resolution kind: 'NodeJs'.", + "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.", + "File 'node_modules/a.ts' does not exist.", + "File 'node_modules/a.tsx' does not exist.", + "File 'node_modules/a.d.ts' does not exist.", + "File 'node_modules/a/package.json' does not exist.", + "File 'node_modules/a/index.ts' does not exist.", + "File 'node_modules/a/index.tsx' does not exist.", + "File 'node_modules/a/index.d.ts' exist - use it as a name resolution result.", + "======== Module name 'a' was successfully resolved to 'node_modules/a/index.d.ts'. ========" + ], + "afterNpmInstallProgram: execute module resolution normally."); + + const afterNpmInstallProgramDiagnostics = afterNpmInstallProgram.getSemanticDiagnostics(afterNpmInstallProgram.getSourceFile("file1.ts")); + assert(afterNpmInstallProgramDiagnostics.length === 0, `afterNpmInstallProgram: program is well-formed with import.`); + } + }); + it("can reuse ambient module declarations from non-modified files", () => { const files = [ { name: "/a/b/app.ts", text: SourceText.New("", "import * as fs from 'fs'", "") }, @@ -468,7 +536,206 @@ namespace ts { "File '/fs.jsx' does not exist.", "======== Module name 'fs' was not resolved. ========", ], "should look for 'fs' again since node.d.ts was changed"); + }); + it("can reuse module resolutions from non-modified files", () => { + const files = [ + { name: "a1.ts", text: SourceText.New("", "", "let x = 1;") }, + { name: "a2.ts", text: SourceText.New("", "", "let x = 1;") }, + { name: "b1.ts", text: SourceText.New("", "export class B { x: number; }", "") }, + { name: "b2.ts", text: SourceText.New("", "export class B { x: number; }", "") }, + { name: "node_modules/@types/typerefs1/index.d.ts", text: SourceText.New("", "", "declare let z: string;") }, + { name: "node_modules/@types/typerefs2/index.d.ts", text: SourceText.New("", "", "declare let z: string;") }, + { + name: "f1.ts", + text: + SourceText.New( + `/// ${newLine}/// ${newLine}/// `, + `import { B } from './b1';${newLine}export let BB = B;`, + "declare module './b1' { interface B { y: string; } }") + }, + { + name: "f2.ts", + text: SourceText.New( + `/// ${newLine}/// `, + `import { B } from './b2';${newLine}import { BB } from './f1';`, + "(new BB).x; (new BB).y;") + }, + ]; + + const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.Classic }; + const program_1 = newProgram(files, files.map(f => f.name), options); + let expectedErrors = 0; + { + assert.deepEqual(program_1.host.getTrace(), + [ + "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs1/package.json' does not exist.", + "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========", + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "======== Resolving module './b2' from 'f2.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b2.ts' exist - use it as a name resolution result.", + "======== Module name './b2' was successfully resolved to 'b2.ts'. ========", + "======== Resolving module './f1' from 'f2.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'f1.ts' exist - use it as a name resolution result.", + "======== Module name './f1' was successfully resolved to 'f1.ts'. ========" + ], + "program_1: execute module reoslution normally."); + + const program_1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("f2.ts")); + assert(program_1Diagnostics.length === expectedErrors, `initial program should be well-formed`); + } + const indexOfF1 = 6; + const program_2 = updateProgram(program_1, program_1.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(`/// ${newLine}/// `); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + + { + const program_2Diagnostics = program_2.getSemanticDiagnostics(program_2.getSourceFile("f2.ts")); + assert(program_2Diagnostics.length === expectedErrors, `removing no-default-lib shouldn't affect any types used.`); + + assert.deepEqual(program_2.host.getTrace(), [ + "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs1/package.json' does not exist.", + "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========", + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_2: reuse module resolutions in f2 since it is unchanged"); + } + + const program_3 = updateProgram(program_2, program_2.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(`/// `); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + + { + const program_3Diagnostics = program_3.getSemanticDiagnostics(program_3.getSourceFile("f2.ts")); + assert(program_3Diagnostics.length === expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`); + + assert.deepEqual(program_3.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_3: reuse module resolutions in f2 since it is unchanged"); + } + + + const program_4 = updateProgram(program_3, program_3.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + + { + const program_4Diagnostics = program_4.getSemanticDiagnostics(program_4.getSourceFile("f2.ts")); + assert(program_4Diagnostics.length === expectedErrors, `a1.ts was unused, so diagnostics should be unaffected.`); + + assert.deepEqual(program_4.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_4: reuse module resolutions in f2 since it is unchanged"); + } + + const program_5 = updateProgram(program_4, program_4.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateImportsAndExports(`import { B } from './b1';`); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + + { + const program_5Diagnostics = program_5.getSemanticDiagnostics(program_5.getSourceFile("f2.ts")); + assert(program_5Diagnostics.length === ++expectedErrors, `import of BB in f1 fails. BB is of type any. Add one error`); + + assert.deepEqual(program_5.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========" + ], "program_5: exports do not affect program structure, so f2's resolutions are silently reused."); + } + + const program_6 = updateProgram(program_5, program_5.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateProgram(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + + { + const program_6Diagnostics = program_6.getSemanticDiagnostics(program_6.getSourceFile("f2.ts")); + assert(program_6Diagnostics.length === expectedErrors, `import of BB in f1 fails.`); + + assert.deepEqual(program_6.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_6: reuse module resolutions in f2 since it is unchanged"); + } + + const program_7 = updateProgram(program_6, program_6.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateImportsAndExports(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + + { + const program_7Diagnostics = program_7.getSemanticDiagnostics(program_7.getSourceFile("f2.ts")); + assert(program_7Diagnostics.length === expectedErrors, `removing import is noop with respect to program, so no change in diagnostics.`); + + assert.deepEqual(program_7.host.getTrace(), [ + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_7 should reuse module resolutions in f2 since it is unchanged"); + } }); }); diff --git a/src/harness/unittests/transform.ts b/src/harness/unittests/transform.ts index 71f50ed94a..deee352ebf 100644 --- a/src/harness/unittests/transform.ts +++ b/src/harness/unittests/transform.ts @@ -68,6 +68,9 @@ namespace ts { transformers: { before: [replaceUndefinedWithVoid0], after: [replaceIdentifiersNamedOldNameWithNewName] + }, + compilerOptions: { + newLine: NewLineKind.CarriageReturnLineFeed } }).outputText; }); diff --git a/src/server/project.ts b/src/server/project.ts index 09c8d74c8c..56c394379a 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -552,7 +552,7 @@ namespace ts.server { // bump up the version if // - oldProgram is not set - this is a first time updateGraph is called // - newProgram is different from the old program and structure of the old program was not reused. - if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) { + if (!oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused & StructureIsReused.Completely))) { hasChanges = true; if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 0bc18ce766..aaf7b535ad 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1,5 +1,14 @@ /* @internal */ namespace ts.codefix { + registerCodeFix({ + errorCodes: [ + Diagnostics.Cannot_find_name_0.code, + Diagnostics.Cannot_find_name_0_Did_you_mean_1.code, + Diagnostics.Cannot_find_namespace_0.code, + Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code + ], + getCodeActions: getImportCodeActions + }); type ImportCodeActionKind = "CodeChange" | "InsertingIntoExistingImport" | "NewImport"; interface ImportCodeAction extends CodeAction { @@ -113,466 +122,458 @@ namespace ts.codefix { } } - registerCodeFix({ - errorCodes: [ - Diagnostics.Cannot_find_name_0.code, - Diagnostics.Cannot_find_name_0_Did_you_mean_1.code, - Diagnostics.Cannot_find_namespace_0.code, - Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code - ], - getCodeActions: (context: CodeFixContext) => { - const sourceFile = context.sourceFile; - const checker = context.program.getTypeChecker(); - const allSourceFiles = context.program.getSourceFiles(); - const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false; + function getImportCodeActions(context: CodeFixContext): ImportCodeAction[] { + const sourceFile = context.sourceFile; + const checker = context.program.getTypeChecker(); + const allSourceFiles = context.program.getSourceFiles(); + const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false; - const token = getTokenAtPosition(sourceFile, context.span.start); - const name = token.getText(); - const symbolIdActionMap = new ImportCodeActionMap(); + const token = getTokenAtPosition(sourceFile, context.span.start); + const name = token.getText(); + const symbolIdActionMap = new ImportCodeActionMap(); - // this is a module id -> module import declaration map - const cachedImportDeclarations: (ImportDeclaration | ImportEqualsDeclaration)[][] = []; - let lastImportDeclaration: Node; + // this is a module id -> module import declaration map + const cachedImportDeclarations: (ImportDeclaration | ImportEqualsDeclaration)[][] = []; + let lastImportDeclaration: Node; - const currentTokenMeaning = getMeaningFromLocation(token); - if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) { - const symbol = checker.getAliasedSymbol(checker.getSymbolAtLocation(token)); - return getCodeActionForImport(symbol, /*isDefault*/ false, /*isNamespaceImport*/ true); + const currentTokenMeaning = getMeaningFromLocation(token); + if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) { + const symbol = checker.getAliasedSymbol(checker.getSymbolAtLocation(token)); + return getCodeActionForImport(symbol, /*isDefault*/ false, /*isNamespaceImport*/ true); + } + + const candidateModules = checker.getAmbientModules(); + for (const otherSourceFile of allSourceFiles) { + if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) { + candidateModules.push(otherSourceFile.symbol); } + } - const candidateModules = checker.getAmbientModules(); - for (const otherSourceFile of allSourceFiles) { - if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) { - candidateModules.push(otherSourceFile.symbol); + for (const moduleSymbol of candidateModules) { + context.cancellationToken.throwIfCancellationRequested(); + + // check the default export + const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol); + if (defaultExport) { + const localSymbol = getLocalSymbolForExportDefault(defaultExport); + if (localSymbol && localSymbol.name === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) { + // check if this symbol is already used + const symbolId = getUniqueSymbolId(localSymbol); + symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, /*isDefault*/ true)); } } - for (const moduleSymbol of candidateModules) { - context.cancellationToken.throwIfCancellationRequested(); + // check exports with the same name + const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExports(name, moduleSymbol); + if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { + const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName); + symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol)); + } + } - // check the default export - const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol); - if (defaultExport) { - const localSymbol = getLocalSymbolForExportDefault(defaultExport); - if (localSymbol && localSymbol.name === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) { - // check if this symbol is already used - const symbolId = getUniqueSymbolId(localSymbol); - symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, /*isDefault*/ true)); + return symbolIdActionMap.getAllActions(); + + function getImportDeclarations(moduleSymbol: Symbol) { + const moduleSymbolId = getUniqueSymbolId(moduleSymbol); + + const cached = cachedImportDeclarations[moduleSymbolId]; + if (cached) { + return cached; + } + + const existingDeclarations: (ImportDeclaration | ImportEqualsDeclaration)[] = []; + for (const importModuleSpecifier of sourceFile.imports) { + const importSymbol = checker.getSymbolAtLocation(importModuleSpecifier); + if (importSymbol === moduleSymbol) { + existingDeclarations.push(getImportDeclaration(importModuleSpecifier)); + } + } + cachedImportDeclarations[moduleSymbolId] = existingDeclarations; + return existingDeclarations; + + function getImportDeclaration(moduleSpecifier: LiteralExpression) { + let node: Node = moduleSpecifier; + while (node) { + if (node.kind === SyntaxKind.ImportDeclaration) { + return node; } + if (node.kind === SyntaxKind.ImportEqualsDeclaration) { + return node; + } + node = node.parent; } + return undefined; + } + } - // check exports with the same name - const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExports(name, moduleSymbol); - if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { - const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName); - symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol)); - } + function getUniqueSymbolId(symbol: Symbol) { + if (symbol.flags & SymbolFlags.Alias) { + return getSymbolId(checker.getAliasedSymbol(symbol)); + } + return getSymbolId(symbol); + } + + function checkSymbolHasMeaning(symbol: Symbol, meaning: SemanticMeaning) { + const declarations = symbol.getDeclarations(); + return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false; + } + + function getCodeActionForImport(moduleSymbol: Symbol, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] { + const existingDeclarations = getImportDeclarations(moduleSymbol); + if (existingDeclarations.length > 0) { + // With an existing import statement, there are more than one actions the user can do. + return getCodeActionsForExistingImport(existingDeclarations); + } + else { + return [getCodeActionForNewImport()]; } - return symbolIdActionMap.getAllActions(); + function getCodeActionsForExistingImport(declarations: (ImportDeclaration | ImportEqualsDeclaration)[]): ImportCodeAction[] { + const actions: ImportCodeAction[] = []; - function getImportDeclarations(moduleSymbol: Symbol) { - const moduleSymbolId = getUniqueSymbolId(moduleSymbol); - - const cached = cachedImportDeclarations[moduleSymbolId]; - if (cached) { - return cached; - } - - const existingDeclarations: (ImportDeclaration | ImportEqualsDeclaration)[] = []; - for (const importModuleSpecifier of sourceFile.imports) { - const importSymbol = checker.getSymbolAtLocation(importModuleSpecifier); - if (importSymbol === moduleSymbol) { - existingDeclarations.push(getImportDeclaration(importModuleSpecifier)); - } - } - cachedImportDeclarations[moduleSymbolId] = existingDeclarations; - return existingDeclarations; - - function getImportDeclaration(moduleSpecifier: LiteralExpression) { - let node: Node = moduleSpecifier; - while (node) { - if (node.kind === SyntaxKind.ImportDeclaration) { - return node; + // It is possible that multiple import statements with the same specifier exist in the file. + // e.g. + // + // import * as ns from "foo"; + // import { member1, member2 } from "foo"; + // + // member3/**/ <-- cusor here + // + // in this case we should provie 2 actions: + // 1. change "member3" to "ns.member3" + // 2. add "member3" to the second import statement's import list + // and it is up to the user to decide which one fits best. + let namespaceImportDeclaration: ImportDeclaration | ImportEqualsDeclaration; + let namedImportDeclaration: ImportDeclaration; + let existingModuleSpecifier: string; + for (const declaration of declarations) { + if (declaration.kind === SyntaxKind.ImportDeclaration) { + const namedBindings = declaration.importClause && declaration.importClause.namedBindings; + if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) { + // case: + // import * as ns from "foo" + namespaceImportDeclaration = declaration; } - if (node.kind === SyntaxKind.ImportEqualsDeclaration) { - return node; + else { + // cases: + // import default from "foo" + // import { bar } from "foo" or combination with the first one + // import "foo" + namedImportDeclaration = declaration; } - node = node.parent; + existingModuleSpecifier = declaration.moduleSpecifier.getText(); + } + else { + // case: + // import foo = require("foo") + namespaceImportDeclaration = declaration; + existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration); } - return undefined; } - } - function getUniqueSymbolId(symbol: Symbol) { - if (symbol.flags & SymbolFlags.Alias) { - return getSymbolId(checker.getAliasedSymbol(symbol)); + if (namespaceImportDeclaration) { + actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration)); } - return getSymbolId(symbol); - } - function checkSymbolHasMeaning(symbol: Symbol, meaning: SemanticMeaning) { - const declarations = symbol.getDeclarations(); - return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false; - } - - function getCodeActionForImport(moduleSymbol: Symbol, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] { - const existingDeclarations = getImportDeclarations(moduleSymbol); - if (existingDeclarations.length > 0) { - // With an existing import statement, there are more than one actions the user can do. - return getCodeActionsForExistingImport(existingDeclarations); + if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause && + (namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) { + /** + * If the existing import declaration already has a named import list, just + * insert the identifier into that list. + */ + const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause); + const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText()); + actions.push(createCodeAction( + Diagnostics.Add_0_to_existing_import_declaration_from_1, + [name, moduleSpecifierWithoutQuotes], + fileTextChanges, + "InsertingIntoExistingImport", + moduleSpecifierWithoutQuotes + )); } else { - return [getCodeActionForNewImport()]; + // we need to create a new import statement, but the existing module specifier can be reused. + actions.push(getCodeActionForNewImport(existingModuleSpecifier)); + } + return actions; + + function getModuleSpecifierFromImportEqualsDeclaration(declaration: ImportEqualsDeclaration) { + if (declaration.moduleReference && declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) { + return declaration.moduleReference.expression.getText(); + } + return declaration.moduleReference.getText(); } - function getCodeActionsForExistingImport(declarations: (ImportDeclaration | ImportEqualsDeclaration)[]): ImportCodeAction[] { - const actions: ImportCodeAction[] = []; - - // It is possible that multiple import statements with the same specifier exist in the file. - // e.g. - // - // import * as ns from "foo"; - // import { member1, member2 } from "foo"; - // - // member3/**/ <-- cusor here - // - // in this case we should provie 2 actions: - // 1. change "member3" to "ns.member3" - // 2. add "member3" to the second import statement's import list - // and it is up to the user to decide which one fits best. - let namespaceImportDeclaration: ImportDeclaration | ImportEqualsDeclaration; - let namedImportDeclaration: ImportDeclaration; - let existingModuleSpecifier: string; - for (const declaration of declarations) { - if (declaration.kind === SyntaxKind.ImportDeclaration) { - const namedBindings = declaration.importClause && declaration.importClause.namedBindings; - if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) { - // case: - // import * as ns from "foo" - namespaceImportDeclaration = declaration; - } - else { - // cases: - // import default from "foo" - // import { bar } from "foo" or combination with the first one - // import "foo" - namedImportDeclaration = declaration; - } - existingModuleSpecifier = declaration.moduleSpecifier.getText(); - } - else { - // case: - // import foo = require("foo") - namespaceImportDeclaration = declaration; - existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration); - } + function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] { + const importList = importClause.namedBindings; + const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name)); + // case 1: + // original text: import default from "module" + // change to: import default, { name } from "module" + // case 2: + // original text: import {} from "module" + // change to: import { name } from "module" + if (!importList || importList.elements.length === 0) { + const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier])); + return createChangeTracker().replaceNode(sourceFile, importClause, newImportClause).getChanges(); } - if (namespaceImportDeclaration) { - actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration)); - } - - if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause && - (namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) { - /** - * If the existing import declaration already has a named import list, just - * insert the identifier into that list. - */ - const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause); - const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText()); - actions.push(createCodeAction( - Diagnostics.Add_0_to_existing_import_declaration_from_1, - [name, moduleSpecifierWithoutQuotes], - fileTextChanges, - "InsertingIntoExistingImport", - moduleSpecifierWithoutQuotes - )); - } - else { - // we need to create a new import statement, but the existing module specifier can be reused. - actions.push(getCodeActionForNewImport(existingModuleSpecifier)); - } - return actions; - - function getModuleSpecifierFromImportEqualsDeclaration(declaration: ImportEqualsDeclaration) { - if (declaration.moduleReference && declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) { - return declaration.moduleReference.expression.getText(); - } - return declaration.moduleReference.getText(); - } - - function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] { - const importList = importClause.namedBindings; - const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name)); - // case 1: - // original text: import default from "module" - // change to: import default, { name } from "module" - // case 2: - // original text: import {} from "module" - // change to: import { name } from "module" - if (!importList || importList.elements.length === 0) { - const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier])); - return createChangeTracker().replaceNode(sourceFile, importClause, newImportClause).getChanges(); - } - - /** - * If the import list has one import per line, preserve that. Otherwise, insert on same line as last element - * import { - * foo - * } from "./module"; - */ - return createChangeTracker().insertNodeInListAfter( - sourceFile, - importList.elements[importList.elements.length - 1], - newImportSpecifier).getChanges(); - } - - function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction { - let namespacePrefix: string; - if (declaration.kind === SyntaxKind.ImportDeclaration) { - namespacePrefix = (declaration.importClause.namedBindings).name.getText(); - } - else { - namespacePrefix = declaration.name.getText(); - } - namespacePrefix = stripQuotes(namespacePrefix); - - /** - * Cases: - * import * as ns from "mod" - * import default, * as ns from "mod" - * import ns = require("mod") - * - * Because there is no import list, we alter the reference to include the - * namespace instead of altering the import declaration. For example, "foo" would - * become "ns.foo" - */ - return createCodeAction( - Diagnostics.Change_0_to_1, - [name, `${namespacePrefix}.${name}`], - createChangeTracker().replaceNode(sourceFile, token, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(), - "CodeChange" - ); - } + /** + * If the import list has one import per line, preserve that. Otherwise, insert on same line as last element + * import { + * foo + * } from "./module"; + */ + return createChangeTracker().insertNodeInListAfter( + sourceFile, + importList.elements[importList.elements.length - 1], + newImportSpecifier).getChanges(); } - function getCodeActionForNewImport(moduleSpecifier?: string): ImportCodeAction { - if (!lastImportDeclaration) { - // insert after any existing imports - for (let i = sourceFile.statements.length - 1; i >= 0; i--) { - const statement = sourceFile.statements[i]; - if (statement.kind === SyntaxKind.ImportEqualsDeclaration || statement.kind === SyntaxKind.ImportDeclaration) { - lastImportDeclaration = statement; - break; - } - } - } - - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport()); - const changeTracker = createChangeTracker(); - const importClause = isDefault - ? createImportClause(createIdentifier(name), /*namedBindings*/ undefined) - : isNamespaceImport - ? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(name))) - : createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name))])); - const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifierWithoutQuotes)); - if (!lastImportDeclaration) { - changeTracker.insertNodeAt(sourceFile, sourceFile.getStart(), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` }); + function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction { + let namespacePrefix: string; + if (declaration.kind === SyntaxKind.ImportDeclaration) { + namespacePrefix = (declaration.importClause.namedBindings).name.getText(); } else { - changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: context.newLineCharacter }); + namespacePrefix = declaration.name.getText(); } + namespacePrefix = stripQuotes(namespacePrefix); - // if this file doesn't have any import statements, insert an import statement and then insert a new line - // between the only import statement and user code. Otherwise just insert the statement because chances - // are there are already a new line seperating code and import statements. + /** + * Cases: + * import * as ns from "mod" + * import default, * as ns from "mod" + * import ns = require("mod") + * + * Because there is no import list, we alter the reference to include the + * namespace instead of altering the import declaration. For example, "foo" would + * become "ns.foo" + */ return createCodeAction( - Diagnostics.Import_0_from_1, - [name, `"${moduleSpecifierWithoutQuotes}"`], - changeTracker.getChanges(), - "NewImport", - moduleSpecifierWithoutQuotes + Diagnostics.Change_0_to_1, + [name, `${namespacePrefix}.${name}`], + createChangeTracker().replaceNode(sourceFile, token, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(), + "CodeChange" ); + } + } - function getModuleSpecifierForNewImport() { - const fileName = sourceFile.fileName; - const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName; - const sourceDirectory = getDirectoryPath(fileName); - const options = context.program.getCompilerOptions(); - - return tryGetModuleNameFromAmbientModule() || - tryGetModuleNameFromTypeRoots() || - tryGetModuleNameAsNodeModule() || - tryGetModuleNameFromBaseUrl() || - tryGetModuleNameFromRootDirs() || - removeFileExtension(getRelativePath(moduleFileName, sourceDirectory)); - - function tryGetModuleNameFromAmbientModule(): string { - if (moduleSymbol.valueDeclaration.kind !== SyntaxKind.SourceFile) { - return moduleSymbol.name; - } + function getCodeActionForNewImport(moduleSpecifier?: string): ImportCodeAction { + if (!lastImportDeclaration) { + // insert after any existing imports + for (let i = sourceFile.statements.length - 1; i >= 0; i--) { + const statement = sourceFile.statements[i]; + if (statement.kind === SyntaxKind.ImportEqualsDeclaration || statement.kind === SyntaxKind.ImportDeclaration) { + lastImportDeclaration = statement; + break; } + } + } - function tryGetModuleNameFromBaseUrl() { - if (!options.baseUrl) { - return undefined; - } + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport()); + const changeTracker = createChangeTracker(); + const importClause = isDefault + ? createImportClause(createIdentifier(name), /*namedBindings*/ undefined) + : isNamespaceImport + ? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(name))) + : createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name))])); + const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifierWithoutQuotes)); + if (!lastImportDeclaration) { + changeTracker.insertNodeAt(sourceFile, sourceFile.getStart(), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` }); + } + else { + changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: context.newLineCharacter }); + } - let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl); - if (!relativeName) { - return undefined; - } + // if this file doesn't have any import statements, insert an import statement and then insert a new line + // between the only import statement and user code. Otherwise just insert the statement because chances + // are there are already a new line seperating code and import statements. + return createCodeAction( + Diagnostics.Import_0_from_1, + [name, `"${moduleSpecifierWithoutQuotes}"`], + changeTracker.getChanges(), + "NewImport", + moduleSpecifierWithoutQuotes + ); - const relativeNameWithIndex = removeFileExtension(relativeName); - relativeName = removeExtensionAndIndexPostFix(relativeName); + function getModuleSpecifierForNewImport() { + const fileName = sourceFile.fileName; + const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName; + const sourceDirectory = getDirectoryPath(fileName); + const options = context.program.getCompilerOptions(); - if (options.paths) { - for (const key in options.paths) { - for (const pattern of options.paths[key]) { - const indexOfStar = pattern.indexOf("*"); - if (indexOfStar === 0 && pattern.length === 1) { - continue; - } - else if (indexOfStar !== -1) { - const prefix = pattern.substr(0, indexOfStar); - const suffix = pattern.substr(indexOfStar + 1); - if (relativeName.length >= prefix.length + suffix.length && - startsWith(relativeName, prefix) && - endsWith(relativeName, suffix)) { - const matchedStar = relativeName.substr(prefix.length, relativeName.length - suffix.length); - return key.replace("\*", matchedStar); - } - } - else if (pattern === relativeName || pattern === relativeNameWithIndex) { - return key; - } - } - } - } + return tryGetModuleNameFromAmbientModule() || + tryGetModuleNameFromTypeRoots() || + tryGetModuleNameAsNodeModule() || + tryGetModuleNameFromBaseUrl() || + tryGetModuleNameFromRootDirs() || + removeFileExtension(getRelativePath(moduleFileName, sourceDirectory)); - return relativeName; + function tryGetModuleNameFromAmbientModule(): string { + if (moduleSymbol.valueDeclaration.kind !== SyntaxKind.SourceFile) { + return moduleSymbol.name; } + } - function tryGetModuleNameFromRootDirs() { - if (options.rootDirs) { - const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs); - const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs); - if (normalizedTargetPath !== undefined) { - const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath) : normalizedTargetPath; - return removeFileExtension(relativePath); - } - } + function tryGetModuleNameFromBaseUrl() { + if (!options.baseUrl) { return undefined; } - function tryGetModuleNameFromTypeRoots() { - const typeRoots = getEffectiveTypeRoots(options, context.host); - if (typeRoots) { - const normalizedTypeRoots = map(typeRoots, typeRoot => toPath(typeRoot, /*basePath*/ undefined, getCanonicalFileName)); - for (const typeRoot of normalizedTypeRoots) { - if (startsWith(moduleFileName, typeRoot)) { - const relativeFileName = moduleFileName.substring(typeRoot.length + 1); - return removeExtensionAndIndexPostFix(relativeFileName); - } - } - } + let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl); + if (!relativeName) { + return undefined; } - function tryGetModuleNameAsNodeModule() { - if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { - // nothing to do here - return undefined; - } + const relativeNameWithIndex = removeFileExtension(relativeName); + relativeName = removeExtensionAndIndexPostFix(relativeName); - const indexOfNodeModules = moduleFileName.indexOf("node_modules"); - if (indexOfNodeModules < 0) { - return undefined; - } - - let relativeFileName: string; - if (sourceDirectory.indexOf(moduleFileName.substring(0, indexOfNodeModules - 1)) === 0) { - // if node_modules folder is in this folder or any of its parent folder, no need to keep it. - relativeFileName = moduleFileName.substring(indexOfNodeModules + 13 /* "node_modules\".length */); - } - else { - relativeFileName = getRelativePath(moduleFileName, sourceDirectory); - } - - relativeFileName = removeFileExtension(relativeFileName); - if (endsWith(relativeFileName, "/index")) { - relativeFileName = getDirectoryPath(relativeFileName); - } - else { - try { - const moduleDirectory = getDirectoryPath(moduleFileName); - const packageJsonContent = JSON.parse(context.host.readFile(combinePaths(moduleDirectory, "package.json"))); - if (packageJsonContent) { - const mainFile = packageJsonContent.main || packageJsonContent.typings; - if (mainFile) { - const mainExportFile = toPath(mainFile, moduleDirectory, getCanonicalFileName); - if (removeFileExtension(mainExportFile) === removeFileExtension(moduleFileName)) { - relativeFileName = getDirectoryPath(relativeFileName); - } + if (options.paths) { + for (const key in options.paths) { + for (const pattern of options.paths[key]) { + const indexOfStar = pattern.indexOf("*"); + if (indexOfStar === 0 && pattern.length === 1) { + continue; + } + else if (indexOfStar !== -1) { + const prefix = pattern.substr(0, indexOfStar); + const suffix = pattern.substr(indexOfStar + 1); + if (relativeName.length >= prefix.length + suffix.length && + startsWith(relativeName, prefix) && + endsWith(relativeName, suffix)) { + const matchedStar = relativeName.substr(prefix.length, relativeName.length - suffix.length); + return key.replace("\*", matchedStar); } } + else if (pattern === relativeName || pattern === relativeNameWithIndex) { + return key; + } } - catch (e) { } } - - return relativeFileName; } + + return relativeName; } - function getPathRelativeToRootDirs(path: string, rootDirs: string[]) { - for (const rootDir of rootDirs) { - const relativeName = getRelativePathIfInDirectory(path, rootDir); - if (relativeName !== undefined) { - return relativeName; + function tryGetModuleNameFromRootDirs() { + if (options.rootDirs) { + const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs); + const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs); + if (normalizedTargetPath !== undefined) { + const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath) : normalizedTargetPath; + return removeFileExtension(relativePath); } } return undefined; } - function removeExtensionAndIndexPostFix(fileName: string) { - fileName = removeFileExtension(fileName); - if (endsWith(fileName, "/index")) { - fileName = fileName.substr(0, fileName.length - 6/* "/index".length */); + function tryGetModuleNameFromTypeRoots() { + const typeRoots = getEffectiveTypeRoots(options, context.host); + if (typeRoots) { + const normalizedTypeRoots = map(typeRoots, typeRoot => toPath(typeRoot, /*basePath*/ undefined, getCanonicalFileName)); + for (const typeRoot of normalizedTypeRoots) { + if (startsWith(moduleFileName, typeRoot)) { + const relativeFileName = moduleFileName.substring(typeRoot.length + 1); + return removeExtensionAndIndexPostFix(relativeFileName); + } + } } - return fileName; } - function getRelativePathIfInDirectory(path: string, directoryPath: string) { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return isRootedDiskPath(relativePath) || startsWith(relativePath, "..") ? undefined : relativePath; - } + function tryGetModuleNameAsNodeModule() { + if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { + // nothing to do here + return undefined; + } - function getRelativePath(path: string, directoryPath: string) { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return moduleHasNonRelativeName(relativePath) ? "./" + relativePath : relativePath; + const indexOfNodeModules = moduleFileName.indexOf("node_modules"); + if (indexOfNodeModules < 0) { + return undefined; + } + + let relativeFileName: string; + if (sourceDirectory.indexOf(moduleFileName.substring(0, indexOfNodeModules - 1)) === 0) { + // if node_modules folder is in this folder or any of its parent folder, no need to keep it. + relativeFileName = moduleFileName.substring(indexOfNodeModules + 13 /* "node_modules\".length */); + } + else { + relativeFileName = getRelativePath(moduleFileName, sourceDirectory); + } + + relativeFileName = removeFileExtension(relativeFileName); + if (endsWith(relativeFileName, "/index")) { + relativeFileName = getDirectoryPath(relativeFileName); + } + else { + try { + const moduleDirectory = getDirectoryPath(moduleFileName); + const packageJsonContent = JSON.parse(context.host.readFile(combinePaths(moduleDirectory, "package.json"))); + if (packageJsonContent) { + const mainFile = packageJsonContent.main || packageJsonContent.typings; + if (mainFile) { + const mainExportFile = toPath(mainFile, moduleDirectory, getCanonicalFileName); + if (removeFileExtension(mainExportFile) === removeFileExtension(moduleFileName)) { + relativeFileName = getDirectoryPath(relativeFileName); + } + } + } + } + catch (e) { } + } + + return relativeFileName; } } + function getPathRelativeToRootDirs(path: string, rootDirs: string[]) { + for (const rootDir of rootDirs) { + const relativeName = getRelativePathIfInDirectory(path, rootDir); + if (relativeName !== undefined) { + return relativeName; + } + } + return undefined; + } + + function removeExtensionAndIndexPostFix(fileName: string) { + fileName = removeFileExtension(fileName); + if (endsWith(fileName, "/index")) { + fileName = fileName.substr(0, fileName.length - 6/* "/index".length */); + } + return fileName; + } + + function getRelativePathIfInDirectory(path: string, directoryPath: string) { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return isRootedDiskPath(relativePath) || startsWith(relativePath, "..") ? undefined : relativePath; + } + + function getRelativePath(path: string, directoryPath: string) { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return moduleHasNonRelativeName(relativePath) ? "./" + relativePath : relativePath; + } } - function createChangeTracker() { - return textChanges.ChangeTracker.fromCodeFixContext(context); - } - - function createCodeAction( - description: DiagnosticMessage, - diagnosticArgs: string[], - changes: FileTextChanges[], - kind: ImportCodeActionKind, - moduleSpecifier?: string): ImportCodeAction { - return { - description: formatMessage.apply(undefined, [undefined, description].concat(diagnosticArgs)), - changes, - kind, - moduleSpecifier - }; - } } - }); + + function createChangeTracker() { + return textChanges.ChangeTracker.fromCodeFixContext(context); + } + + function createCodeAction( + description: DiagnosticMessage, + diagnosticArgs: string[], + changes: FileTextChanges[], + kind: ImportCodeActionKind, + moduleSpecifier?: string): ImportCodeAction { + return { + description: formatMessage.apply(undefined, [undefined, description].concat(diagnosticArgs)), + changes, + kind, + moduleSpecifier + }; + } + } } diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index c34b5c6aa9..2c1becb8dd 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -283,7 +283,9 @@ namespace ts.formatting { this.NoSpaceAfterDot = new Rule(RuleDescriptor.create3(SyntaxKind.DotToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsNonJsxSameLineTokenContext), RuleAction.Delete)); // No space before and after indexer - this.NoSpaceBeforeOpenBracket = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.OpenBracketToken), RuleOperation.create2(new RuleOperationContext(Rules.IsNonJsxSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeOpenBracket = new Rule( + RuleDescriptor.create2(Shared.TokenRange.AnyExcept(SyntaxKind.AsyncKeyword), SyntaxKind.OpenBracketToken), + RuleOperation.create2(new RuleOperationContext(Rules.IsNonJsxSameLineTokenContext), RuleAction.Delete)); this.NoSpaceAfterCloseBracket = new Rule(RuleDescriptor.create3(SyntaxKind.CloseBracketToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsNonJsxSameLineTokenContext, Rules.IsNotBeforeBlockInFunctionDeclarationContext), RuleAction.Delete)); // Place a space before open brace in a function declaration diff --git a/src/services/formatting/rulesMap.ts b/src/services/formatting/rulesMap.ts index cd6f2e539d..5b4eacd2c2 100644 --- a/src/services/formatting/rulesMap.ts +++ b/src/services/formatting/rulesMap.ts @@ -41,8 +41,7 @@ namespace ts.formatting { } private FillRule(rule: Rule, rulesBucketConstructionStateList: RulesBucketConstructionState[]): void { - const specificRule = rule.Descriptor.LeftTokenRange !== Shared.TokenRange.Any && - rule.Descriptor.RightTokenRange !== Shared.TokenRange.Any; + const specificRule = rule.Descriptor.LeftTokenRange.isSpecific() && rule.Descriptor.RightTokenRange.isSpecific(); rule.Descriptor.LeftTokenRange.GetTokens().forEach((left) => { rule.Descriptor.RightTokenRange.GetTokens().forEach((right) => { diff --git a/src/services/formatting/tokenRange.ts b/src/services/formatting/tokenRange.ts index 05d7369e77..28f22cec47 100644 --- a/src/services/formatting/tokenRange.ts +++ b/src/services/formatting/tokenRange.ts @@ -6,6 +6,7 @@ namespace ts.formatting { export interface ITokenAccess { GetTokens(): SyntaxKind[]; Contains(token: SyntaxKind): boolean; + isSpecific(): boolean; } export class TokenRangeAccess implements ITokenAccess { @@ -27,6 +28,8 @@ namespace ts.formatting { public Contains(token: SyntaxKind): boolean { return this.tokens.indexOf(token) >= 0; } + + public isSpecific() { return true; } } export class TokenValuesAccess implements ITokenAccess { @@ -43,6 +46,8 @@ namespace ts.formatting { public Contains(token: SyntaxKind): boolean { return this.tokens.indexOf(token) >= 0; } + + public isSpecific() { return true; } } export class TokenSingleValueAccess implements ITokenAccess { @@ -56,15 +61,18 @@ namespace ts.formatting { public Contains(tokenValue: SyntaxKind): boolean { return tokenValue === this.token; } + + public isSpecific() { return true; } + } + + const allTokens: SyntaxKind[] = []; + for (let token = SyntaxKind.FirstToken; token <= SyntaxKind.LastToken; token++) { + allTokens.push(token); } export class TokenAllAccess implements ITokenAccess { public GetTokens(): SyntaxKind[] { - const result: SyntaxKind[] = []; - for (let token = SyntaxKind.FirstToken; token <= SyntaxKind.LastToken; token++) { - result.push(token); - } - return result; + return allTokens; } public Contains(): boolean { @@ -74,6 +82,22 @@ namespace ts.formatting { public toString(): string { return "[allTokens]"; } + + public isSpecific() { return false; } + } + + export class TokenAllExceptAccess implements ITokenAccess { + constructor(readonly except: SyntaxKind) {} + + public GetTokens(): SyntaxKind[] { + return allTokens.filter(t => t !== this.except); + } + + public Contains(token: SyntaxKind): boolean { + return token !== this.except; + } + + public isSpecific() { return false; } } export class TokenRange { @@ -92,8 +116,8 @@ namespace ts.formatting { return new TokenRange(new TokenRangeAccess(f, to, except)); } - static AllTokens(): TokenRange { - return new TokenRange(new TokenAllAccess()); + static AnyExcept(token: SyntaxKind): TokenRange { + return new TokenRange(new TokenAllExceptAccess(token)); } public GetTokens(): SyntaxKind[] { @@ -108,8 +132,12 @@ namespace ts.formatting { return this.tokenAccess.toString(); } - static Any: TokenRange = TokenRange.AllTokens(); - static AnyIncludingMultilineComments = TokenRange.FromTokens(TokenRange.Any.GetTokens().concat([SyntaxKind.MultiLineCommentTrivia])); + public isSpecific() { + return this.tokenAccess.isSpecific(); + } + + static Any: TokenRange = new TokenRange(new TokenAllAccess()); + static AnyIncludingMultilineComments = TokenRange.FromTokens([...allTokens, SyntaxKind.MultiLineCommentTrivia]); static Keywords = TokenRange.FromRange(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); static BinaryOperators = TokenRange.FromRange(SyntaxKind.FirstBinaryOperator, SyntaxKind.LastBinaryOperator); static BinaryKeywordOperators = TokenRange.FromTokens([SyntaxKind.InKeyword, SyntaxKind.InstanceOfKeyword, SyntaxKind.OfKeyword, SyntaxKind.AsKeyword, SyntaxKind.IsKeyword]); diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences1.js b/tests/baselines/reference/correctlyMarkAliasAsReferences1.js new file mode 100644 index 0000000000..7816881f4a --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences1.js @@ -0,0 +1,23 @@ +//// [tests/cases/conformance/jsx/correctlyMarkAliasAsReferences1.tsx] //// + +//// [declaration.d.ts] +declare module "classnames"; + +//// [0.tsx] +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps; // any +let k = ; + + +//// [0.js] +/// +import * as cx from 'classnames'; +import * as React from "react"; +let buttonProps; // any +let k = React.createElement("button", Object.assign({}, buttonProps), + React.createElement("span", { className: cx('class1', { class2: true }) })); diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences1.symbols b/tests/baselines/reference/correctlyMarkAliasAsReferences1.symbols new file mode 100644 index 0000000000..3267a29050 --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences1.symbols @@ -0,0 +1,29 @@ +=== tests/cases/conformance/jsx/0.tsx === +/// +import * as cx from 'classnames'; +>cx : Symbol(cx, Decl(0.tsx, 1, 6)) + +import * as React from "react"; +>React : Symbol(React, Decl(0.tsx, 2, 6)) + +let buttonProps; // any +>buttonProps : Symbol(buttonProps, Decl(0.tsx, 4, 3)) + +let k = ; +>button : Symbol(JSX.IntrinsicElements.button, Decl(react.d.ts, 2385, 43)) + +=== tests/cases/conformance/jsx/declaration.d.ts === +declare module "classnames"; +No type information for this code. +No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences1.types b/tests/baselines/reference/correctlyMarkAliasAsReferences1.types new file mode 100644 index 0000000000..dbcad2fdc3 --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences1.types @@ -0,0 +1,35 @@ +=== tests/cases/conformance/jsx/0.tsx === +/// +import * as cx from 'classnames'; +>cx : any + +import * as React from "react"; +>React : typeof React + +let buttonProps; // any +>buttonProps : any + +let k = : JSX.Element +>button : any +>buttonProps : any + + +> : JSX.Element +>span : any +>className : any +>cx('class1', { class2: true }) : any +>cx : any +>'class1' : "class1" +>{ class2: true } : { class2: boolean; } +>class2 : boolean +>true : true + + ; +>button : any + +=== tests/cases/conformance/jsx/declaration.d.ts === +declare module "classnames"; +No type information for this code. +No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences2.js b/tests/baselines/reference/correctlyMarkAliasAsReferences2.js new file mode 100644 index 0000000000..7af59b017a --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences2.js @@ -0,0 +1,23 @@ +//// [tests/cases/conformance/jsx/correctlyMarkAliasAsReferences2.tsx] //// + +//// [declaration.d.ts] +declare module "classnames"; + +//// [0.tsx] +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps : {[attributeName: string]: ''} +let k = ; + + +//// [0.js] +/// +import * as cx from 'classnames'; +import * as React from "react"; +let buttonProps; +let k = React.createElement("button", Object.assign({}, buttonProps), + React.createElement("span", { className: cx('class1', { class2: true }) })); diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences2.symbols b/tests/baselines/reference/correctlyMarkAliasAsReferences2.symbols new file mode 100644 index 0000000000..76d9b8cdfe --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences2.symbols @@ -0,0 +1,30 @@ +=== tests/cases/conformance/jsx/0.tsx === +/// +import * as cx from 'classnames'; +>cx : Symbol(cx, Decl(0.tsx, 1, 6)) + +import * as React from "react"; +>React : Symbol(React, Decl(0.tsx, 2, 6)) + +let buttonProps : {[attributeName: string]: ''} +>buttonProps : Symbol(buttonProps, Decl(0.tsx, 4, 3)) +>attributeName : Symbol(attributeName, Decl(0.tsx, 4, 20)) + +let k = ; +>button : Symbol(JSX.IntrinsicElements.button, Decl(react.d.ts, 2385, 43)) + +=== tests/cases/conformance/jsx/declaration.d.ts === +declare module "classnames"; +No type information for this code. +No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences2.types b/tests/baselines/reference/correctlyMarkAliasAsReferences2.types new file mode 100644 index 0000000000..cfaba960da --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences2.types @@ -0,0 +1,36 @@ +=== tests/cases/conformance/jsx/0.tsx === +/// +import * as cx from 'classnames'; +>cx : any + +import * as React from "react"; +>React : typeof React + +let buttonProps : {[attributeName: string]: ''} +>buttonProps : { [attributeName: string]: ""; } +>attributeName : string + +let k = : JSX.Element +>button : any +>buttonProps : { [attributeName: string]: ""; } + + +> : JSX.Element +>span : any +>className : any +>cx('class1', { class2: true }) : any +>cx : any +>'class1' : "class1" +>{ class2: true } : { class2: boolean; } +>class2 : boolean +>true : true + + ; +>button : any + +=== tests/cases/conformance/jsx/declaration.d.ts === +declare module "classnames"; +No type information for this code. +No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences3.js b/tests/baselines/reference/correctlyMarkAliasAsReferences3.js new file mode 100644 index 0000000000..166e29d56d --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences3.js @@ -0,0 +1,23 @@ +//// [tests/cases/conformance/jsx/correctlyMarkAliasAsReferences3.tsx] //// + +//// [declaration.d.ts] +declare module "classnames"; + +//// [0.tsx] +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps; +let k = ; + + +//// [0.js] +/// +import * as cx from 'classnames'; +import * as React from "react"; +let buttonProps; +let k = React.createElement("button", Object.assign({}, buttonProps), + React.createElement("span", { className: cx('class1', { class2: true }) })); diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences3.symbols b/tests/baselines/reference/correctlyMarkAliasAsReferences3.symbols new file mode 100644 index 0000000000..088f553baa --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences3.symbols @@ -0,0 +1,29 @@ +=== tests/cases/conformance/jsx/0.tsx === +/// +import * as cx from 'classnames'; +>cx : Symbol(cx, Decl(0.tsx, 1, 6)) + +import * as React from "react"; +>React : Symbol(React, Decl(0.tsx, 2, 6)) + +let buttonProps; +>buttonProps : Symbol(buttonProps, Decl(0.tsx, 4, 3)) + +let k = ; +>button : Symbol(JSX.IntrinsicElements.button, Decl(react.d.ts, 2385, 43)) + +=== tests/cases/conformance/jsx/declaration.d.ts === +declare module "classnames"; +No type information for this code. +No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences3.types b/tests/baselines/reference/correctlyMarkAliasAsReferences3.types new file mode 100644 index 0000000000..cca59ae2f5 --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences3.types @@ -0,0 +1,35 @@ +=== tests/cases/conformance/jsx/0.tsx === +/// +import * as cx from 'classnames'; +>cx : any + +import * as React from "react"; +>React : typeof React + +let buttonProps; +>buttonProps : any + +let k = : JSX.Element +>button : any +>buttonProps : undefined + + +> : JSX.Element +>span : any +>className : any +>cx('class1', { class2: true }) : any +>cx : any +>'class1' : "class1" +>{ class2: true } : { class2: boolean; } +>class2 : boolean +>true : true + + ; +>button : any + +=== tests/cases/conformance/jsx/declaration.d.ts === +declare module "classnames"; +No type information for this code. +No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/correctlyMarkAliasAsReferences4.js b/tests/baselines/reference/correctlyMarkAliasAsReferences4.js new file mode 100644 index 0000000000..631d06cc5b --- /dev/null +++ b/tests/baselines/reference/correctlyMarkAliasAsReferences4.js @@ -0,0 +1,19 @@ +//// [tests/cases/conformance/jsx/correctlyMarkAliasAsReferences4.tsx] //// + +//// [declaration.d.ts] +declare module "classnames"; + +//// [0.tsx] +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps : {[attributeName: string]: ''} +let k = ; diff --git a/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences2.tsx b/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences2.tsx new file mode 100644 index 0000000000..9755c624fc --- /dev/null +++ b/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences2.tsx @@ -0,0 +1,17 @@ +// @target: es2017 +// @jsx: react +// @moduleResolution: node +// @libFiles: react.d.ts,lib.d.ts + +// @filename: declaration.d.ts +declare module "classnames"; + +// @filename: 0.tsx +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps : {[attributeName: string]: ''} +let k = ; diff --git a/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences3.tsx b/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences3.tsx new file mode 100644 index 0000000000..c3bfb0c8a4 --- /dev/null +++ b/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences3.tsx @@ -0,0 +1,18 @@ +// @target: es2017 +// @jsx: react +// @moduleResolution: node +// @noImplicitAny: true +// @libFiles: react.d.ts,lib.d.ts + +// @filename: declaration.d.ts +declare module "classnames"; + +// @filename: 0.tsx +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps; +let k = ; diff --git a/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences4.tsx b/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences4.tsx new file mode 100644 index 0000000000..edb9f75f87 --- /dev/null +++ b/tests/cases/conformance/jsx/correctlyMarkAliasAsReferences4.tsx @@ -0,0 +1,15 @@ +// @target: es2017 +// @jsx: react +// @moduleResolution: node +// @libFiles: react.d.ts,lib.d.ts + +// @filename: declaration.d.ts +declare module "classnames"; + +// @filename: 0.tsx +/// +import * as cx from 'classnames'; +import * as React from "react"; + +let buttonProps : {[attributeName: string]: ''} +let k =