Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(19)

Side by Side Diff: pkg/analysis_server/lib/src/services/refactoring/extract_local.dart

Issue 489973002: Initial 'Extract Local' implementation. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: tweaks Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
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.
4
5 library services.src.refactoring.extract_local;
6
7 import 'dart:async';
8
9 import 'package:analysis_server/src/protocol2.dart' show SourceEdit;
10 import 'package:analysis_server/src/services/correction/change.dart';
11 import 'package:analysis_server/src/services/correction/selection_analyzer.dart' ;
12 import 'package:analysis_server/src/services/correction/source_range.dart';
13 import 'package:analysis_server/src/services/correction/status.dart';
14 import 'package:analysis_server/src/services/correction/strings.dart';
15 import 'package:analysis_server/src/services/correction/util.dart';
16 import 'package:analysis_server/src/services/refactoring/naming_conventions.dart ';
17 import 'package:analysis_server/src/services/refactoring/refactoring.dart';
18 import 'package:analysis_server/src/services/refactoring/refactoring_internal.da rt';
19 import 'package:analysis_server/src/services/search/element_visitors.dart';
20 import 'package:analyzer/src/generated/ast.dart';
21 import 'package:analyzer/src/generated/element.dart';
22 import 'package:analyzer/src/generated/java_core.dart';
23 import 'package:analyzer/src/generated/scanner.dart';
24 import 'package:analyzer/src/generated/source.dart';
25
26
27 const String _TOKEN_SEPARATOR = "\uFFFF";
28
29
30 /**
31 * [ExtractLocalRefactoring] implementation.
32 */
33 class ExtractLocalRefactoringImpl extends RefactoringImpl implements
34 ExtractLocalRefactoring {
35 final CompilationUnit unit;
36 final int selectionOffset;
37 final int selectionLength;
38 String file;
39 SourceRange selectionRange;
40 CorrectionUtils utils;
41
42 String name;
43 bool extractAll = true;
44 final List<String> names = <String>[];
45 final List<int> offsets = <int>[];
46 final List<int> lengths = <int>[];
47
48 Expression rootExpression;
49 Expression singleExpression;
50 bool wholeStatementExpression = false;
51 String stringLiteralPart;
52 final List<SourceRange> occurrences = <SourceRange>[];
53 final Set<String> excludedVariableNames = new Set<String>();
54
55 ExtractLocalRefactoringImpl(this.unit, this.selectionOffset,
56 this.selectionLength) {
57 file = unit.element.source.fullName;
58 selectionRange = new SourceRange(selectionOffset, selectionLength);
59 utils = new CorrectionUtils(unit);
60 }
61
62 String get declarationKeyword {
63 if (_isPartOfConstantExpression(rootExpression)) {
64 return "const";
65 } else {
66 return "var";
67 }
68 }
69
70 @override
71 String get refactoringName => 'Extract Local Variable';
72
73 @override
74 Future<RefactoringStatus> checkFinalConditions() {
75 RefactoringStatus result = new RefactoringStatus();
76 if (excludedVariableNames.contains(name)) {
77 result.addWarning(
78 format(
79 "A variable with name '{0}' is already defined in the visible scop e.",
80 name));
81 }
82 return new Future.value(result);
83 }
84
85 @override
86 Future<RefactoringStatus> checkInitialConditions() {
87 RefactoringStatus result = new RefactoringStatus();
88 // selection
89 result.addStatus(_checkSelection());
90 // occurrences
91 if (!result.hasFatalError) {
92 _prepareOccurrences();
93 _prepareExcludedNames();
94 }
95 // suggested names
96 _prepareNames();
97 // done
98 return new Future.value(result);
99 }
100
101 @override
102 RefactoringStatus checkName() {
103 return validateVariableName(name);
104 }
105
106 @override
107 Future<Change> createChange() {
108 Change change = new Change(refactoringName);
109 // prepare occurrences
110 List<SourceRange> occurrences;
111 if (extractAll) {
112 occurrences = this.occurrences;
113 } else {
114 occurrences = [selectionRange];
115 }
116 // If the whole expression of a statement is selected, like '1 + 2',
117 // then convert it into a variable declaration statement.
118 if (wholeStatementExpression && occurrences.length == 1) {
119 String keyword = declarationKeyword;
120 String declarationSource = '$keyword $name = ';
121 SourceEdit edit =
122 new SourceEdit(singleExpression.offset, 0, declarationSource);
123 change.addEdit(file, edit);
124 return new Future.value(change);
125 }
126 // add variable declaration
127 {
128 String declarationSource;
129 if (stringLiteralPart != null) {
130 declarationSource = "var ${name} = '${stringLiteralPart}';";
131 } else {
132 String keyword = declarationKeyword;
133 String initializerSource = utils.getRangeText(selectionRange);
134 declarationSource = "${keyword} ${name} = ${initializerSource};";
135 }
136 // prepare location for declaration
137 Statement targetStatement = _findTargetStatement(occurrences);
138 String prefix = utils.getNodePrefix(targetStatement);
139 // insert variable declaration
140 String eol = utils.endOfLine;
141 SourceEdit edit = new SourceEdit(
142 targetStatement.offset,
143 0,
144 '${declarationSource}${eol}${prefix}');
145 change.addEdit(file, edit);
146 }
147 // prepare replacement
148 String occurrenceReplacement = name;
149 if (stringLiteralPart != null) {
150 occurrenceReplacement = "\${$name}";
151 }
152 // replace occurrences with variable reference
153 for (SourceRange range in occurrences) {
154 SourceEdit edit = editFromRange(range, occurrenceReplacement);
155 change.addEdit(file, edit);
156 }
157 // done
158 return new Future.value(change);
159 }
160
161 @override
162 bool requiresPreview() => false;
163
164 /**
165 * Checks if [selectionRange] selects [Expression] which can be extracted, and
166 * location of this [DartExpression] in AST allows extracting.
167 */
168 RefactoringStatus _checkSelection() {
169 _ExtractExpressionAnalyzer _selectionAnalyzer =
170 new _ExtractExpressionAnalyzer(selectionRange);
171 unit.accept(_selectionAnalyzer);
172 AstNode coveringNode = _selectionAnalyzer.coveringNode;
173 // may be fatal error
174 {
175 RefactoringStatus status = _selectionAnalyzer.status;
176 if (status.hasFatalError) {
177 return status;
178 }
179 }
180 // we need enclosing block to add variable declaration statement
181 if (coveringNode == null ||
182 coveringNode.getAncestor((node) => node is Block) == null) {
183 return new RefactoringStatus.fatal(
184 'Expression inside of function must be selected '
185 'to activate this refactoring.');
186 }
187 // part of string literal
188 if (coveringNode is StringLiteral) {
189 stringLiteralPart = utils.getRangeText(selectionRange);
190 if (stringLiteralPart.startsWith("'") ||
191 stringLiteralPart.startsWith('"') ||
192 stringLiteralPart.endsWith("'") ||
193 stringLiteralPart.endsWith('"')) {
194 return new RefactoringStatus.fatal(
195 'Cannot extract only leading or trailing quote of string literal.');
196 }
197 return new RefactoringStatus();
198 }
199 // single node selected
200 if (_selectionAnalyzer.selectedNodes.length == 1 &&
201 !utils.selectionIncludesNonWhitespaceOutsideNode(
202 selectionRange,
203 _selectionAnalyzer.firstSelectedNode)) {
204 AstNode selectedNode = _selectionAnalyzer.firstSelectedNode;
205 if (selectedNode is Expression) {
206 rootExpression = selectedNode;
207 singleExpression = rootExpression;
208 wholeStatementExpression =
209 singleExpression.parent is ExpressionStatement;
210 return new RefactoringStatus();
211 }
212 }
213 // fragment of binary expression selected
214 if (coveringNode is BinaryExpression) {
215 BinaryExpression binaryExpression = coveringNode;
216 if (utils.validateBinaryExpressionRange(
217 binaryExpression,
218 selectionRange)) {
219 rootExpression = binaryExpression;
220 singleExpression = null;
221 return new RefactoringStatus();
222 }
223 }
224 // invalid selection
225 return new RefactoringStatus.fatal(
226 'Expression must be selected to activate this refactoring.');
227 }
228
229 /**
230 * Returns [AstNode]s at the offsets of the given [SourceRange]s.
231 */
232 List<AstNode> _findNodes(List<SourceRange> ranges) {
233 List<AstNode> nodes = <AstNode>[];
234 for (SourceRange range in ranges) {
235 AstNode node = new NodeLocator.con1(range.offset).searchWithin(unit);
236 nodes.add(node);
237 }
238 return nodes;
239 }
240
241 /**
242 * @return the [Statement] such that variable declaration added before it will be visible in
243 * all given occurrences.
244 */
245 Statement _findTargetStatement(List<SourceRange> occurrences) {
246 List<AstNode> nodes = _findNodes(occurrences);
247 List<AstNode> firstParents = getParents(nodes[0]);
248 AstNode commonParent = getNearestCommonAncestor(nodes);
249 if (commonParent is Block) {
250 int commonIndex = firstParents.indexOf(commonParent);
251 return firstParents[commonIndex + 1] as Statement;
252 } else {
253 return commonParent.getAncestor((node) => node is Statement);
254 }
255 }
256
257 /**
258 * @return `true` if it is OK to extract the node with the given [SourceRange] .
259 */
260 bool _isExtractable(SourceRange range) {
261 _ExtractExpressionAnalyzer analyzer = new _ExtractExpressionAnalyzer(range);
262 utils.unit.accept(analyzer);
263 return analyzer.status.isOK;
264 }
265
266 bool _isPartOfConstantExpression(AstNode node) {
267 if (node is TypedLiteral) {
268 return node.constKeyword != null;
269 }
270 if (node is InstanceCreationExpression) {
271 InstanceCreationExpression creation = node;
272 return creation.isConst;
273 }
274 if (node is ArgumentList ||
275 node is ConditionalExpression ||
276 node is BinaryExpression ||
277 node is ParenthesizedExpression ||
278 node is PrefixExpression ||
279 node is Literal ||
280 node is MapLiteralEntry) {
281 return _isPartOfConstantExpression(node.parent);
282 }
283 return false;
284 }
285
286 void _prepareExcludedNames() {
287 excludedVariableNames.clear();
288 // TODO(scheglov) clean up?
289 AstNode enclosingNode =
290 new NodeLocator.con1(selectionOffset).searchWithin(unit);
291 Block enclosingBlock = enclosingNode.getAncestor((node) => node is Block);
292 if (enclosingBlock != null) {
293 SourceRange newVariableVisibleRange =
294 rangeStartEnd(selectionRange, enclosingBlock.end);
295 ExecutableElement enclosingExecutable =
296 getEnclosingExecutableElement(enclosingNode);
297 if (enclosingExecutable != null) {
298 visitChildren(enclosingExecutable, (Element element) {
299 if (element is LocalElement) {
300 SourceRange elementRange = element.visibleRange;
301 if (elementRange != null &&
302 elementRange.intersects(newVariableVisibleRange)) {
303 excludedVariableNames.add(element.displayName);
304 }
305 }
306 return true;
307 });
308 }
309 }
310 }
311
312 void _prepareNames() {
313 names.clear();
314 // TODO(scheglov) implement
315 // Set<String> excluded = excludedVariableNames;
316 // if (_stringLiteralPart != null) {
317 // return getVariableNameSuggestions(_stringLiteralPart, excluded);
318 // } else if (_singleExpression != null) {
319 // _guessedNames = CorrectionUtils.getVariableNameSuggestions2(_singleExpre ssion.staticType, _singleExpression, excluded);
320 // } else {
321 // _guessedNames = ArrayUtils.EMPTY_STRING_ARRAY;
322 // }
323 }
324
325 /**
326 * @return all occurrences of the source which matches given selection, sorted by offset. First
327 * [SourceRange] is same as the given selection. May be empty, but not
328 * <code>null</code>.
329 */
330 List<SourceRange> _prepareOccurrences() {
331 // prepare selection
332 String selectionSource;
333 {
334 String rawSelectionSource = utils.getRangeText(selectionRange);
335 List<Token> selectionTokens = TokenUtils.getTokens(rawSelectionSource);
336 selectionSource = selectionTokens.join(_TOKEN_SEPARATOR);
337 }
338 // prepare enclosing function
339 AstNode enclosingFunction;
340 {
341 AstNode selectionNode =
342 new NodeLocator.con1(selectionOffset).searchWithin(unit);
343 enclosingFunction = getEnclosingExecutableNode(selectionNode);
344 }
345 // visit function
346 enclosingFunction.accept(
347 new _OccurrencesVisitor(this, occurrences, selectionSource));
348 // done
349 return occurrences;
350 }
351 }
352
353
354 /**
355 * [SelectionAnalyzer] for [ExtractLocalRefactoringImpl].
356 */
357 class _ExtractExpressionAnalyzer extends SelectionAnalyzer {
358 final RefactoringStatus status = new RefactoringStatus();
359
360 _ExtractExpressionAnalyzer(SourceRange selection) : super(selection);
361
362 /**
363 * Records fatal error with given message.
364 */
365 void invalidSelection(String message) {
366 _invalidSelection(message, null);
367 }
368
369 @override
370 Object visitAssignmentExpression(AssignmentExpression node) {
371 super.visitAssignmentExpression(node);
372 Expression lhs = node.leftHandSide;
373 if (_isFirstSelectedNode(lhs)) {
374 _invalidSelection(
375 'Cannot extract the left-hand side of an assignment.',
376 new RefactoringStatusContext.forNode(lhs));
377 }
378 return null;
379 }
380
381 @override
382 Object visitSimpleIdentifier(SimpleIdentifier node) {
383 super.visitSimpleIdentifier(node);
384 if (_isFirstSelectedNode(node)) {
385 // name of declaration
386 if (node.inDeclarationContext()) {
387 invalidSelection('Cannot extract the name part of a declaration.');
388 }
389 // method name
390 Element element = node.bestElement;
391 if (element is FunctionElement || element is MethodElement) {
392 invalidSelection('Cannot extract a single method name.');
393 }
394 // name in property access
395 AstNode parent = node.parent;
396 if (parent is PrefixedIdentifier && identical(parent.identifier, node)) {
397 invalidSelection('Cannot extract name part of a property access.');
398 }
399 if (parent is PropertyAccess && identical(parent.propertyName, node)) {
400 invalidSelection('Cannot extract name part of a property access.');
401 }
402 }
403 return null;
404 }
405
406 /**
407 * Records fatal error with given message and [RefactoringStatusContext].
408 */
409 void _invalidSelection(String message, RefactoringStatusContext context) {
410 status.addFatalError(message, context);
411 reset();
412 }
413
414 bool _isFirstSelectedNode(AstNode node) => identical(firstSelectedNode, node);
415 }
416
417
418 class _HasStatementVisitor extends GeneralizingAstVisitor {
419 final List<bool> result;
420
421 _HasStatementVisitor(this.result);
422
423 @override
424 visitStatement(Statement node) {
425 result[0] = true;
426 }
427 }
428
429
430 class _OccurrencesVisitor extends GeneralizingAstVisitor<Object> {
431 final ExtractLocalRefactoringImpl ref;
432
433 List<SourceRange> occurrences;
434
435 String selectionSource;
436
437 _OccurrencesVisitor(this.ref, this.occurrences, this.selectionSource);
438
439 @override
440 Object visitBinaryExpression(BinaryExpression node) {
441 if (!_hasStatements(node)) {
442 _tryToFindOccurrenceFragment(node);
443 return null;
444 }
445 return super.visitBinaryExpression(node);
446 }
447
448 @override
449 Object visitExpression(Expression node) {
450 if (ref._isExtractable(rangeNode(node))) {
451 _tryToFindOccurrence(node);
452 }
453 return super.visitExpression(node);
454 }
455
456 @override
457 Object visitSimpleStringLiteral(SimpleStringLiteral node) {
458 if (ref.stringLiteralPart != null) {
459 int occuLength = ref.stringLiteralPart.length;
460 String value = node.value;
461 int valueOffset = node.offset + (node.isMultiline ? 3 : 1);
462 int lastIndex = 0;
463 while (true) {
464 int index = value.indexOf(ref.stringLiteralPart, lastIndex);
465 if (index == -1) {
466 break;
467 }
468 lastIndex = index + occuLength;
469 int occuStart = valueOffset + index;
470 SourceRange occuRange = rangeStartLength(occuStart, occuLength);
471 occurrences.add(occuRange);
472 }
473 return null;
474 }
475 return visitExpression(node);
476 }
477
478 void _addOccurrence(SourceRange range) {
479 if (range.intersects(ref.selectionRange)) {
480 occurrences.add(ref.selectionRange);
481 } else {
482 occurrences.add(range);
483 }
484 }
485
486 bool _hasStatements(AstNode root) {
487 List<bool> result = [false];
488 root.accept(new _HasStatementVisitor(result));
489 return result[0];
490 }
491
492 void _tryToFindOccurrence(Expression node) {
493 String nodeSource = ref.utils.getNodeText(node);
494 List<Token> nodeTokens = TokenUtils.getTokens(nodeSource);
495 nodeSource = nodeTokens.join(_TOKEN_SEPARATOR);
496 if (nodeSource == selectionSource) {
497 SourceRange occuRange = rangeNode(node);
498 _addOccurrence(occuRange);
499 }
500 }
501
502 void _tryToFindOccurrenceFragment(Expression node) {
503 int nodeOffset = node.offset;
504 String nodeSource = ref.utils.getNodeText(node);
505 List<Token> nodeTokens = TokenUtils.getTokens(nodeSource);
506 nodeSource = nodeTokens.join(_TOKEN_SEPARATOR);
507 // find "selection" in "node" tokens
508 int lastIndex = 0;
509 while (true) {
510 // find next occurrence
511 int index = nodeSource.indexOf(selectionSource, lastIndex);
512 if (index == -1) {
513 break;
514 }
515 lastIndex = index + selectionSource.length;
516 // find start/end tokens
517 int startTokenIndex =
518 countMatches(nodeSource.substring(0, index), _TOKEN_SEPARATOR);
519 int endTokenIndex =
520 countMatches(nodeSource.substring(0, lastIndex), _TOKEN_SEPARATOR);
521 Token startToken = nodeTokens[startTokenIndex];
522 Token endToken = nodeTokens[endTokenIndex];
523 // add occurrence range
524 int occuStart = nodeOffset + startToken.offset;
525 int occuEnd = nodeOffset + endToken.end;
526 SourceRange occuRange = rangeStartEnd(occuStart, occuEnd);
527 _addOccurrence(occuRange);
528 }
529 }
530 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698