Skip to content

Commit 1807147

Browse files
author
Andy Hanson
committed
Allow untyped imports
1 parent 49a6391 commit 1807147

26 files changed

+446
-47
lines changed

src/compiler/checker.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ namespace ts {
10591059
const moduleSymbol = resolveExternalModuleName(node, (<ImportDeclaration>node.parent).moduleSpecifier);
10601060

10611061
if (moduleSymbol) {
1062-
const exportDefaultSymbol = isShorthandAmbientModuleSymbol(moduleSymbol) ?
1062+
const exportDefaultSymbol = isUntypedModuleSymbol(moduleSymbol) ?
10631063
moduleSymbol :
10641064
moduleSymbol.exports["export="] ?
10651065
getPropertyOfType(getTypeOfSymbol(moduleSymbol.exports["export="]), "default") :
@@ -1135,7 +1135,7 @@ namespace ts {
11351135
if (targetSymbol) {
11361136
const name = specifier.propertyName || specifier.name;
11371137
if (name.text) {
1138-
if (isShorthandAmbientModuleSymbol(moduleSymbol)) {
1138+
if (isUntypedModuleSymbol(moduleSymbol)) {
11391139
return moduleSymbol;
11401140
}
11411141

@@ -1355,15 +1355,27 @@ namespace ts {
13551355
}
13561356

13571357
const isRelative = isExternalModuleNameRelative(moduleName);
1358+
let quotedName: string | undefined;
13581359
if (!isRelative) {
1359-
const symbol = getSymbol(globals, '"' + moduleName + '"', SymbolFlags.ValueModule);
1360+
quotedName = '"' + moduleName + '"';
1361+
const symbol = getSymbol(globals, quotedName, SymbolFlags.ValueModule);
13601362
if (symbol) {
13611363
// merged symbol is module declaration symbol combined with all augmentations
13621364
return getMergedSymbol(symbol);
13631365
}
13641366
}
13651367

13661368
const resolvedModule = getResolvedModule(getSourceFileOfNode(location), moduleReference);
1369+
if (resolvedModule && resolvedModule.isUntyped) {
1370+
Debug.assert(!isRelative); // Only global modules may be untyped
1371+
Debug.assert(quotedName !== undefined);
1372+
const newSymbol = createSymbol(SymbolFlags.ValueModule, quotedName);
1373+
// Module symbols are expected to have 'exports', although since this is an untyped module it can be empty.
1374+
newSymbol.exports = createMap<Symbol>();
1375+
globals[quotedName] = newSymbol;
1376+
return newSymbol;
1377+
}
1378+
13671379
const sourceFile = resolvedModule && host.getSourceFile(resolvedModule.resolvedFileName);
13681380
if (sourceFile) {
13691381
if (sourceFile.symbol) {
@@ -3432,7 +3444,7 @@ namespace ts {
34323444
function getTypeOfFuncClassEnumModule(symbol: Symbol): Type {
34333445
const links = getSymbolLinks(symbol);
34343446
if (!links.type) {
3435-
if (symbol.valueDeclaration.kind === SyntaxKind.ModuleDeclaration && isShorthandAmbientModuleSymbol(symbol)) {
3447+
if (symbol.flags & SymbolFlags.Module && isUntypedModuleSymbol(symbol)) {
34363448
links.type = anyType;
34373449
}
34383450
else {
@@ -18707,8 +18719,8 @@ namespace ts {
1870718719

1870818720
function moduleExportsSomeValue(moduleReferenceExpression: Expression): boolean {
1870918721
let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression);
18710-
if (!moduleSymbol || isShorthandAmbientModuleSymbol(moduleSymbol)) {
18711-
// If the module is not found or is shorthand, assume that it may export a value.
18722+
if (!moduleSymbol || isUntypedModuleSymbol(moduleSymbol)) {
18723+
// If the module is not found or is untyped, assume that it may export a value.
1871218724
return true;
1871318725
}
1871418726

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -2861,6 +2861,10 @@
28612861
"category": "Error",
28622862
"code": 6140
28632863
},
2864+
"Resolving real path for '{0}', result is untyped package '{1}'": {
2865+
"category": "Message",
2866+
"code": 6141
2867+
},
28642868
"Variable '{0}' implicitly has an '{1}' type.": {
28652869
"category": "Error",
28662870
"code": 7005

src/compiler/moduleNameResolver.ts

+81-35
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ namespace ts {
1515
}
1616

1717
/* @internal */
18-
export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations {
19-
return { resolvedModule: resolvedFileName ? { resolvedFileName, isExternalLibraryImport } : undefined, failedLookupLocations };
18+
export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport: boolean, isUntyped: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations {
19+
return { resolvedModule: resolvedFileName ? { resolvedFileName, isExternalLibraryImport, isUntyped } : undefined, failedLookupLocations };
2020
}
2121

2222
function moduleHasNonRelativeName(moduleName: string): boolean {
@@ -172,7 +172,7 @@ namespace ts {
172172
for (const typeRoot of primarySearchPaths) {
173173
const candidate = combinePaths(typeRoot, typeReferenceDirectiveName);
174174
const candidateDirectory = getDirectoryPath(candidate);
175-
const resolvedFile = loadNodeModuleFromDirectory(typeReferenceExtensions, candidate, failedLookupLocations,
175+
const resolvedFile = loadNodeModuleFromDirectoryNoPackageJson(typeReferenceExtensions, candidate, failedLookupLocations,
176176
!directoryProbablyExists(candidateDirectory, host), moduleResolutionState);
177177

178178
if (resolvedFile) {
@@ -203,7 +203,7 @@ namespace ts {
203203
if (traceEnabled) {
204204
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
205205
}
206-
resolvedFile = loadModuleFromNodeModules(typeReferenceDirectiveName, initialLocationForSecondaryLookup, failedLookupLocations, moduleResolutionState, /*checkOneLevel*/ false);
206+
resolvedFile = loadTypedModuleFromNodeModules(typeReferenceDirectiveName, initialLocationForSecondaryLookup, failedLookupLocations, moduleResolutionState, /*checkOneLevel*/ false);
207207
if (traceEnabled) {
208208
if (resolvedFile) {
209209
trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, resolvedFile, false);
@@ -523,13 +523,18 @@ namespace ts {
523523
failedLookupLocations, supportedExtensions, state);
524524

525525
let isExternalLibraryImport = false;
526+
let isUntyped = false;
526527
if (!resolvedFileName) {
527528
if (moduleHasNonRelativeName(moduleName)) {
528529
if (traceEnabled) {
529530
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder, moduleName);
530531
}
531-
resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state, /*checkOneLevel*/ false);
532-
isExternalLibraryImport = resolvedFileName !== undefined;
532+
const result = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state, /*checkOneLevel*/ false, /*allowUntyped*/ !compilerOptions.noImplicitAny);
533+
isExternalLibraryImport = result !== undefined;
534+
if (isExternalLibraryImport) {
535+
resolvedFileName = result.path;
536+
isUntyped = result.isUntyped;
537+
}
533538
}
534539
else {
535540
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
@@ -541,11 +546,12 @@ namespace ts {
541546
const originalFileName = resolvedFileName;
542547
resolvedFileName = normalizePath(host.realpath(resolvedFileName));
543548
if (traceEnabled) {
544-
trace(host, Diagnostics.Resolving_real_path_for_0_result_1, originalFileName, resolvedFileName);
549+
const diagnostic = isUntyped ? Diagnostics.Resolving_real_path_for_0_result_is_untyped_package_1 : Diagnostics.Resolving_real_path_for_0_result_1;
550+
trace(host, diagnostic, originalFileName, resolvedFileName);
545551
}
546552
}
547553

548-
return createResolvedModule(resolvedFileName, isExternalLibraryImport, failedLookupLocations);
554+
return createResolvedModule(resolvedFileName, isExternalLibraryImport, isUntyped, failedLookupLocations);
549555
}
550556

551557
function nodeLoadModuleByRelativeName(candidate: string, supportedExtensions: string[], failedLookupLocations: string[],
@@ -557,7 +563,7 @@ namespace ts {
557563

558564
const resolvedFileName = !pathEndsWithDirectorySeparator(candidate) && loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, onlyRecordFailures, state);
559565

560-
return resolvedFileName || loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, onlyRecordFailures, state);
566+
return resolvedFileName || loadNodeModuleFromDirectoryNoPackageJson(supportedExtensions, candidate, failedLookupLocations, onlyRecordFailures, state);
561567
}
562568

563569
/* @internal */
@@ -619,10 +625,15 @@ namespace ts {
619625
}
620626
}
621627

622-
function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState): string {
628+
function loadNodeModuleFromDirectoryNoPackageJson(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState): string {
629+
return discardUntypedResults(loadNodeModuleFromDirectory(extensions, candidate, failedLookupLocation, onlyRecordFailures, state));
630+
}
631+
632+
function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState): ResolutionResult {
623633
const packageJsonPath = pathToPackageJson(candidate);
624634
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
625-
if (directoryExists && state.host.fileExists(packageJsonPath)) {
635+
const packageJsonExists = directoryExists && state.host.fileExists(packageJsonPath);
636+
if (packageJsonExists) {
626637
if (state.traceEnabled) {
627638
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
628639
}
@@ -633,7 +644,7 @@ namespace ts {
633644
const result = tryFile(typesFile, failedLookupLocation, onlyRecordFailures, state) ||
634645
tryAddingExtensions(typesFile, extensions, failedLookupLocation, onlyRecordFailures, state);
635646
if (result) {
636-
return result;
647+
return { isUntyped: false, path: result };
637648
}
638649
}
639650
else {
@@ -650,57 +661,92 @@ namespace ts {
650661
failedLookupLocation.push(packageJsonPath);
651662
}
652663

653-
return loadModuleFromFile(combinePaths(candidate, "index"), extensions, failedLookupLocation, !directoryExists, state);
664+
const file = loadModuleFromFile(combinePaths(candidate, "index"), extensions, failedLookupLocation, !directoryExists, state);
665+
return file
666+
? { isUntyped: false, path: file }
667+
: packageJsonExists
668+
? { isUntyped: true, path: packageJsonPath }
669+
: undefined;
654670
}
655671

656672
function pathToPackageJson(directory: string): string {
657673
return combinePaths(directory, "package.json");
658674
}
659675

660-
function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string {
676+
/** 3 cases: We may have found nothing, found a typed module (this includes `--allowJs`), or only found a package.json (so it's untyped). */
677+
export type ResolutionResult = undefined | { isUntyped: boolean, path: string };
678+
679+
/** Gets only a typed resolution result. */
680+
function discardUntypedResults(result: ResolutionResult): string | undefined {
681+
return result && !result.isUntyped ? result.path : undefined;
682+
}
683+
684+
function loadTypedModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string | undefined {
685+
return discardUntypedResults(loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state));
686+
}
687+
688+
function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): ResolutionResult {
661689
const nodeModulesFolder = combinePaths(directory, "node_modules");
662690
const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host);
663691
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
664692
const supportedExtensions = getSupportedExtensions(state.compilerOptions);
665693

666-
let result = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, !nodeModulesFolderExists, state);
667-
if (result) {
668-
return result;
669-
}
670-
result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
671-
if (result) {
672-
return result;
673-
}
694+
const file = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, !nodeModulesFolderExists, state);
695+
return file
696+
? { isUntyped: false, path: file }
697+
: loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
698+
}
699+
700+
function loadTypedModuleFromNodeModules(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, checkOneLevel: boolean): string | undefined {
701+
return discardUntypedResults(loadModuleFromNodeModules(moduleName, directory, failedLookupLocations, state, checkOneLevel, /*allowUntyped*/ false));
674702
}
675703

676704
/* @internal */
677-
export function loadModuleFromNodeModules(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, checkOneLevel: boolean): string {
678-
return loadModuleFromNodeModulesWorker(moduleName, directory, failedLookupLocations, state, checkOneLevel, /*typesOnly*/ false);
705+
export function loadModuleFromNodeModules(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, checkOneLevel: boolean, allowUntyped: boolean): ResolutionResult {
706+
return loadModuleFromNodeModulesWorker(moduleName, directory, failedLookupLocations, state, checkOneLevel, /*typesOnly*/ false, allowUntyped);
679707
}
680708

681-
function loadModuleFromNodeModulesAtTypes(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string {
682-
return loadModuleFromNodeModulesWorker(moduleName, directory, failedLookupLocations, state, /*checkOneLevel*/ false, /*typesOnly*/ true);
709+
function loadModuleFromNodeModulesAtTypes(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string | undefined {
710+
return discardUntypedResults(loadModuleFromNodeModulesWorker(moduleName, directory, failedLookupLocations, state, /*checkOneLevel*/ false, /*typesOnly*/ true, /*allowUntyped*/ false));
683711
}
684712

685-
function loadModuleFromNodeModulesWorker(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, checkOneLevel: boolean, typesOnly: boolean): string {
713+
function loadModuleFromNodeModulesWorker(
714+
moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, checkOneLevel: boolean, typesOnly: boolean, allowUntyped: boolean): ResolutionResult {
686715
directory = normalizeSlashes(directory);
716+
/**
717+
* Remembers the path to a package.json for the package.
718+
* If we have `/foo/node_modules/bar/package.json` and `/node_modules/@types/bar/index.d.ts` we want to return the latter,
719+
* so we don't immediately return package.json results; we want to keep climbing to look for a typed package.
720+
*/
721+
let packageJson: string | undefined;
687722
while (true) {
688723
const baseName = getBaseFileName(directory);
689724
if (baseName !== "node_modules") {
690-
let packageResult: string | undefined;
725+
let packageResult: ResolutionResult;
691726
if (!typesOnly) {
692727
// Try to load source from the package
693728
packageResult = loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state);
694-
if (packageResult && hasTypeScriptFileExtension(packageResult)) {
729+
if (packageResult && !packageResult.isUntyped && hasTypeScriptFileExtension(packageResult.path)) {
695730
// Always prefer a TypeScript (.ts, .tsx, .d.ts) file shipped with the package
696731
return packageResult;
697732
}
698733
}
699734

700735
// Else prefer a types package over non-TypeScript results (e.g. JavaScript files)
701-
const typesResult = loadModuleFromNodeModulesFolder(combinePaths("@types", moduleName), directory, failedLookupLocations, state);
702-
if (typesResult || packageResult) {
703-
return typesResult || packageResult;
736+
const typesResult = loadTypedModuleFromNodeModulesFolder(combinePaths("@types", moduleName), directory, failedLookupLocations, state);
737+
if (typesResult) {
738+
return { isUntyped: false, path: typesResult };
739+
}
740+
741+
if (packageResult) {
742+
if (packageResult.isUntyped) {
743+
if (allowUntyped) {
744+
packageJson = packageResult.path;
745+
}
746+
}
747+
else {
748+
return packageResult;
749+
}
704750
}
705751
}
706752

@@ -711,7 +757,8 @@ namespace ts {
711757

712758
directory = parentPath;
713759
}
714-
return undefined;
760+
761+
return packageJson && { isUntyped: true, path: packageJson };
715762
}
716763

717764
export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
@@ -723,7 +770,7 @@ namespace ts {
723770

724771
const resolvedFileName = tryLoadModuleUsingOptionalResolutionSettings(moduleName, containingDirectory, loadModuleFromFile, failedLookupLocations, supportedExtensions, state);
725772
if (resolvedFileName) {
726-
return createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/false, failedLookupLocations);
773+
return createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/false, /*isUntyped*/false, failedLookupLocations);
727774
}
728775

729776
let referencedSourceFile: string;
@@ -737,7 +784,6 @@ namespace ts {
737784
referencedSourceFile = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, /*onlyRecordFailures*/ false, state);
738785
}
739786

740-
741787
return referencedSourceFile
742788
? { resolvedModule: { resolvedFileName: referencedSourceFile }, failedLookupLocations }
743789
: { resolvedModule: undefined, failedLookupLocations };

src/compiler/program.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,7 @@ namespace ts {
13261326
}
13271327

13281328
const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModulesJsDepth;
1329-
const shouldAddFile = resolution && !options.noResolve && i < file.imports.length && !elideImport;
1329+
const shouldAddFile = resolution && !options.noResolve && i < file.imports.length && !elideImport && !resolution.isUntyped;
13301330

13311331
if (elideImport) {
13321332
modulesWithElidedImports[file.path] = true;

0 commit comments

Comments
 (0)