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

Imports conflict resolution #9093

Merged
merged 10 commits into from
Mar 4, 2024
Merged

Conversation

vitvakatu
Copy link
Contributor

@vitvakatu vitvakatu commented Feb 19, 2024

Pull Request Description

Closes #5353

When name conflict is detected, we use fully qualified name instead of the usual one.

imports.conflict.resolution.mp4

Important Notes

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • All code has been tested:
    • Unit tests have been written where possible.
    • If GUI codebase was changed, the GUI was tested when built using ./run ide build.

@vitvakatu vitvakatu added x-new-feature Type: new feature request -gui labels Feb 19, 2024
@vitvakatu vitvakatu self-assigned this Feb 19, 2024
app/gui2/src/stores/graph/imports.ts Show resolved Hide resolved
}

/* Substitute `name` inside `expression` with `to`. */
export function substituteQualifiedName(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a major flaw in the current implementation, and I’m not sure how to overcome it.
The failing test case would be:

// …
const expr = Ast.parse('Name', edit)
substituteQualifiedName(edit, expr, 'Name', 'OtherName' as QualifiedName)

This will not change expr, because updateValue requires some parent, but expr has none.

I overcome it by doing conflict resolution later, when we already have assignment ast: variable = expr, and in this case expr always has the parent, so updateValue works.

How it should be done?
cc @kazcw

Copy link
Contributor

Choose a reason for hiding this comment

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

I would separate substituteQualifiedName into two functions:

A QN operation:
getQualifiedNameSubstitution(expression: Ast, name, to): Ast.Owned | undefined

A higher-order recursive-mutation helper:
substituteRecursive(expression: MutableAst, f: (ast: MutableAst) => Owned | undefined)

If your use-case needs it, substituteRecursive could return the new Owned value if it replaces a value that had no parent--but I would expect it is possible to structure things so that that is not needed (i.e. depending on situation use getQualifiedNameSubstitution directly or use it with substituteRecursive), and that would be IMO clearer.

@vitvakatu vitvakatu marked this pull request as ready for review February 19, 2024 11:57
@vitvakatu
Copy link
Contributor Author

The engine error that you see on the recording is caused by the current importing rule, which implies that the item should be exported from Main to be able to use it by a fully qualified name. So Database.Data.Table.Table needs to be exported in Standard.Database.Main. It means that the current conflict resolution will provide invalid code in a lot of cases.

app/gui2/src/components/ComponentBrowser/input.ts Outdated Show resolved Hide resolved
Comment on lines 296 to 301
const importedIdent =
anImport.kind == 'Qualified' ? qnLastSegment(anImport.module) : anImport.import
const noLongerNeeded = !code.value.includes(importedIdent)
if (!noLongerNeeded && !alreadyAdded) {
filtered.push(anImport)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I know that's an older code, but what about imports for extension methods? These imports may be from any module, whose name is not in the input at all.

app/gui2/src/components/ComponentBrowser/input.ts Outdated Show resolved Hide resolved
const noLongerNeeded = !code.value.includes(importedIdent)
if (!noLongerNeeded && !alreadyAdded) {
finalImports.push(anImport)
function importsToAdd(): ImportsForEntry[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite like this API change. Every array element is ImportsForEntry but technically not all will contain all "Imports for entry", as the latter may have imports filtered out.

Do we use anywhere this information (i.e. which import was to what entry) outside? Maybe we could just return flat RequiredImport instead?

@@ -44,7 +44,7 @@ const value = computed({
if (requiresImport) graph.addMissingImports(edit, theImport)
props.onUpdate({ edit })
} else {
graph.addMissingImports(edit, theImport)
graph.addMissingImportsDisregardConflicts(edit, theImport)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we disregard conflicts here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, I’m not sure what was the initial reasoning here.

Comment on lines 313 to 314
if (!entry) return
const conflictingIds = suggestionDb.conflictingNames.lookup(entry.name)
Copy link
Contributor

Choose a reason for hiding this comment

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

It does not look like valid code. If we are about to add Table import for making Table.new node, we should look up conflicts with Table, not new.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You’re correct, it was fixed

Comment on lines 242 to 243
// Keep in mind, that we need to provide the id of `entry`, not `requiredEntry` here!
imports.value.push({ id, imports: requiredImports(suggestionDb, requiredEntry) })
Copy link
Contributor

Choose a reason for hiding this comment

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

Cannot we gather here actually imported entries only? For example, when we add Table.new node: We should check conflicts for Table imports and only Table needs to be replaced with its fully qualified name, not entire chain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, it was indeed an overcomplication of things.

Comment on lines 323 to 326
const usedAs =
entry.memberOf != null
? (`${qnLastSegment(entry.memberOf)}.${entry.name}` as QualifiedName)
: entry.name
Copy link
Contributor

Choose a reason for hiding this comment

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

What if user wrote partially qualified name? For example, Data.Vector.new, and importing Data makes conflict?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@@ -412,7 +412,7 @@ function hideComponentBrowser() {
componentBrowserVisible.value = false
}

function onComponentBrowserCommit(content: string, requiredImports: RequiredImport[]) {
function onComponentBrowserCommit(content: string, requiredImports: ImportsForEntry[]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wait a sec, we do not add imports altogether when editing nodes! An older code too, but this time I guess we may just fix it in a separate issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That’s an oversight, yes. I would add it in the follow-up: #9241

}

/* Substitute `name` inside `expression` with `to`. */
export function substituteQualifiedName(
Copy link
Contributor

Choose a reason for hiding this comment

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

This substitution won't work in cases like Foo.bar (Foo -> Foo.baz) - here we should replace only one Foo!

I thought a bit, but I don't know how to tackle it. Component Browser Input already analyses its content to know what identifiers are from outside, so it could use the information here also. On the other hand, I'm aware we also want to track import conflicts in other cases than inserting/editing node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed, we would need to refactor CB input logic to work with AST directly for this, and even then it can still be complicated.

@vitvakatu vitvakatu requested a review from farmaazon March 1, 2024 14:35
Comment on lines 347 to 349
const suggestionId = component?.suggestionId ?? selectedSuggestionId.value
if (suggestion == null || suggestionId == null) return null
input.applySuggestion(suggestionId)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is here still a need for obtaining suggestion? We could just pass suggestionId and applySuggestion should handle 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.

Done

if (requiredEntry) {
// Keep in mind, that we need to provide the id of `entry`, not `requiredEntry` here!
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment doesn't seem to be relevant anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

const [entryId] = suggestionDb.nameToId.lookup(entryFQN)
if (entryId == null) return
const entry = suggestionDb.get(entryId)!
const conflictingIds = suggestionDb.conflictingNames.lookup(entry.name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically, we could just read the last segment of entryFQN, as name, and don't fetch the entry (only entryId).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, fixed

@farmaazon farmaazon mentioned this pull request Mar 4, 2024
5 tasks
@vitvakatu vitvakatu added CI: Ready to merge This PR is eligible for automatic merge CI: No changelog needed Do not require a changelog entry for this PR. labels Mar 4, 2024
@mergify mergify bot merged commit 3be5e58 into develop Mar 4, 2024
25 of 28 checks passed
@mergify mergify bot deleted the wip/vitvakatu/import-conflict-resolution branch March 4, 2024 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
-gui CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge x-new-feature Type: new feature request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Do not create name clashes when adding new imports
3 participants