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

match leading slash imports with path mappings - fixes #13730 #27980

Merged
merged 1 commit into from
Nov 8, 2018

Conversation

EECOLOR
Copy link
Contributor

@EECOLOR EECOLOR commented Oct 18, 2018

Checklist:

  • There is an associated issue
  • The issue is labeled 'Bug' or 'help wanted' or is in the Community milestone
  • Code is up-to-date with the master branch
  • You've successfully run jake runtests locally
  • You've signed the CLA
  • There are new or updated unit tests validating the change

About the code changes: I can imagine you would like to have the code structured differently or have different traces. I just looked at what currently been implemented and took the most straightforward approach making the changes easy to diff. Feel free to make suggestions.

About the tests: I am missing information to provide the correct amount of tests, or actually I don't know what your policy is. I think the tests I added should cover all of the code I added and prevent future breakage.

About performance: I tried to minimize the performance impact. I am not sure how often this code is called in situations where high performance is required. That said, I noticed a lot of places where performance could be improved, so I am not sure how much you guys want to focus on that.

About caching: I noticed a few places where isExternalModuleNameRelative is used in relation to caching. We might want to add the 'is potentially an alias import' there as well. This however requires some extra thought. An example of such a place:

if (perFolderCache) {
  perFolderCache.set(moduleName, result);
  if (!isExternalModuleNameRelative(moduleName)) {
    // put result in per-module name cache
    cache!.getOrCreateCacheForModuleName(moduleName).set(containingDirectory, result);
  }
}

Fixes #13730

@msftclas
Copy link

msftclas commented Oct 18, 2018

CLA assistant check
All CLA requirements met.

