Skip to content

Commit 047c0f8

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
[Analyzer] Support analyzing open files without open workspaces
This introduces the concept of temporary analysis roots, used to ensure loose files that are opened can be analyzed even when there are no other analysis roots (and therefore no existing analysis drivers). Fixes Dart-Code/Dart-Code#2764. Change-Id: I6b208f7c2fe8b538d7254691221fb72604ef156e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/161500 Commit-Queue: Danny Tuppeny <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent f846d0e commit 047c0f8

File tree

8 files changed

+309
-91
lines changed

8 files changed

+309
-91
lines changed

pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class IntializedMessageHandler extends MessageHandler<InitializedParams, void> {
3131
await server.fetchClientConfigurationAndPerformDynamicRegistration();
3232

3333
if (!server.initializationOptions.onlyAnalyzeProjectsWithOpenFiles) {
34-
server.setAnalysisRoots(openWorkspacePaths);
34+
server.updateAnalysisRoots(openWorkspacePaths, const []);
3535
}
3636

3737
return success();

pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,11 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
6262
registerHandler(ShutdownMessageHandler(server));
6363
registerHandler(ExitMessageHandler(server));
6464
registerHandler(
65-
TextDocumentOpenHandler(
66-
server,
67-
server.initializationOptions.onlyAnalyzeProjectsWithOpenFiles,
68-
),
65+
TextDocumentOpenHandler(server),
6966
);
7067
registerHandler(TextDocumentChangeHandler(server));
7168
registerHandler(
72-
TextDocumentCloseHandler(
73-
server,
74-
server.initializationOptions.onlyAnalyzeProjectsWithOpenFiles,
75-
),
69+
TextDocumentCloseHandler(server),
7670
);
7771
registerHandler(HoverHandler(server));
7872
registerHandler(CompletionHandler(

pkg/analysis_server/lib/src/lsp/handlers/handler_text_document_changes.dart

Lines changed: 12 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,7 @@ class TextDocumentChangeHandler
7777

7878
class TextDocumentCloseHandler
7979
extends MessageHandler<DidCloseTextDocumentParams, void> {
80-
/// Whether analysis roots are based on open files and should be updated.
81-
bool updateAnalysisRoots;
82-
83-
TextDocumentCloseHandler(LspAnalysisServer server, this.updateAnalysisRoots)
84-
: super(server);
80+
TextDocumentCloseHandler(LspAnalysisServer server) : super(server);
8581

8682
@override
8783
Method get handlesMessage => Method.textDocument_didClose;
@@ -98,25 +94,7 @@ class TextDocumentCloseHandler
9894
server.removePriorityFile(path);
9995
server.documentVersions.remove(path);
10096
server.onOverlayDestroyed(path);
101-
102-
if (updateAnalysisRoots) {
103-
// If there are no other open files in this context, we can remove it
104-
// from the analysis roots.
105-
final contextFolder = server.contextManager.getContextFolderFor(path);
106-
var hasOtherFilesInContext = false;
107-
for (var otherDocPath in server.documentVersions.keys) {
108-
if (server.contextManager.getContextFolderFor(otherDocPath) ==
109-
contextFolder) {
110-
hasOtherFilesInContext = true;
111-
break;
112-
}
113-
}
114-
if (!hasOtherFilesInContext) {
115-
final projectFolder =
116-
_findProjectFolder(server.resourceProvider, path);
117-
server.updateAnalysisRoots([], [projectFolder]);
118-
}
119-
}
97+
server.removeTemporaryAnalysisRoot(path);
12098

12199
return success();
122100
});
@@ -125,13 +103,7 @@ class TextDocumentCloseHandler
125103

126104
class TextDocumentOpenHandler
127105
extends MessageHandler<DidOpenTextDocumentParams, void> {
128-
/// Whether analysis roots are based on open files and should be updated.
129-
bool updateAnalysisRoots;
130-
131-
DateTime lastSentAnalyzeOpenFilesWarnings;
132-
133-
TextDocumentOpenHandler(LspAnalysisServer server, this.updateAnalysisRoots)
134-
: super(server);
106+
TextDocumentOpenHandler(LspAnalysisServer server) : super(server);
135107

136108
@override
137109
Method get handlesMessage => Method.textDocument_didOpen;
@@ -160,32 +132,15 @@ class TextDocumentOpenHandler
160132

161133
driver?.addFile(path);
162134

163-
// If there was no current driver for this file, then we may need to add
164-
// its project folder as an analysis root.
165-
if (updateAnalysisRoots && driver == null) {
166-
final projectFolder = _findProjectFolder(server.resourceProvider, path);
167-
if (projectFolder != null) {
168-
server.updateAnalysisRoots([projectFolder], []);
169-
} else {
170-
// There was no pubspec - ideally we should add just the file
171-
// here but we don't currently support that.
172-
// https://github.com/dart-lang/sdk/issues/32256
173-
174-
// Send a warning to the user, but only if we haven't already in the
175-
// last 60 seconds.
176-
if (lastSentAnalyzeOpenFilesWarnings == null ||
177-
(DateTime.now()
178-
.difference(lastSentAnalyzeOpenFilesWarnings)
179-
.inSeconds >
180-
60)) {
181-
lastSentAnalyzeOpenFilesWarnings = DateTime.now();
182-
server.showMessageToUser(
183-
MessageType.Warning,
184-
'When using onlyAnalyzeProjectsWithOpenFiles, files opened that '
185-
'are not contained within project folders containing pubspec.yaml, '
186-
'.packages or BUILD files will not be analyzed.');
187-
}
188-
}
135+
// Figure out the best analysis root for this file and register it as a temporary
136+
// analysis root. We need to register it even if we found a driver, so that if
137+
// the driver existed only because of another open file, it will not be removed
138+
// when that file is closed.
139+
final analysisRoot = driver?.contextRoot?.root ??
140+
_findProjectFolder(server.resourceProvider, path) ??
141+
dirname(path);
142+
if (analysisRoot != null) {
143+
server.addTemporaryAnalysisRoot(path, analysisRoot);
189144
}
190145

191146
server.addPriorityFile(path);

pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ class LspAnalysisServer extends AbstractAnalysisServer {
102102

103103
StreamSubscription _pluginChangeSubscription;
104104

105+
/// Temporary analysis roots for open files.
106+
///
107+
/// When a file is opened and there is no driver available (for example no
108+
/// folder was opened in the editor, so the set of analysis roots is empty)
109+
/// we add temporary roots for the project (or containing) folder. When the
110+
/// file is closed, it is removed from this map and if no other open file
111+
/// uses that root, it will be removed from the set of analysis roots.
112+
///
113+
/// key: file path of the open file
114+
/// value: folder to be used as a root.
115+
final _temporaryAnalysisRoots = <String, String>{};
116+
117+
/// The set of analysis roots explicitly added to the workspace.
118+
final _explicitAnalysisRoots = HashSet<String>();
119+
105120
/// Initialize a newly created server to send and receive messages to the
106121
/// given [channel].
107122
LspAnalysisServer(
@@ -170,6 +185,12 @@ class LspAnalysisServer extends AbstractAnalysisServer {
170185
}
171186
}
172187

188+
/// Adds a temporary analysis root for an open file.
189+
void addTemporaryAnalysisRoot(String filePath, String folderPath) {
190+
_temporaryAnalysisRoots[filePath] = folderPath;
191+
_refreshAnalysisRoots();
192+
}
193+
173194
/// The socket from which messages are being read has been closed.
174195
void done() {}
175196

@@ -417,6 +438,12 @@ class LspAnalysisServer extends AbstractAnalysisServer {
417438
}
418439
}
419440

441+
/// Removes any temporary analysis root for a file that was closed.
442+
void removeTemporaryAnalysisRoot(String filePath) {
443+
_temporaryAnalysisRoots.remove(filePath);
444+
_refreshAnalysisRoots();
445+
}
446+
420447
void sendErrorResponse(Message message, ResponseError error) {
421448
if (message is RequestMessage) {
422449
channel.sendResponse(ResponseMessage(
@@ -492,14 +519,6 @@ class LspAnalysisServer extends AbstractAnalysisServer {
492519
));
493520
}
494521

495-
void setAnalysisRoots(List<String> includedPaths) {
496-
declarationsTracker?.discardContexts();
497-
final uniquePaths = HashSet<String>.of(includedPaths ?? const []);
498-
notificationManager.setAnalysisRoots(includedPaths, []);
499-
contextManager.setRoots(uniquePaths.toList(), []);
500-
addContextsToDeclarationsTracker();
501-
}
502-
503522
/// Returns `true` if closing labels should be sent for [file] with the given
504523
/// absolute path.
505524
bool shouldSendClosingLabelsFor(String file) {
@@ -567,12 +586,12 @@ class LspAnalysisServer extends AbstractAnalysisServer {
567586

568587
void updateAnalysisRoots(List<String> addedPaths, List<String> removedPaths) {
569588
// TODO(dantup): This is currently case-sensitive!
570-
final newPaths =
571-
HashSet<String>.of(contextManager.includedPaths ?? const [])
572-
..addAll(addedPaths ?? const [])
573-
..removeAll(removedPaths ?? const []);
574589

575-
setAnalysisRoots(newPaths.toList());
590+
_explicitAnalysisRoots
591+
..addAll(addedPaths ?? const [])
592+
..removeAll(removedPaths ?? const []);
593+
594+
_refreshAnalysisRoots();
576595
}
577596

578597
void _afterOverlayChanged(String path, dynamic changeForPlugins) {
@@ -589,6 +608,18 @@ class LspAnalysisServer extends AbstractAnalysisServer {
589608
capabilitiesComputer.performDynamicRegistration();
590609
}
591610

611+
void _refreshAnalysisRoots() {
612+
// Always include any temporary analysis roots for open files.
613+
final includedPaths = HashSet<String>.of(_explicitAnalysisRoots)
614+
..addAll(_temporaryAnalysisRoots.values)
615+
..toList();
616+
617+
declarationsTracker?.discardContexts();
618+
notificationManager.setAnalysisRoots(includedPaths.toList(), []);
619+
contextManager.setRoots(includedPaths.toList(), []);
620+
addContextsToDeclarationsTracker();
621+
}
622+
592623
void _updateDriversAndPluginsPriorityFiles() {
593624
final priorityFilesList = priorityFiles.toList();
594625
driverMap.values.forEach((driver) {

pkg/analysis_server/test/lsp/change_workspace_folders_test.dart

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,139 @@ class ChangeWorkspaceFoldersTest extends AbstractLspAnalysisServerTest {
5959
);
6060
}
6161

62+
Future<void>
63+
test_changeWorkspaceFolders_addExplicitParentOfImplicit_closeFile() async {
64+
final nestedFolderPath =
65+
join(workspaceFolder1Path, 'nested', 'deeply', 'in', 'folders');
66+
final nestedFilePath = join(nestedFolderPath, 'test.dart');
67+
final nestedFileUri = Uri.file(nestedFilePath);
68+
await newFile(nestedFilePath);
69+
70+
await initialize(allowEmptyRootUri: true);
71+
await openFile(nestedFileUri, '');
72+
73+
// Expect implicit root for the open file.
74+
expect(
75+
server.contextManager.includedPaths,
76+
unorderedEquals([nestedFolderPath]),
77+
);
78+
79+
// Add the real project root to the workspace (which should become an
80+
// explicit root).
81+
await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
82+
expect(
83+
server.contextManager.includedPaths,
84+
unorderedEquals([workspaceFolder1Path, nestedFolderPath]),
85+
);
86+
87+
// Closing the file should not result in the project being removed.
88+
await closeFile(nestedFileUri);
89+
expect(
90+
server.contextManager.includedPaths,
91+
unorderedEquals([workspaceFolder1Path]),
92+
);
93+
}
94+
95+
Future<void>
96+
test_changeWorkspaceFolders_addExplicitParentOfImplicit_closeFolder() async {
97+
final nestedFolderPath =
98+
join(workspaceFolder1Path, 'nested', 'deeply', 'in', 'folders');
99+
final nestedFilePath = join(nestedFolderPath, 'test.dart');
100+
final nestedFileUri = Uri.file(nestedFilePath);
101+
await newFile(nestedFilePath);
102+
103+
await initialize(allowEmptyRootUri: true);
104+
await openFile(nestedFileUri, '');
105+
106+
// Expect implicit root for the open file.
107+
expect(
108+
server.contextManager.includedPaths,
109+
unorderedEquals([nestedFolderPath]),
110+
);
111+
112+
// Add the real project root to the workspace (which should become an
113+
// explicit root).
114+
await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
115+
expect(
116+
server.contextManager.includedPaths,
117+
unorderedEquals([workspaceFolder1Path, nestedFolderPath]),
118+
);
119+
120+
// Removing the workspace folder should result in falling back to just the
121+
// nested folder.
122+
await changeWorkspaceFolders(remove: [workspaceFolder1Uri]);
123+
expect(
124+
server.contextManager.includedPaths,
125+
unorderedEquals([nestedFolderPath]),
126+
);
127+
}
128+
129+
Future<void>
130+
test_changeWorkspaceFolders_addImplicitChildOfExplicitParent_closeFile() async {
131+
final nestedFolderPath =
132+
join(workspaceFolder1Path, 'nested', 'deeply', 'in', 'folders');
133+
final nestedFilePath = join(nestedFolderPath, 'test.dart');
134+
final nestedFileUri = Uri.file(nestedFilePath);
135+
await newFile(nestedFilePath);
136+
137+
await initialize(workspaceFolders: [workspaceFolder1Uri]);
138+
139+
// Expect explicit root for the workspace folder.
140+
expect(
141+
server.contextManager.includedPaths,
142+
unorderedEquals([workspaceFolder1Path]),
143+
);
144+
145+
// Open a file, though no new root is expected as it was mapped to the existing
146+
// open folder.
147+
await openFile(nestedFileUri, '');
148+
expect(
149+
server.contextManager.includedPaths,
150+
unorderedEquals([workspaceFolder1Path]),
151+
);
152+
153+
// Closing the file should not result in the project being removed.
154+
await closeFile(nestedFileUri);
155+
expect(
156+
server.contextManager.includedPaths,
157+
unorderedEquals([workspaceFolder1Path]),
158+
);
159+
}
160+
161+
Future<void>
162+
test_changeWorkspaceFolders_addImplicitChildOfExplicitParent_closeFolder() async {
163+
final nestedFolderPath =
164+
join(workspaceFolder1Path, 'nested', 'deeply', 'in', 'folders');
165+
final nestedFilePath = join(nestedFolderPath, 'test.dart');
166+
final nestedFileUri = Uri.file(nestedFilePath);
167+
await newFile(nestedFilePath);
168+
169+
await initialize(workspaceFolders: [workspaceFolder1Uri]);
170+
171+
// Expect explicit root for the workspace folder.
172+
expect(
173+
server.contextManager.includedPaths,
174+
unorderedEquals([workspaceFolder1Path]),
175+
);
176+
177+
// Open a file, though no new root is expected as it was mapped to the existing
178+
// open folder.
179+
await openFile(nestedFileUri, '');
180+
expect(
181+
server.contextManager.includedPaths,
182+
unorderedEquals([workspaceFolder1Path]),
183+
);
184+
185+
// Removing the workspace folder will retain the workspace folder, as that's
186+
// the folder we picked when the file was opened since there was already
187+
// a root for it.
188+
await changeWorkspaceFolders(remove: [workspaceFolder1Uri]);
189+
expect(
190+
server.contextManager.includedPaths,
191+
unorderedEquals([workspaceFolder1Path]),
192+
);
193+
}
194+
62195
Future<void> test_changeWorkspaceFolders_remove() async {
63196
await initialize(
64197
workspaceFolders: [workspaceFolder1Uri, workspaceFolder2Uri],
@@ -79,7 +212,7 @@ class ChangeWorkspaceFoldersTest extends AbstractLspAnalysisServerTest {
79212

80213
// Generate an error in the test project.
81214
final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
82-
await openFile(mainFileUri, 'String a = 1;');
215+
newFile(mainFilePath, content: 'String a = 1;');
83216
final initialDiagnostics = await firstDiagnosticsUpdate;
84217
expect(initialDiagnostics, hasLength(1));
85218

0 commit comments

Comments
 (0)