-
Notifications
You must be signed in to change notification settings - Fork 341
Code action to add missing import declarations #2452
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
base: main
Are you sure you want to change the base?
Code action to add missing import declarations #2452
Conversation
ahoppen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice. Left some thoughts inline.
Sources/SwiftLanguageService/CodeActions/AddMissingImports.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftLanguageService/CodeActions/AddMissingImports.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftLanguageService/CodeActions/AddMissingImports.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftLanguageService/CodeActions/AddMissingImports.swift
Outdated
Show resolved
Hide resolved
…ic matching - Calculate proper import insertion position (after existing imports or file headers) - Filter out current module to avoid self-imports - Check diagnostic code first, fall back to string matching for robustness - Add helper functions: extractModuleName, importInsertionPosition, extractTypeName - Add Diagnostic.codeString extension for code extraction
Signed-off-by: Karan <[email protected]>
Signed-off-by: Karan <[email protected]>
582c014 to
135db1d
Compare
… build/indexing part is done Signed-off-by: Karan <[email protected]>
6a8765a to
1657d49
Compare
|
ping @ahoppen in case this got lost |
ahoppen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the ping, @PhantomInTheWire. I indeed missed this PR.
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| internal import BuildServerIntegration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove the internal access modifiers in this file.
| guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue] else { | ||
| return nil | ||
| } | ||
| guard let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict) else { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue] else { | |
| return nil | |
| } | |
| guard let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict) else { | |
| return nil | |
| } | |
| guard let textDocument = TextDocumentIdentifier(fromLSPAny: dictionary[CodingKeys.textDocument.stringValue]) else { | |
| return nil | |
| } |
|
|
||
| // If no educational note available, fall back to diagnostic ID (e.g., "cannot_find_in_scope") | ||
| if code == nil, let diagnosticID { | ||
| code = .string(diagnosticID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LSP documentation for code says
The diagnostic's code, which might appear in the user interface.
I don’t want to show the diagnostic IDs in the user interface, so I think we should store the diagnostic ID in the data dictionary.
| let relevantDiagnostics = request.context.diagnostics.filter(AddMissingImportsHelper.isMissingTypeOrValueDiagnostic) | ||
| guard !relevantDiagnostics.isEmpty else { return [] } | ||
|
|
||
| guard let buildSettings = await self.compileCommand(for: request.textDocument.uri, fallbackAfterTimeout: true), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only fall back to fallback build settings after timeout if we listen for updated build settings to refresh results (eg. for diagnostics). This isn’t the case here.
| guard let buildSettings = await self.compileCommand(for: request.textDocument.uri, fallbackAfterTimeout: true), | |
| guard let buildSettings = await self.compileCommand(for: request.textDocument.uri, fallbackAfterTimeout: false), |
|
|
||
| guard | ||
| let workspace = await self.sourceKitLSPServer?.workspaceForDocument(uri: request.textDocument.uri), | ||
| let index = await workspace.index(checkedFor: .modifiedFiles) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if a file is modified, it’s reasonable to assume that the declarations within it are still the same, so we can relax the index check here a little bit.
| let index = await workspace.index(checkedFor: .modifiedFiles) | |
| let index = await workspace.index(checkedFor: .deletedFiles) |
| if isSorted, | ||
| let insertBeforeImport = importDecls.first(where: { importDecl in | ||
| guard let firstPath = importDecl.path.first?.name.text else { return false } | ||
| return firstPath > newModule | ||
| }) | ||
| { | ||
| return snapshot.position(of: insertBeforeImport.position) | ||
| } else if let lastImport = importDecls.last { | ||
| return snapshot.position(of: lastImport.endPosition) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can always use the isSorted case here. That way we try and find some reasonable sorted-ish location even if a single import statement doesn’t match alphabetical order.
| extension SwiftLanguageService { | ||
| /// Finds missing import code actions for diagnostics indicating unresolved symbols. | ||
| /// Exposed for unit testing. | ||
| package static func findMissingImports( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I blind or is this only called by tests? In which case we aren’t testing the actual implementation in the tests this one. I would have probably relied a lot more on tests that use SwiftPMTestProject instead of writing tests that exercise this alternate code path.
That being said, if you manage to write more unit-style tests with minimal amount of alternative code, that would of course be great.
| // This test relies on the index, so it needs to run where we can build and index. | ||
| try SkipUnless.longTestsEnabled() | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think this test is any slower than other tests that we have, relying on the index. Generally a slow test is a test that takes more than 1 or 2 seconds to execute, which I’d be surprised if this one does. So, I’d remove this check.
| DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)) | ||
| ) | ||
| diagnostic = diags.fullReport?.items.first { $0.message.contains("LibStruct") } | ||
| XCTAssert(diagnostic != nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessary because we XCTUnwrap below.
| [ | ||
| uri: [ | ||
| TextEdit(range: Range(positions["1️⃣"]), newText: "import Lib\n") | ||
| ] | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| [ | |
| uri: [ | |
| TextEdit(range: Range(positions["1️⃣"]), newText: "import Lib\n") | |
| ] | |
| ] | |
| [ | |
| uri: [TextEdit(range: Range(positions["1️⃣"]), newText: "import Lib\n")] | |
| ] |
fixes: #1089
This PR implements a new code action, AddMissingImports, which provides quick fixes to automatically add import statements for unresolved types or values. It leverages the semantic index to identify defining modules and intelligently inserts the necessary imports into the source file.
Key Features