@@ -1433,3 +1444,9 @@ namespace ts {
return value !== undefined ? { value } : undefined;
}
}
/* @internal */
namespace ts {
export function startsWithSlash(name: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this function is not used anywhere else, you could avoid exporting it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great tip. I tried that first, but I had some trouble which seemed to be caused by the /* internal */ comment, it could not resolve CharacterCodes. I just tried to move it again, and now it works. My knowledge of TypeScript is very low, so I will adjust it.

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Oct 19, 2018

Question for the maintainers: do you guys want me to squash the commits once the code has stabilized?

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Oct 20, 2018

@sheetalkamat I have made some changes:

  • Added a test to reflect the intent expressed in the result of the design meeting
  • Changed the names of the tests to better reflect the intent
  • Adjusted the code to more clearly communicate the flow: "if we have paths available and it is eligible for path mapping, try to load using the paths"

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Oct 20, 2018

There is one potential problem left.

When thinking about this problem it is important to remember that the function isExternalModuleNameRelative means isExternalModuleNameRelativeOrRoot or isExternalModuleNameNotANodeModule'. And when you read !isExternalModuleNameRelative(moduleName)you could sayisExternalModuleNameANodeModule`.

Before this change, all !isExternalModuleNameRelative(moduleName) could potentially be mapped using paths. And, the other way around, mapped paths would always be non-root and non-relative.

With this change, all !pathIsRelative(moduleName) and all isRootedDiskPath(moduleName) || isUrl(moduleName) could potentially be mapped using paths (where the last case has the requirement that we have a potential mapping). And, the other way around, mapped paths would always be non-relative.

This is not necessarily a problem. However, in some code bases there is 'invisible' coupling between concepts. The coupling for this case would be the assumption that !isExternalModuleNameRelative(moduleName) is potentially mapped.

As far as I can tell (by tracing back the introduction of path mapping and the introduction of root path's as 'relative') any coupling that exist would be accidental. I can also imagine that if a developer used !isExternalModuleNameRelative(moduleName) to mean a node module he/she did not think about the possibility of it being a mapped path and thus potentially not a node module.

Looking at the uses of isExternalModuleNameRelative my domain knowledge is lacking to determine the following two things:

  • Did it contain a wrong assumption about the meaning of isExternalModuleNameRelative (in combination with paths) before this change?
  • Does this change impact correct assumptions about the meaning of isExternalModuleNameRelative (in combination with paths)?

My advise would be to create a separate issue to track down the uses of isExternalModuleNameRelative and:

  • investigate if it has any relation to node modules or rooted paths
  • determine if the fact that those paths might actually not be a node module or rooted path (once resolved) has any impact

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Oct 20, 2018

I have squashed the commits.

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Oct 25, 2018

@sheetalkamat What's next?

if (!baseUrl) {
return undefined;
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName);
}
if (paths) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be pulled out into separate function instead of inlining. Its easier to read.
We also need trace above this line about using baseUrl in that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this should be pulled out into separate function instead of inlining.

I thought about that. I however couldn't think of a name. The name tryLoadModuleUsingPaths is already taken. What do you suggest?

We also need trace above this line about using baseUrl in that.

The trace baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1 is not exactly correct for what is happening. And since we have the trace paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0 I thought that would be enough. But if you really want it there I could of course add it back in.

Copy link
Member

Choose a reason for hiding this comment

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

Yes please add that back in there since its used in there anyways.

function isEligibleForPathMapping(moduleName: string, paths: MapLike<string[]>): boolean {
if (isRootedDiskPath(moduleName) || isUrl(moduleName)) {
// check for potential alias otherwise { '*': './src/*' } will match
const root = moduleName.slice(0, getRootLength(moduleName));
Copy link
Member

Choose a reason for hiding this comment

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

You don't need all these checks. The path mapping needs to be done if: moduleName is not relative path or it is absolute path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did you read the comment?

It would break backwards compatibility and existing configurations.

Copy link
Member

Choose a reason for hiding this comment

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

@DanielRosenwasser isn't this change expected breaking change ?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I thought that was the point of this PR in the first place. What exactly are the extra checks meant to protect against?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Root (or url) imports failing (resolving to another file) in existing projects failing because they have a * mapping.

Copy link
Member

Choose a reason for hiding this comment

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

As mentioned this is desired to be a breaking change. Please remove these extra checks

const resolved = tryLoadModuleUsingPaths(extensions, moduleName, baseUrl, paths, loader, /*onlyRecordFailures*/ false, state);
if (resolved) return resolved.value;
}

if (!isExternalModuleNameRelative(moduleName)) {
Copy link
Member

Choose a reason for hiding this comment

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

Also needs some tests to ensure that caching is working as intended (and hasn't changed)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you point me in the right direction? I am unsure what would be needed.

Copy link
Member

Choose a reason for hiding this comment

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

There are tests in src/testRunner/moduleResolution.ts that verify caching is working as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sheetalkamat Can you please explain how I can run the unit tests? Thank you

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sheetalkamat I figured it out, they are run by default

@sheetalkamat
Copy link
Member

Also need changes in getStringLiteralCompletionsFromModuleNamesWorker and corresponding tests with module name completions..

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Oct 29, 2018

Also need changes in getStringLiteralCompletionsFromModuleNamesWorker and corresponding tests with module name completions.

What kind of inputs would this fail on?

@sheetalkamat
Copy link
Member

More than failing of completions, we need to make sure the path completions are correct when there is path mapping.

@EECOLOR EECOLOR force-pushed the leading-slash-imports branch 2 times, most recently from a361e89 to 340bb87 Compare November 1, 2018 22:45
@EECOLOR
Copy link
Contributor Author

EECOLOR commented Nov 1, 2018

@sheetalkamat I have updated the code to reflect your comments (apart from the ones that still have questions). I also squashed my commits again and rebased on master. Could you please take another look?

if (isRootedDiskPath(moduleName) || isUrl(moduleName)) {
// check for potential alias otherwise { '*': './src/*' } will match
const root = moduleName.slice(0, getRootLength(moduleName));
const potentialAlias = getOwnKeys(paths).find(path => path.startsWith(root));
Copy link
Member

Choose a reason for hiding this comment

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

find and startsWith are ES2015 and we support ES5 environments, so use the helper functions from core.ts

// check for potential alias otherwise { '*': './src/*' } will match
const root = moduleName.slice(0, getRootLength(moduleName));
const potentialAlias = getOwnKeys(paths).find(path => path.startsWith(root));
return Boolean(potentialAlias);
Copy link
Member

Choose a reason for hiding this comment

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

!!

if (compilerOptions.rootDirs) {
return getCompletionEntriesForDirectoryFragmentWithRootDirs(
compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath);
if (isRootedDiskPath(literalValue) || isUrl(literalValue)) {
Copy link
Member

Choose a reason for hiding this comment

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

@andy-ms can you please review completions change

@sheetalkamat sheetalkamat requested a review from a user November 7, 2018 03:01
compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath);
if (isRootedDiskPath(literalValue) || isUrl(literalValue)) {
if (compilerOptions.baseUrl) {
return getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker);
Copy link

Choose a reason for hiding this comment

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

The two getCompletionEntriesForNonRelativeModules calls are identical, as are the two getCompletionEntriesForRelativeModules calls. This could be a single conditional instead of several nested ones.

return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (isRootedDiskPath(literalValue) || isUrl(literalValue))
    ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath)
    : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker);

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Nov 7, 2018

@sheetalkamat I have made the required changes.

Copy link
Member

@sheetalkamat sheetalkamat left a comment

Choose a reason for hiding this comment

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

What is the change in tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter ?? Please revert it. Thanks

@ghost
Copy link

ghost commented Nov 8, 2018

@sheetalkamat Those are really difficult to revert -- it happens when the submodule is updated by git. I think it's fine because we regularly update these anyway.

@sheetalkamat sheetalkamat merged commit b534fb4 into microsoft:master Nov 8, 2018
@sheetalkamat
Copy link
Member

@EECOLOR thank you for contribution. This should be available in tonight's nightly build.

@EECOLOR
Copy link
Contributor Author

EECOLOR commented Nov 8, 2018

@sheetalkamat thank you and your team for your help

@EECOLOR EECOLOR deleted the leading-slash-imports branch November 8, 2018 19:42
@EECOLOR
Copy link
Contributor Author

EECOLOR commented Nov 9, 2018

@sheetalkamat did something go wrong with the nightly build?

@sheetalkamat
Copy link
Member

@weswigham @DanielRosenwasser would know.

@weswigham
Copy link
Member

Looks like for some reason the nightly publish job didn't get scheduled until I just went to go check on the task. Likely an azure devops issue - since I believe it just triggered, we'll probably publish a nightly in about 30 minutes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants