OLD | NEW |
---|---|
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 import 'dart:async'; | |
6 | |
5 import 'package:analysis_server/protocol/protocol_generated.dart'; | 7 import 'package:analysis_server/protocol/protocol_generated.dart'; |
6 import 'package:analyzer/dart/analysis/results.dart'; | 8 import 'package:analyzer/dart/analysis/results.dart'; |
7 import 'package:analyzer/dart/analysis/session.dart'; | 9 import 'package:analyzer/dart/ast/ast.dart'; |
10 import 'package:analyzer/dart/ast/ast_factory.dart'; | |
11 import 'package:analyzer/dart/ast/token.dart'; | |
8 import 'package:analyzer/dart/element/element.dart'; | 12 import 'package:analyzer/dart/element/element.dart'; |
9 import 'package:analyzer_plugin/protocol/protocol_common.dart'; | 13 import 'package:analyzer/file_system/file_system.dart'; |
14 import 'package:analyzer/src/dart/ast/ast_factory.dart'; | |
15 import 'package:analyzer/src/dart/ast/token.dart'; | |
16 import 'package:analyzer/src/dart/resolver/scope.dart'; | |
17 import 'package:analyzer/src/generated/source.dart'; | |
18 import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element; | |
10 import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dar t'; | 19 import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dar t'; |
20 import 'package:analyzer_plugin/utilities/range_factory.dart'; | |
21 import 'package:front_end/src/base/syntactic_entity.dart'; | |
22 import 'package:path/src/context.dart'; | |
11 | 23 |
12 /** | 24 /** |
13 * An object used to compute the edits required to ensure that a list of | 25 * An object used to compute a set of edits to add imports to a given library in |
14 * elements is imported into a given library. | 26 * order to make a given set of elements visible. |
27 * | |
28 * This is used to implement the `edit.importElements` request. | |
15 */ | 29 */ |
16 class ImportElementsComputer { | 30 class ImportElementsComputer { |
17 /** | 31 /** |
18 * The analysis session used to compute the unit. | 32 * The resource provider used to access the file system. |
19 */ | 33 */ |
20 final AnalysisSession session; | 34 final ResourceProvider resourceProvider; |
21 | 35 |
22 /** | 36 /** |
23 * The library element representing the library to which the imports are to be | 37 * The resolution result associated with the defining compilation unit of the |
24 * added. | 38 * library to which imports might be added. |
25 */ | 39 */ |
26 final LibraryElement libraryElement; | 40 final ResolveResult libraryResult; |
27 | 41 |
28 /** | 42 /** |
29 * The path of the defining compilation unit of the library. | 43 * Initialize a newly created builder. |
30 */ | 44 */ |
31 final String path; | 45 ImportElementsComputer(this.resourceProvider, this.libraryResult); |
32 | 46 |
33 /** | 47 /** |
34 * The elements that are to be imported into the library. | 48 * Create the edits that will cause the list of [importedElements] to be |
35 */ | 49 * imported into the library at the given [path]. |
36 final List<ImportedElements> elements; | 50 */ |
37 | 51 Future<SourceChange> createEdits( |
38 /** | 52 List<ImportedElements> importedElementsList) async { |
39 * Initialize a newly created computer to compute the edits required to ensure | 53 List<ImportedElements> filteredImportedElements = |
40 * that the given list of [elements] is imported into a given [library]. | 54 _filterImportedElements(importedElementsList); |
41 */ | 55 LibraryElement libraryElement = libraryResult.libraryElement; |
42 ImportElementsComputer(ResolveResult result, this.path, this.elements) | 56 SourceFactory sourceFactory = libraryElement.context.sourceFactory; |
scheglov
2017/07/27 20:54:15
It might be worth eventually to expose SourceFacto
Brian Wilkerson
2017/07/27 20:55:33
I agree. I'll try to tackle that soon.
| |
43 : session = result.session, | 57 List<ImportDirective> existingImports = <ImportDirective>[]; |
44 libraryElement = result.libraryElement; | 58 for (var directive in libraryResult.unit.directives) { |
45 | 59 if (directive is ImportDirective) { |
46 /** | 60 existingImports.add(directive); |
47 * Compute and return the list of edits. | 61 } |
48 */ | 62 } |
49 List<SourceEdit> compute() { | 63 |
50 DartChangeBuilder builder = new DartChangeBuilder(session); | 64 DartChangeBuilder builder = new DartChangeBuilder(libraryResult.session); |
51 builder.addFileEdit(path, (DartFileEditBuilder builder) { | 65 await builder.addFileEdit(libraryResult.path, |
52 // TODO(brianwilkerson) Implement this. | 66 (DartFileEditBuilder builder) { |
67 for (ImportedElements importedElements in filteredImportedElements) { | |
68 List<ImportDirective> matchingImports = | |
69 _findMatchingImports(existingImports, importedElements); | |
70 if (matchingImports.isEmpty) { | |
71 // | |
72 // The required library is not being imported with a matching prefix, | |
73 // so we need to add an import. | |
74 // | |
75 File importedFile = resourceProvider.getFile(importedElements.path); | |
76 Uri uri = sourceFactory.restoreUri(importedFile.createSource()); | |
77 Source importedSource = importedFile.createSource(uri); | |
78 String importUri = | |
79 _getLibrarySourceUri(libraryElement, importedSource); | |
80 int offset = _offsetForInsertion(importUri); | |
81 builder.addInsertion(offset, (DartEditBuilder builder) { | |
82 builder.writeln(); | |
83 builder.write("import '"); | |
84 builder.write(importUri); | |
85 builder.write("'"); | |
86 if (importedElements.prefix.isNotEmpty) { | |
87 builder.write(' as '); | |
88 builder.write(importedElements.prefix); | |
89 } | |
90 builder.write(';'); | |
91 }); | |
92 } else { | |
93 // | |
94 // There are some imports of the library with a matching prefix. We | |
95 // need to determine whether the names are already visible or whether | |
96 // we need to make edits to make them visible. | |
97 // | |
98 // Compute the edits that need to be made. | |
99 // | |
100 Map<ImportDirective, _ImportUpdate> updateMap = | |
101 <ImportDirective, _ImportUpdate>{}; | |
102 for (String requiredName in importedElements.elements) { | |
103 _computeUpdate(updateMap, matchingImports, requiredName); | |
104 } | |
105 // | |
106 // Apply the edits. | |
107 // | |
108 for (ImportDirective directive in updateMap.keys) { | |
109 _ImportUpdate update = updateMap[directive]; | |
110 List<String> namesToUnhide = update.namesToUnhide; | |
111 List<String> namesToShow = update.namesToShow; | |
112 namesToShow.sort(); | |
113 NodeList<Combinator> combinators = directive.combinators; | |
114 int combinatorCount = combinators.length; | |
115 for (int combinatorIndex = 0; | |
116 combinatorIndex < combinatorCount; | |
117 combinatorIndex++) { | |
118 Combinator combinator = combinators[combinatorIndex]; | |
119 if (combinator is HideCombinator && namesToUnhide.isNotEmpty) { | |
120 NodeList<SimpleIdentifier> hiddenNames = combinator.hiddenNames; | |
121 int nameCount = hiddenNames.length; | |
122 int first = -1; | |
123 for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) { | |
124 if (namesToUnhide.contains(hiddenNames[nameIndex].name)) { | |
125 if (first < 0) { | |
126 first = nameIndex; | |
127 } | |
128 } else { | |
129 if (first >= 0) { | |
130 // Remove a range of names. | |
131 builder.addDeletion(range.startStart( | |
132 hiddenNames[first], hiddenNames[nameIndex])); | |
133 first = -1; | |
134 } | |
135 } | |
136 } | |
137 if (first == 0) { | |
138 // Remove the whole combinator. | |
139 if (combinatorIndex == 0) { | |
140 if (combinatorCount > 1) { | |
141 builder.addDeletion(range.startStart( | |
142 combinator, combinators[combinatorIndex + 1])); | |
143 } else { | |
144 SyntacticEntity precedingNode = directive.prefix ?? | |
145 directive.deferredKeyword ?? | |
146 directive.uri; | |
147 if (precedingNode == null) { | |
148 builder.addDeletion(range.node(combinator)); | |
149 } else { | |
150 builder.addDeletion( | |
151 range.endEnd(precedingNode, combinator)); | |
152 } | |
153 } | |
154 } else { | |
155 builder.addDeletion(range.endEnd( | |
156 combinators[combinatorIndex - 1], combinator)); | |
157 } | |
158 } else if (first > 0) { | |
159 // Remove a range of names that includes the last name. | |
160 builder.addDeletion(range.endEnd( | |
161 hiddenNames[first - 1], hiddenNames[nameCount - 1])); | |
162 } | |
163 } else if (combinator is ShowCombinator && | |
164 namesToShow.isNotEmpty) { | |
165 // TODO(brianwilkerson) Add the names in alphabetic order. | |
166 builder.addInsertion(combinator.shownNames.last.end, | |
167 (DartEditBuilder builder) { | |
168 for (String nameToShow in namesToShow) { | |
169 builder.write(', '); | |
170 builder.write(nameToShow); | |
171 } | |
172 }); | |
173 } | |
174 } | |
175 } | |
176 } | |
177 } | |
53 }); | 178 }); |
54 return <SourceEdit>[]; // builder.sourceChange | 179 return builder.sourceChange; |
180 } | |
181 | |
182 /** | |
183 * Choose the import for which the least amount of work is required, | |
184 * preferring to do no work in there is an import that already makes the name | |
185 * visible, and preferring to remove hide combinators rather than add show | |
186 * combinators. | |
187 * | |
188 * The name is visible without needing any changes if: | |
189 * - there is an import with no combinators, | |
190 * - there is an import with only hide combinators and none of them hide the | |
191 * name, | |
192 * - there is an import that shows the name and doesn't subsequently hide the | |
193 * name. | |
194 */ | |
195 void _computeUpdate(Map<ImportDirective, _ImportUpdate> updateMap, | |
196 List<ImportDirective> matchingImports, String requiredName) { | |
197 /** | |
198 * Return `true` if the [requiredName] is in the given list of [names]. | |
199 */ | |
200 bool nameIn(NodeList<SimpleIdentifier> names) { | |
201 for (SimpleIdentifier name in names) { | |
202 if (name.name == requiredName) { | |
203 return true; | |
204 } | |
205 } | |
206 return false; | |
207 } | |
208 | |
209 ImportDirective preferredDirective = null; | |
210 int bestEditCount = -1; | |
211 bool deleteHide = false; | |
212 bool addShow = false; | |
213 | |
214 for (ImportDirective directive in matchingImports) { | |
215 NodeList<Combinator> combinators = directive.combinators; | |
216 if (combinators.isEmpty) { | |
217 return; | |
218 } | |
219 bool hasHide = false; | |
220 bool needsShow = false; | |
221 int editCount = 0; | |
222 for (Combinator combinator in combinators) { | |
223 if (combinator is HideCombinator) { | |
224 if (nameIn(combinator.hiddenNames)) { | |
225 hasHide = true; | |
226 editCount++; | |
227 } | |
228 } else if (combinator is ShowCombinator) { | |
229 if (needsShow || !nameIn(combinator.shownNames)) { | |
230 needsShow = true; | |
231 editCount++; | |
232 } | |
233 } | |
234 } | |
235 if (editCount == 0) { | |
236 return; | |
237 } else if (bestEditCount < 0 || editCount < bestEditCount) { | |
238 preferredDirective = directive; | |
239 bestEditCount = editCount; | |
240 deleteHide = hasHide; | |
241 addShow = needsShow; | |
242 } | |
243 } | |
244 | |
245 _ImportUpdate update = updateMap.putIfAbsent( | |
246 preferredDirective, () => new _ImportUpdate(preferredDirective)); | |
247 if (deleteHide) { | |
248 update.unhide(requiredName); | |
249 } | |
250 if (addShow) { | |
251 update.show(requiredName); | |
252 } | |
253 } | |
254 | |
255 /** | |
256 * Filter the given list of imported elements ([originalList]) so that only | |
257 * the names that are not already defined still remain. Names that are already | |
258 * defined are removed even if they might not resolve to the same name as in | |
259 * the original source. | |
260 */ | |
261 List<ImportedElements> _filterImportedElements( | |
262 List<ImportedElements> originalList) { | |
263 LibraryElement libraryElement = libraryResult.libraryElement; | |
264 LibraryScope libraryScope = new LibraryScope(libraryElement); | |
265 AstFactory factory = new AstFactoryImpl(); | |
266 List<ImportedElements> filteredList = <ImportedElements>[]; | |
267 for (ImportedElements elements in originalList) { | |
268 List<String> originalElements = elements.elements; | |
269 List<String> filteredElements = originalElements.toList(); | |
270 for (String name in originalElements) { | |
271 Identifier identifier = factory | |
272 .simpleIdentifier(new StringToken(TokenType.IDENTIFIER, name, -1)); | |
273 if (elements.prefix.isNotEmpty) { | |
274 SimpleIdentifier prefix = factory.simpleIdentifier( | |
275 new StringToken(TokenType.IDENTIFIER, elements.prefix, -1)); | |
276 Token period = new SimpleToken(TokenType.PERIOD, -1); | |
277 identifier = factory.prefixedIdentifier(prefix, period, identifier); | |
278 } | |
279 Element element = libraryScope.lookup(identifier, libraryElement); | |
280 if (element != null) { | |
281 filteredElements.remove(name); | |
282 } | |
283 } | |
284 if (originalElements.length == filteredElements.length) { | |
285 filteredList.add(elements); | |
286 } else if (filteredElements.isNotEmpty) { | |
287 filteredList.add(new ImportedElements( | |
288 elements.path, elements.prefix, filteredElements)); | |
289 } | |
290 } | |
291 return filteredList; | |
292 } | |
293 | |
294 /** | |
295 * Return all of the import elements in the list of [existingImports] that | |
296 * match the given specification of [importedElements], or an empty list if | |
297 * there are no such imports. | |
298 */ | |
299 List<ImportDirective> _findMatchingImports( | |
300 List<ImportDirective> existingImports, | |
301 ImportedElements importedElements) { | |
302 List<ImportDirective> matchingImports = <ImportDirective>[]; | |
303 for (ImportDirective existingImport in existingImports) { | |
304 if (_matches(existingImport, importedElements)) { | |
305 matchingImports.add(existingImport); | |
306 } | |
307 } | |
308 return matchingImports; | |
309 } | |
310 | |
311 /** | |
312 * Computes the best URI to import [what] into [from]. | |
313 * | |
314 * Copied from DartFileEditBuilderImpl. | |
315 */ | |
316 String _getLibrarySourceUri(LibraryElement from, Source what) { | |
317 String whatPath = what.fullName; | |
318 // check if an absolute URI (such as 'dart:' or 'package:') | |
319 Uri whatUri = what.uri; | |
320 String whatUriScheme = whatUri.scheme; | |
321 if (whatUriScheme != '' && whatUriScheme != 'file') { | |
322 return whatUri.toString(); | |
323 } | |
324 // compute a relative URI | |
325 Context context = resourceProvider.pathContext; | |
326 String fromFolder = context.dirname(from.source.fullName); | |
327 String relativeFile = context.relative(whatPath, from: fromFolder); | |
328 return context.split(relativeFile).join('/'); | |
329 } | |
330 | |
331 /** | |
332 * Return `true` if the given [import] matches the given specification of | |
333 * [importedElements]. They will match if they import the same library using | |
334 * the same prefix. | |
335 */ | |
336 bool _matches(ImportDirective import, ImportedElements importedElements) { | |
337 return (import.element as ImportElement).importedLibrary.source.fullName == | |
338 importedElements.path && | |
339 (import.prefix?.name ?? '') == importedElements.prefix; | |
340 } | |
341 | |
342 /** | |
343 * Return the offset at which an import of the given [importUri] should be | |
344 * inserted. | |
345 * | |
346 * Partially copied from DartFileEditBuilderImpl. | |
347 */ | |
348 int _offsetForInsertion(String importUri) { | |
349 // TODO(brianwilkerson) Fix this to find the right location. | |
350 // See DartFileEditBuilderImpl._addLibraryImports for inspiration. | |
351 CompilationUnit unit = libraryResult.unit; | |
352 LibraryDirective libraryDirective; | |
353 List<ImportDirective> importDirectives = <ImportDirective>[]; | |
354 for (Directive directive in unit.directives) { | |
355 if (directive is LibraryDirective) { | |
356 libraryDirective = directive; | |
357 } else if (directive is ImportDirective) { | |
358 importDirectives.add(directive); | |
359 } | |
360 } | |
361 if (importDirectives.isEmpty) { | |
362 if (libraryDirective == null) { | |
363 return 0; | |
364 } | |
365 return libraryDirective.end; | |
366 } | |
367 return importDirectives.last.end; | |
55 } | 368 } |
56 } | 369 } |
370 | |
371 /** | |
372 * Information about how a given import directive needs to be updated in order | |
373 * to make the required names visible. | |
374 */ | |
375 class _ImportUpdate { | |
376 /** | |
377 * The import directive to be updated. | |
378 */ | |
379 final ImportDirective import; | |
380 | |
381 /** | |
382 * The list of names that are currently hidden that need to not be hidden. | |
383 */ | |
384 final List<String> namesToUnhide = <String>[]; | |
385 | |
386 /** | |
387 * The list of names that need to be added to show clauses. | |
388 */ | |
389 final List<String> namesToShow = <String>[]; | |
390 | |
391 /** | |
392 * Initialize a newly created information holder to hold information about | |
393 * updates to the given [import]. | |
394 */ | |
395 _ImportUpdate(this.import); | |
396 | |
397 /** | |
398 * Record that the given [name] needs to be added to show combinators. | |
399 */ | |
400 void show(String name) { | |
401 namesToShow.add(name); | |
402 } | |
403 | |
404 /** | |
405 * Record that the given [name] needs to be removed from hide combinators. | |
406 */ | |
407 void unhide(String name) { | |
408 namesToUnhide.add(name); | |
409 } | |
410 } | |
OLD | NEW |