Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reference library directives #7549

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,11 @@
"category": "Error",
"code": 4082
},
"Conflicting library definitions for '{0}' found at '{1}' and '{2}'. Copy the correct file to a local typings folder to resolve this conflict.": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, missing quotes: .. local 'typings' folder ..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibly: Copy the correct file to the local 'typings' folder

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to suggested text

"category": "Message",
"code": 4090
},

"The current host does not support the '{0}' option.": {
"category": "Error",
"code": 5001
Expand Down
9 changes: 8 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5505,6 +5505,7 @@ namespace ts {
function processReferenceComments(sourceFile: SourceFile): void {
const triviaScanner = createScanner(sourceFile.languageVersion, /*skipTrivia*/false, LanguageVariant.Standard, sourceText);
const referencedFiles: FileReference[] = [];
const referencedLibraries: FileReference[] = [];
const amdDependencies: { path: string; name: string }[] = [];
let amdModuleName: string;

Expand All @@ -5531,7 +5532,12 @@ namespace ts {
sourceFile.hasNoDefaultLib = referencePathMatchResult.isNoDefaultLib;
const diagnosticMessage = referencePathMatchResult.diagnosticMessage;
if (fileReference) {
referencedFiles.push(fileReference);
if (referencePathMatchResult.isLibraryReference) {
referencedLibraries.push(fileReference);
}
else {
referencedFiles.push(fileReference);
}
}
if (diagnosticMessage) {
parseDiagnostics.push(createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, diagnosticMessage));
Expand Down Expand Up @@ -5563,6 +5569,7 @@ namespace ts {
}

sourceFile.referencedFiles = referencedFiles;
sourceFile.referencedLibraries = referencedLibraries;
sourceFile.amdDependencies = amdDependencies;
sourceFile.moduleName = amdModuleName;
}
Expand Down
151 changes: 131 additions & 20 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ namespace ts {

const emptyArray: any[] = [];

const defaultLibrarySearchPaths = <Path[]>[
"typings/",
"node_modules/",
"node_modules/@types/",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought we are going to make it @typings.

];

export const version = "1.9.0";

export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean): string {
Expand Down Expand Up @@ -371,7 +377,7 @@ namespace ts {
const traceEnabled = isTraceEnabled(compilerOptions, host);

const failedLookupLocations: string[] = [];
const state = {compilerOptions, host, traceEnabled, skipTsx: false};
const state = { compilerOptions, host, traceEnabled, skipTsx: false };
let resolvedFileName = tryLoadModuleUsingOptionalResolutionSettings(moduleName, containingDirectory, nodeLoadModuleByRelativeName,
failedLookupLocations, supportedExtensions, state);

Expand Down Expand Up @@ -407,7 +413,7 @@ namespace ts {
}

/* @internal */
export function directoryProbablyExists(directoryName: string, host: { directoryExists?: (directoryName: string) => boolean } ): boolean {
export function directoryProbablyExists(directoryName: string, host: { directoryExists?: (directoryName: string) => boolean }): boolean {
// if host does not support 'directoryExists' assume that directory will exist
return !host.directoryExists || host.directoryExists(directoryName);
}
Expand Down Expand Up @@ -554,7 +560,7 @@ namespace ts {


return referencedSourceFile
? { resolvedModule: { resolvedFileName: referencedSourceFile }, failedLookupLocations }
? { resolvedModule: { resolvedFileName: referencedSourceFile }, failedLookupLocations }
: { resolvedModule: undefined, failedLookupLocations };
}

Expand Down Expand Up @@ -649,9 +655,9 @@ namespace ts {

export function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[] {
let diagnostics = program.getOptionsDiagnostics(cancellationToken).concat(
program.getSyntacticDiagnostics(sourceFile, cancellationToken),
program.getGlobalDiagnostics(cancellationToken),
program.getSemanticDiagnostics(sourceFile, cancellationToken));
program.getSyntacticDiagnostics(sourceFile, cancellationToken),
program.getGlobalDiagnostics(cancellationToken),
program.getSemanticDiagnostics(sourceFile, cancellationToken));

if (program.getCompilerOptions().declaration) {
diagnostics = diagnostics.concat(program.getDeclarationDiagnostics(sourceFile, cancellationToken));
Expand Down Expand Up @@ -690,6 +696,14 @@ namespace ts {
let program: Program;
let files: SourceFile[] = [];
let fileProcessingDiagnostics = createDiagnosticCollection();
const currentDirectory = host.getCurrentDirectory();
const resolvedLibraries: Map<ResolvedLibrary> = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so are they case sensitive? is this intentional? if not use fileMap.

let libraryRoot =
(options.rootDir && ts.toPath(options.rootDir, currentDirectory, host.getCanonicalFileName)) ||
(options.configFilePath && getDirectoryPath(getNormalizedAbsolutePath(options.configFilePath, currentDirectory)));
if (libraryRoot === undefined) {
libraryRoot = computeCommonSourceDirectoryOfFilenames(rootNames);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kinda unexpected. so tsc a.ts will result in a different search location from tsc a\a.ts b\b.ts, even if a.ts has a reference to b.ts?
I would say make it an error to try to resolve a library reference, without a root, and ask the user to specify rootDir.

}
const programDiagnostics = createDiagnosticCollection();

let commonSourceDirectory: string;
Expand All @@ -706,7 +720,6 @@ namespace ts {
// Map storing if there is emit blocking diagnostics for given input
const hasEmitBlockingDiagnostics = createFileMap<boolean>(getCanonicalFileName);

const currentDirectory = host.getCurrentDirectory();
const resolveModuleNamesWorker = host.resolveModuleNames
? ((moduleNames: string[], containingFile: string) => host.resolveModuleNames(moduleNames, containingFile))
: ((moduleNames: string[], containingFile: string) => {
Expand Down Expand Up @@ -883,8 +896,8 @@ namespace ts {
const oldResolution = getResolvedModule(oldSourceFile, moduleNames[i]);
const resolutionChanged = oldResolution
? !newResolution ||
oldResolution.resolvedFileName !== newResolution.resolvedFileName ||
!!oldResolution.isExternalLibraryImport !== !!newResolution.isExternalLibraryImport
oldResolution.resolvedFileName !== newResolution.resolvedFileName ||
!!oldResolution.isExternalLibraryImport !== !!newResolution.isExternalLibraryImport
: newResolution;

if (resolutionChanged) {
Expand Down Expand Up @@ -1007,9 +1020,9 @@ namespace ts {
}

function getDiagnosticsHelper(
sourceFile: SourceFile,
getDiagnostics: (sourceFile: SourceFile, cancellationToken: CancellationToken) => Diagnostic[],
cancellationToken: CancellationToken): Diagnostic[] {
sourceFile: SourceFile,
getDiagnostics: (sourceFile: SourceFile, cancellationToken: CancellationToken) => Diagnostic[],
cancellationToken: CancellationToken): Diagnostic[] {
if (sourceFile) {
return getDiagnostics(sourceFile, cancellationToken);
}
Expand Down Expand Up @@ -1484,6 +1497,7 @@ namespace ts {
const basePath = getDirectoryPath(fileName);
if (!options.noResolve) {
processReferencedFiles(file, basePath);
processReferencedLibraries(file, libraryRoot);
}

// always process imported modules to record module name resolutions
Expand All @@ -1507,6 +1521,97 @@ namespace ts {
});
}

function findLibraryDefinition(searchPath: string) {
let typingFilename = "index.d.ts";
const packageJsonPath = combinePaths(searchPath, "package.json");
if (host.fileExists(packageJsonPath)) {
let package: { typings?: string } = {};
try {
package = JSON.parse(host.readFile(packageJsonPath));
}
catch (e) { }

if (package.typings) {
typingFilename = package.typings;
}
}

const combinedPath = normalizePath(combinePaths(searchPath, typingFilename));
return host.fileExists(combinedPath) ? combinedPath : undefined;
}

function processReferencedLibraries(file: SourceFile, compilationRoot: string) {
const primarySearchPaths = map(getEffectiveLibraryPrimarySearchPaths(), path => combinePaths(compilationRoot, path));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see getEffectiveLibraryPrimarySearchPaths used else where, so consider pushing combining paths to it as well, since the path coming from the tsconfig.json are already mapped to the root directory, so it is only the default ones that we need to combine them with the root.


const failedSearchPaths: string[] = [];
const moduleResolutionState: ModuleResolutionState = {
compilerOptions: options,
host: host,
skipTsx: true,
traceEnabled: false
};

for (const ref of file.referencedLibraries) {
// If we already found this library as a primary reference, or failed to find it, nothing to do
const previousResolution = resolvedLibraries[ref.fileName];
if (previousResolution && (previousResolution.primary || (previousResolution.resolvedFileName === undefined))) {
continue;
}

let foundIt = false;

// Check primary library paths
for (const primaryPath of primarySearchPaths) {
const searchPath = combinePaths(primaryPath, ref.fileName);
const resolvedFile = findLibraryDefinition(searchPath);
if (resolvedFile) {
resolvedLibraries[ref.fileName] = { primary: true, resolvedFileName: resolvedFile };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you add it to resolvedLibraries before you call processSourceFile on it, in case it (or something it references) has a library reference for the same library? Seems it could get circular otherwise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't crash due to the checks in processSourceFile (added a test), but good catch. Rearranged.

processSourceFile(resolvedFile, /*isDefaultLib*/ false, /*isReference*/ true, file, ref.pos, ref.end);
foundIt = true;
break;
}
}

// Check secondary library paths
if (!foundIt) {
const secondaryResult = loadModuleFromNodeModules(ref.fileName, file.fileName, failedSearchPaths, moduleResolutionState);
if (secondaryResult) {
foundIt = true;
// If we already resolved to this file, it must have been a secondary reference. Check file contents
// for sameness and possibly issue an error
if (previousResolution) {
const otherFileText = host.readFile(secondaryResult);
if (otherFileText !== getSourceFile(previousResolution.resolvedFileName).text) {
fileProcessingDiagnostics.add(createFileDiagnostic(file, ref.pos, ref.end - ref.pos,
Diagnostics.Conflicting_library_definitions_for_0_found_at_1_and_2_Copy_the_correct_file_to_a_local_typings_folder_to_resolve_this_conflict,
ref.fileName,
secondaryResult,
previousResolution.resolvedFileName));
}
}
else {
// First resolution of this library
resolvedLibraries[ref.fileName] = { primary: false, resolvedFileName: secondaryResult };
processSourceFile(secondaryResult, /*isDefaultLib*/ false, /*isReference*/ true, file, ref.pos, ref.end);
}
}
}

if (!foundIt) {
fileProcessingDiagnostics.add(createFileDiagnostic(file, ref.pos, ref.end - ref.pos, Diagnostics.Cannot_find_name_0, ref.fileName));
// Create an entry as a primary lookup result so we don't keep doing this
resolvedLibraries[ref.fileName] = { primary: true, resolvedFileName: undefined };
}
}
}

function getEffectiveLibraryPrimarySearchPaths(): Path[] {
return <Path[]>(options.librarySearchPaths ||
(options.configFilePath ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... so if the user provides librarySearchPaths, they effectively override all 3 of the defaults?

Also, why is the configFilePath added to the front? Does this mean if I have my tsconfig file in my ./src project root, an it tries to load a jquery typing, it will first look for ./src/jquery/ (and see if there is a package.json with a typing field)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

I removed "" from the list of default search paths.

[options.configFilePath].concat(defaultLibrarySearchPaths) :
defaultLibrarySearchPaths));
}

function getCanonicalFileName(fileName: string): string {
return host.getCanonicalFileName(fileName);
}
Expand Down Expand Up @@ -1553,15 +1658,11 @@ namespace ts {
return;
}

function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string {
function computeCommonSourceDirectoryOfFilenames(fileNames: string[]): string {
let commonPathComponents: string[];
const failed = forEach(files, sourceFile => {
const failed = forEach(fileNames, sourceFile => {
// Each file contributes into common source file path
if (isDeclarationFile(sourceFile)) {
return;
}

const sourcePathComponents = getNormalizedPathComponents(sourceFile.fileName, currentDirectory);
const sourcePathComponents = getNormalizedPathComponents(sourceFile, currentDirectory);
sourcePathComponents.pop(); // The base file name is not part of the common directory path

if (!commonPathComponents) {
Expand Down Expand Up @@ -1601,6 +1702,16 @@ namespace ts {
return getNormalizedPathFromPathComponents(commonPathComponents);
}

function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string {
const fileNames: string[] = [];
for (const file of sourceFiles) {
if (!file.isDeclarationFile) {
fileNames.push(file.fileName);
}
}
return computeCommonSourceDirectoryOfFilenames(fileNames);
}

function checkSourceFilesBelongToPath(sourceFiles: SourceFile[], rootDirectory: string): boolean {
let allFilesBelongToPath = true;
if (sourceFiles) {
Expand Down Expand Up @@ -1742,7 +1853,7 @@ namespace ts {

// If we failed to find a good common directory, but outDir is specified and at least one of our files is on a windows drive/URL/other resource, add a failure
if (options.outDir && dir === "" && forEach(files, file => getRootLength(file.fileName) > 1)) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files));
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files));
}
}

Expand Down
1 change: 1 addition & 0 deletions src/compiler/tsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ namespace ts {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* compilerHost */ undefined);
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
configParseResult.options.configFilePath = configFileName as Path;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not this be set in parseConfigFileTextToJson?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry meant, parseJsonConfigFileContent

return configParseResult;
}

Expand Down
14 changes: 13 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@ namespace ts {
amdDependencies: AmdDependency[];
moduleName: string;
referencedFiles: FileReference[];
referencedLibraries: FileReference[];
languageVariant: LanguageVariant;
isDeclarationFile: boolean;

Expand Down Expand Up @@ -2415,6 +2416,7 @@ namespace ts {
jsx?: JsxEmit;
reactNamespace?: string;
listFiles?: boolean;
librarySearchPaths?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not see parsing to this anywhere. has the changes to commandlineparser been missed intentionally?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

locale?: string;
mapRoot?: string;
module?: ModuleKind;
Expand Down Expand Up @@ -2466,8 +2468,11 @@ namespace ts {
// Do not perform validation of output file name in transpile scenarios
/* @internal */ suppressOutputPathCheck?: boolean;

list?: string[];
/* @internal */
// When options come from a config file, its path is recorded here
configFilePath?: string;

list?: string[];
[option: string]: CompilerOptionsValue;
}

Expand Down Expand Up @@ -2743,6 +2748,13 @@ namespace ts {
isExternalLibraryImport?: boolean;
}

export interface ResolvedLibrary {
// True if the library was found in a primary lookup location
primary: boolean;
// The location of the .d.ts file we located, or undefined if resolution failed
resolvedFileName?: string;
}

export interface ResolvedModuleWithFailedLookupLocations {
resolvedModule: ResolvedModule;
failedLookupLocations: string[];
Expand Down
23 changes: 13 additions & 10 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace ts {
fileReference?: FileReference;
diagnosticMessage?: DiagnosticMessage;
isNoDefaultLib?: boolean;
isLibraryReference?: boolean;
}

export interface SynthesizedNode extends Node {
Expand Down Expand Up @@ -498,6 +499,7 @@ namespace ts {
}

export let fullTripleSlashReferencePathRegEx = /^(\/\/\/\s*<reference\s+path\s*=\s*)('|")(.+?)\2.*?\/>/;
export let fullTripleSlashReferenceLibraryRegEx = /^(\/\/\/\s*<reference\s+library\s*=\s*)('|")(.+?)\2.*?\/>/;
export let fullTripleSlashAMDReferencePathRegEx = /^(\/\/\/\s*<amd-dependency\s+path\s*=\s*)('|")(.+?)\2.*?\/>/;

export function isTypeNode(node: Node): boolean {
Expand Down Expand Up @@ -1538,25 +1540,26 @@ namespace ts {
};
}
else {
const matchResult = fullTripleSlashReferencePathRegEx.exec(comment);
if (matchResult) {
const refMatchResult = fullTripleSlashReferencePathRegEx.exec(comment);
const refLibResult = !refMatchResult && fullTripleSlashReferenceLibraryRegEx.exec(comment);
if (refMatchResult || refLibResult) {
const start = commentRange.pos;
const end = commentRange.end;
return {
fileReference: {
pos: start,
end: end,
fileName: matchResult[3]
fileName: (refMatchResult || refLibResult)[3]
},
isNoDefaultLib: false
};
}
else {
return {
diagnosticMessage: Diagnostics.Invalid_reference_directive_syntax,
isNoDefaultLib: false
isNoDefaultLib: false,
isLibraryReference: !!refLibResult
};
}

return {
diagnosticMessage: Diagnostics.Invalid_reference_directive_syntax,
isNoDefaultLib: false
};
}
}

Expand Down
Loading