add some errors for diagnosing repos with issues

This commit is contained in:
Sheetal Nandi
2025-09-17 12:47:43 -07:00
parent c63de15a99
commit da6576d2ae
4 changed files with 184 additions and 26 deletions
+15
View File
@@ -664,6 +664,21 @@ export function getCommonSourceDirectory(
return commonSourceDirectory;
}
export function getCommonSourceDirectory60(options: CompilerOptions): string | undefined {
if (!options.rootDir && !options.composite && !options.outFile && options.configFilePath) {
// Project compilations never infer their root from the input source paths
let commonSourceDirectory = getDirectoryPath(normalizeSlashes(options.configFilePath));
if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== directorySeparator) {
// Make sure directory path ends with directory separator so this string can directly
// used to replace with "" to get the relative path of the source file and the relative path doesn't
// start with / making it rooted path
commonSourceDirectory += directorySeparator;
}
return commonSourceDirectory;
}
}
/** @internal */
export function getCommonSourceDirectoryOfConfig({ options, fileNames }: ParsedCommandLine, ignoreCase: boolean): string {
return getCommonSourceDirectory(
+56 -18
View File
@@ -42,6 +42,7 @@ import {
getBaseFileName,
GetCanonicalFileName,
getCommonSourceDirectory,
getCommonSourceDirectory60,
getCompilerOptionValue,
getDirectoryPath,
GetEffectiveTypeRootsHost,
@@ -80,6 +81,7 @@ import {
normalizePath,
normalizeSlashes,
PackageId,
packageIdIsEqual,
packageIdToString,
ParsedPatterns,
Path,
@@ -148,6 +150,16 @@ function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | und
}
}
function resolvedIsEqual(a: Resolved | undefined, b: Resolved | undefined) {
return a === b ||
!!a && !!b &&
a.path === b.path &&
a.extension === b.extension &&
packageIdIsEqual(a.packageId, b.packageId) &&
a.originalPath === b.originalPath &&
a.resolvedUsingTsExtension === b.resolvedUsingTsExtension;
}
/** Result of trying to resolve a module. */
interface Resolved {
path: string;
@@ -2932,23 +2944,46 @@ function getLoadModuleFromTargetExportOrImport(extensions: Extensions, state: Mo
packagePath,
));
}
for (const commonSourceDirGuess of commonSourceDirGuesses) {
const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess);
for (const candidateDir of candidateDirectories) {
if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames(state))) {
// The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension
const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator
const possibleInputBase = combinePaths(commonSourceDirGuess, pathFragment);
const jsAndDtsExtensions = [Extension.Mjs, Extension.Cjs, Extension.Js, Extension.Json, Extension.Dmts, Extension.Dcts, Extension.Dts];
for (const ext of jsAndDtsExtensions) {
if (fileExtensionIs(possibleInputBase, ext)) {
const inputExts = getPossibleOriginalInputExtensionForExtension(possibleInputBase);
for (const possibleExt of inputExts) {
if (!extensionIsOk(extensions, possibleExt)) continue;
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state));
if (state.host.fileExists(possibleInputWithInputExtension)) {
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state));
}
const result = guessFromCommonDirs(commonSourceDirGuesses, finalPath);
const commonDir60 = toAbsolutePath(getCommonSourceDirectory60(state.compilerOptions));
if (commonDir60) {
if (!arrayIsEqualTo(commonSourceDirGuesses, [commonDir60])) {
const result60 = guessFromCommonDirs([commonDir60], finalPath);
// Compare and if not same report -- and add made up diagnostics
if (!searchResultIsEqual(result, result60, resolvedIsEqual)) {
state.reportDiagnostic(createCompilerDiagnostic(
isImports
? Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_import_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate
: Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_export_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate,
entry === "" ? "." : entry, // replace empty string with `.` - the reverse of the operation done when entries are built - so main entrypoint errors don't look weird
packagePath + "\nSheetal:: Change in behaviour: guessed " + commonSourceDirGuesses.join(", ") + " will be in 6.0::" + commonDir60 +
"\nResult " + JSON.stringify(result) + "\n Result.6.0: " + JSON.stringify(result60),
));
}
}
}
return result;
}
return undefined;
}
function guessFromCommonDirs(commonSourceDirGuesses: string[], finalPath: string) {
for (const commonSourceDirGuess of commonSourceDirGuesses) {
const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess);
for (const candidateDir of candidateDirectories) {
if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames(state))) {
// The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension
const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator
const possibleInputBase = combinePaths(commonSourceDirGuess, pathFragment);
const jsAndDtsExtensions = [Extension.Mjs, Extension.Cjs, Extension.Js, Extension.Json, Extension.Dmts, Extension.Dcts, Extension.Dts];
for (const ext of jsAndDtsExtensions) {
if (fileExtensionIs(possibleInputBase, ext)) {
const inputExts = getPossibleOriginalInputExtensionForExtension(possibleInputBase);
for (const possibleExt of inputExts) {
if (!extensionIsOk(extensions, possibleExt)) continue;
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state));
if (state.host.fileExists(possibleInputWithInputExtension)) {
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state));
}
}
}
@@ -2956,7 +2991,6 @@ function getLoadModuleFromTargetExportOrImport(extensions: Extensions, state: Mo
}
}
}
return undefined;
function getOutputDirectoriesForBaseDirectory(commonSourceDirGuess: string) {
// Config file ouput paths are processed to be relative to the host's current directory, while
@@ -3416,3 +3450,7 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
state.host.useCaseSensitiveFileNames();
}
function searchResultIsEqual<T>(a: SearchResult<T> | undefined, b: SearchResult<T> | undefined, compareValue: (a: T | undefined, b: T | undefined) => boolean) {
return a === b || !!a && !!b && compareValue(a.value, b.value);
}
+71 -7
View File
@@ -63,6 +63,8 @@ import {
DiagnosticWithLocation,
directorySeparator,
DirectoryStructureHost,
emitDetachedComments,
emitFileNamesIsEqual,
emitFiles,
EmitHost,
emitModuleKindIsNonNodeESM,
@@ -110,6 +112,7 @@ import {
getBaseFileName,
GetCanonicalFileName,
getCommonSourceDirectory as ts_getCommonSourceDirectory,
getCommonSourceDirectory60,
getCommonSourceDirectoryOfConfig,
getDeclarationDiagnostics as ts_getDeclarationDiagnostics,
getDefaultLibFileName,
@@ -131,6 +134,7 @@ import {
getNormalizedAbsolutePathWithoutRoot,
getNormalizedPathComponents,
getOutputDeclarationFileName,
getOutputPathsFor,
getPackageScopeForPath,
getPathFromPathComponents,
getPositionOfLineAndCharacter,
@@ -233,6 +237,7 @@ import {
NodeWithTypeArguments,
noop,
normalizePath,
normalizeSlashes,
notImplementedResolver,
noTransformers,
ObjectLiteralExpression,
@@ -293,6 +298,7 @@ import {
SourceFile,
sourceFileAffectingCompilerOptions,
sourceFileMayBeEmitted,
sourceFileMayBeEmitted60,
startsWith,
Statement,
StringLiteral,
@@ -2149,6 +2155,7 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro
return commonSourceDirectory;
}
const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program));
commonSourceDirectory = ts_getCommonSourceDirectory(
options,
() => mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName),
@@ -2156,6 +2163,24 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro
getCanonicalFileName,
commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory),
);
const commonDir60 = getCommonSourceDirectory60(options);
if (commonDir60) {
const emittedFiles60 = filter(files, file => sourceFileMayBeEmitted60(file, program));
const commonDir2 = getDirectoryPath(normalizeSlashes(options.configFilePath!));
const result = checkSourceFilesBelongToPathWorker(emittedFiles60, commonDir2);
if (!result.allFilesBelongToPath) {
result.filesWithError?.forEach(sourceFile => {
programDiagnostics.addLazyConfigDiagnostic(
sourceFile,
Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files,
sourceFile.fileName,
"!!! Sheetal CommonDir computed: " + commonSourceDirectory + " commonDir in 6.0 : " + commonDir60,
);
});
}
}
programDiagnostics.setCommonSourceDirectory(commonSourceDirectory);
return commonSourceDirectory;
}
@@ -4009,24 +4034,33 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro
}
function checkSourceFilesBelongToPath(sourceFiles: readonly SourceFile[], rootDirectory: string): boolean {
const result = checkSourceFilesBelongToPathWorker(sourceFiles, rootDirectory);
result.filesWithError?.forEach(sourceFile => {
programDiagnostics.addLazyConfigDiagnostic(
sourceFile,
Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files,
sourceFile.fileName,
rootDirectory,
);
});
return result.allFilesBelongToPath;
}
function checkSourceFilesBelongToPathWorker(sourceFiles: readonly SourceFile[], rootDirectory: string) {
let allFilesBelongToPath = true;
let filesWithError: SourceFile[] | undefined;
const absoluteRootDirectoryPath = host.getCanonicalFileName(getNormalizedAbsolutePath(rootDirectory, currentDirectory));
for (const sourceFile of sourceFiles) {
if (!sourceFile.isDeclarationFile) {
const absoluteSourceFilePath = host.getCanonicalFileName(getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory));
if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) {
programDiagnostics.addLazyConfigDiagnostic(
sourceFile,
Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files,
sourceFile.fileName,
rootDirectory,
);
(filesWithError ??= []).push(sourceFile);
allFilesBelongToPath = false;
}
}
}
return allFilesBelongToPath;
return { allFilesBelongToPath, filesWithError };
}
function parseProjectReferenceConfigFile(ref: ProjectReference): ResolvedProjectReference | undefined {
@@ -4403,6 +4437,36 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro
verifyEmitFilePath(emitFileNames.declarationFilePath, emitFilesSeen);
});
}
{
const commonDirWithConfig = getCommonSourceDirectory60(options);
if (commonDirWithConfig) {
const emitHost = getEmitHost();
const emitHostWithConfig = {
...emitHost,
getCommonSourceDirectory: () => commonDirWithConfig,
};
for (const sourceFile of emitHost.getSourceFiles()) {
const canBeEmitted = sourceFileMayBeEmitted(sourceFile, emitHost);
const canBeEmitted60 = sourceFileMayBeEmitted60(sourceFile, emitHostWithConfig);
const outputPaths = canBeEmitted ?
getOutputPathsFor(sourceFile, emitHost, /*forceDtsPaths*/ false) :
undefined;
const outputPaths60 = canBeEmitted60 ?
getOutputPathsFor(sourceFile, emitHostWithConfig, /*forceDtsPaths*/ false) :
undefined;
if (!emitFileNamesIsEqual(outputPaths, outputPaths60)) {
// Report error
programDiagnostics.addConfigDiagnostic(createCompilerDiagnostic(
Diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files,
"!!! Sheetal: Output layout chaned for file: " + sourceFile.fileName +
"\n outputPaths:: " + JSON.stringify(outputPaths) +
"\n Output paths in 6.0: " + JSON.stringify(outputPaths60),
));
}
}
}
}
// Verify that all the emit files are unique and don't overwrite input files
function verifyEmitFilePath(emitFileName: string | undefined, emitFilesSeen: Set<string>) {
+42 -1
View File
@@ -170,6 +170,7 @@ import {
getCombinedModifierFlags,
getCombinedNodeFlags,
getCommonSourceDirectory,
getCommonSourceDirectory60,
getContainerFlags,
getDirectoryPath,
getImpliedNodeFormatForEmitWorker,
@@ -897,7 +898,8 @@ export function createModeMismatchDetails(currentSourceFile: SourceFile): Diagno
return result;
}
function packageIdIsEqual(a: PackageId | undefined, b: PackageId | undefined): boolean {
/** @internal */
export function packageIdIsEqual(a: PackageId | undefined, b: PackageId | undefined): boolean {
return a === b || !!a && !!b && a.name === b.name && a.subModuleName === b.subModuleName && a.version === b.version && a.peerDependencies === b.peerDependencies;
}
@@ -6549,6 +6551,16 @@ export interface EmitFileNames {
buildInfoPath?: string | undefined;
}
/** @internal */
export function emitFileNamesIsEqual(a: EmitFileNames | undefined, b: EmitFileNames | undefined): boolean {
return a === b || !!a && !!b &&
a.jsFilePath === b.jsFilePath &&
a.sourceMapFilePath === b.jsFilePath &&
a.declarationFilePath === b.declarationFilePath &&
a.declarationMapPath === b.declarationMapPath &&
a.buildInfoPath === b.buildInfoPath;
}
/**
* Gets the source files that are expected to have an emit output.
*
@@ -6616,6 +6628,35 @@ export function sourceFileMayBeEmitted(sourceFile: SourceFile, host: SourceFileM
return true;
}
export function sourceFileMayBeEmitted60(sourceFile: SourceFile, host: SourceFileMayBeEmittedHost, forceDtsEmit?: boolean): boolean {
const options = host.getCompilerOptions();
// Js files are emitted only if option is enabled
if (options.noEmitForJsFiles && isSourceFileJS(sourceFile)) return false;
// Declaration files are not emitted
if (sourceFile.isDeclarationFile) return false;
// Source file from node_modules are not emitted
if (host.isSourceFileFromExternalLibrary(sourceFile)) return false;
// forcing dts emit => file needs to be emitted
if (forceDtsEmit) return true;
// Check other conditions for file emit
// Source files from referenced projects are not emitted
if (host.isSourceOfProjectReferenceRedirect(sourceFile.fileName)) return false;
// Any non json file should be emitted
if (!isJsonSourceFile(sourceFile)) return true;
if (host.getRedirectFromSourceFile(sourceFile.fileName)) return false;
// Emit json file if outFile is specified
if (options.outFile) return true;
// Json file is not emitted if outDir is not specified
if (!options.outDir) return false;
// Otherwise if rootDir or composite config file, we know common sourceDir and can check if file would be emitted in same location
if (!options.rootDir && !options.composite && options.configFilePath) {
const commonDir = getNormalizedAbsolutePath(getCommonSourceDirectory60(options)!, host.getCurrentDirectory());
const outputPath = getSourceFilePathInNewDirWorker(sourceFile.fileName, options.outDir, host.getCurrentDirectory(), commonDir, host.getCanonicalFileName);
if (comparePaths(sourceFile.fileName, outputPath, host.getCurrentDirectory(), !host.useCaseSensitiveFileNames()) === Comparison.EqualTo) return false;
}
return true;
}
/** @internal */
export function getSourceFilePathInNewDir(fileName: string, host: EmitHost, newDirPath: string): string {
return getSourceFilePathInNewDirWorker(fileName, newDirPath, host.getCurrentDirectory(), host.getCommonSourceDirectory(), f => host.getCanonicalFileName(f));