blob: 974f9db7c25fc1f0fd68995a2597ab98f5723c13 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library services.src.correction.statement_analyzer;
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/services/correction/selection_analyzer.dart';
import 'package:analysis_server/src/services/correction/source_range.dart';
import 'package:analysis_server/src/services/correction/status.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/generated/source.dart';
/**
* Returns [Token]s of the given Dart source, not `null`, may be empty if no
* tokens or some exception happens.
*/
List<Token> _getTokens(String text) {
try {
List<Token> tokens = <Token>[];
Scanner scanner = new Scanner(null, new CharSequenceReader(text), null);
Token token = scanner.tokenize();
while (token.type != TokenType.EOF) {
tokens.add(token);
token = token.next;
}
return tokens;
} catch (e) {
return new List<Token>(0);
}
}
/**
* Analyzer to check if a selection covers a valid set of statements of AST.
*/
class StatementAnalyzer extends SelectionAnalyzer {
final CompilationUnit unit;
RefactoringStatus _status = new RefactoringStatus();
StatementAnalyzer(this.unit, SourceRange selection) : super(selection);
/**
* Returns the [RefactoringStatus] result of selection checking.
*/
RefactoringStatus get status => _status;
/**
* Records fatal error with given message and [Location].
*/
void invalidSelection(String message, [Location context]) {
if (!_status.hasFatalError) {
_status.addFatalError(message, context);
}
reset();
}
@override
Object visitCompilationUnit(CompilationUnit node) {
super.visitCompilationUnit(node);
if (!hasSelectedNodes) {
return null;
}
// check that selection does not begin/end in comment
{
int selectionStart = selection.offset;
int selectionEnd = selection.end;
List<SourceRange> commentRanges = getCommentRanges(unit);
for (SourceRange commentRange in commentRanges) {
if (commentRange.contains(selectionStart)) {
invalidSelection("Selection begins inside a comment.");
}
if (commentRange.containsExclusive(selectionEnd)) {
invalidSelection("Selection ends inside a comment.");
}
}
}
// more checks
if (!_status.hasFatalError) {
_checkSelectedNodes(node);
}
return null;
}
@override
Object visitDoStatement(DoStatement node) {
super.visitDoStatement(node);
List<AstNode> selectedNodes = this.selectedNodes;
if (_contains(selectedNodes, node.body)) {
invalidSelection(
"Operation not applicable to a 'do' statement's body and expression.");
}
return null;
}
@override
Object visitForStatement(ForStatement node) {
super.visitForStatement(node);
List<AstNode> selectedNodes = this.selectedNodes;
bool containsInit = _contains(selectedNodes, node.initialization) ||
_contains(selectedNodes, node.variables);
bool containsCondition = _contains(selectedNodes, node.condition);
bool containsUpdaters = _containsAny(selectedNodes, node.updaters);
bool containsBody = _contains(selectedNodes, node.body);
if (containsInit && containsCondition) {
invalidSelection(
"Operation not applicable to a 'for' statement's initializer and condition.");
} else if (containsCondition && containsUpdaters) {
invalidSelection(
"Operation not applicable to a 'for' statement's condition and updaters.");
} else if (containsUpdaters && containsBody) {
invalidSelection(
"Operation not applicable to a 'for' statement's updaters and body.");
}
return null;
}
@override
Object visitSwitchStatement(SwitchStatement node) {
super.visitSwitchStatement(node);
List<AstNode> selectedNodes = this.selectedNodes;
List<SwitchMember> switchMembers = node.members;
for (AstNode selectedNode in selectedNodes) {
if (switchMembers.contains(selectedNode)) {
invalidSelection(
"Selection must either cover whole switch statement or parts of a single case block.");
break;
}
}
return null;
}
@override
Object visitTryStatement(TryStatement node) {
super.visitTryStatement(node);
AstNode firstSelectedNode = this.firstSelectedNode;
if (firstSelectedNode != null) {
if (firstSelectedNode == node.body ||
firstSelectedNode == node.finallyBlock) {
invalidSelection(
"Selection must either cover whole try statement or parts of try, catch, or finally block.");
} else {
List<CatchClause> catchClauses = node.catchClauses;
for (CatchClause catchClause in catchClauses) {
if (firstSelectedNode == catchClause ||
firstSelectedNode == catchClause.body ||
firstSelectedNode == catchClause.exceptionParameter) {
invalidSelection(
"Selection must either cover whole try statement or parts of try, catch, or finally block.");
}
}
}
}
return null;
}
@override
Object visitWhileStatement(WhileStatement node) {
super.visitWhileStatement(node);
List<AstNode> selectedNodes = this.selectedNodes;
if (_contains(selectedNodes, node.condition) &&
_contains(selectedNodes, node.body)) {
invalidSelection(
"Operation not applicable to a while statement's expression and body.");
}
return null;
}
/**
* Checks final selected [AstNode]s after processing [CompilationUnit].
*/
void _checkSelectedNodes(CompilationUnit unit) {
List<AstNode> nodes = selectedNodes;
// some tokens before first selected node
{
AstNode firstNode = nodes[0];
SourceRange rangeBeforeFirstNode = rangeStartStart(selection, firstNode);
if (_hasTokens(rangeBeforeFirstNode)) {
invalidSelection(
"The beginning of the selection contains characters that "
"do not belong to a statement.",
newLocation_fromUnit(unit, rangeBeforeFirstNode));
}
}
// some tokens after last selected node
{
AstNode lastNode = nodes.last;
SourceRange rangeAfterLastNode = rangeEndEnd(lastNode, selection);
if (_hasTokens(rangeAfterLastNode)) {
invalidSelection(
"The end of the selection contains characters that "
"do not belong to a statement.",
newLocation_fromUnit(unit, rangeAfterLastNode));
}
}
}
/**
* Returns `true` if there are [Token]s in the given [SourceRange].
*/
bool _hasTokens(SourceRange range) {
CompilationUnitElement unitElement = unit.element;
String fullText = unitElement.context.getContents(unitElement.source).data;
String rangeText = fullText.substring(range.offset, range.end);
return _getTokens(rangeText).isNotEmpty;
}
/**
* Returns `true` if [nodes] contains [node].
*/
static bool _contains(List<AstNode> nodes, AstNode node) =>
nodes.contains(node);
/**
* Returns `true` if [nodes] contains one of the [otherNodes].
*/
static bool _containsAny(List<AstNode> nodes, List<AstNode> otherNodes) {
for (AstNode otherNode in otherNodes) {
if (nodes.contains(otherNode)) {
return true;
}
}
return false;
}
}