blob: f7a28e70957d2d4f749a3d194acb84c21157ed15 [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.
import 'dart:async';
import 'package:analysis_server/plugin/edit/assist/assist_core.dart';
import 'package:analysis_server/plugin/edit/assist/assist_dart.dart';
import 'package:analysis_server/plugin/edit/fix/fix_core.dart';
import 'package:analysis_server/plugin/edit/fix/fix_dart.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/computer/import_elements_computer.dart';
import 'package:analysis_server/src/domain_abstract.dart';
import 'package:analysis_server/src/edit/edit_dartfix.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/plugin/result_converter.dart';
import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/services/completion/postfix/postfix_completion.dart';
import 'package:analysis_server/src/services/completion/statement/statement_completion.dart';
import 'package:analysis_server/src/services/correction/assist_internal.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analysis_server/src/services/correction/organize_directives.dart';
import 'package:analysis_server/src/services/correction/sort_members.dart';
import 'package:analysis_server/src/services/correction/status.dart';
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/standard_resolution_map.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart' as engine;
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/ast_provider_driver.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/element/ast_provider.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart' as engine;
import 'package:analyzer/src/error/codes.dart' as engine;
import 'package:analyzer/src/generated/engine.dart' as engine;
import 'package:analyzer/src/generated/parser.dart' as engine;
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_constants.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:dart_style/dart_style.dart';
int test_resetCount = 0;
bool test_simulateRefactoringException_change = false;
bool test_simulateRefactoringException_final = false;
bool test_simulateRefactoringException_init = false;
bool test_simulateRefactoringReset_afterCreateChange = false;
bool test_simulateRefactoringReset_afterFinalConditions = false;
bool test_simulateRefactoringReset_afterInitialConditions = false;
/**
* Instances of the class [EditDomainHandler] implement a [RequestHandler]
* that handles requests in the edit domain.
*/
class EditDomainHandler extends AbstractRequestHandler {
/**
* The [SearchEngine] for this server.
*/
SearchEngine searchEngine;
/**
* The workspace for rename refactorings.
*/
RefactoringWorkspace refactoringWorkspace;
/**
* The object used to manage uncompleted refactorings.
*/
_RefactoringManager refactoringManager;
/**
* Initialize a newly created handler to handle requests for the given [server].
*/
EditDomainHandler(AnalysisServer server) : super(server) {
searchEngine = server.searchEngine;
refactoringWorkspace =
new RefactoringWorkspace(server.driverMap.values, searchEngine);
_newRefactoringManager();
}
Future dartfix(Request request) async {
// TODO(danrubel): Add support for dartfix plugins
//
// Compute fixes
//
var dartFix = new EditDartFix(server, request);
Response response = await dartFix.compute();
server.sendResponse(response);
}
Response format(Request request) {
server.options.analytics?.sendEvent('edit', 'format');
EditFormatParams params = new EditFormatParams.fromRequest(request);
String file = params.file;
String unformattedSource;
try {
Source source = server.resourceProvider.getFile(file).createSource();
unformattedSource =
server.fileContentOverlay[file] ?? source.contents.data;
} catch (e) {
return new Response.formatInvalidFile(request);
}
int start = params.selectionOffset;
int length = params.selectionLength;
// No need to preserve 0,0 selection
if (start == 0 && length == 0) {
start = null;
length = null;
}
SourceCode code = new SourceCode(unformattedSource,
uri: null,
isCompilationUnit: true,
selectionStart: start,
selectionLength: length);
DartFormatter formatter = new DartFormatter(pageWidth: params.lineLength);
SourceCode formattedResult;
try {
formattedResult = formatter.formatSource(code);
} on FormatterException {
return new Response.formatWithErrors(request);
}
String formattedSource = formattedResult.text;
List<SourceEdit> edits = <SourceEdit>[];
if (formattedSource != unformattedSource) {
//TODO: replace full replacements with smaller, more targeted edits
SourceEdit edit =
new SourceEdit(0, unformattedSource.length, formattedSource);
edits.add(edit);
}
int newStart = formattedResult.selectionStart;
int newLength = formattedResult.selectionLength;
// Sending null start/length values would violate protocol, so convert back
// to 0.
if (newStart == null) {
newStart = 0;
}
if (newLength == null) {
newLength = 0;
}
return new EditFormatResult(edits, newStart, newLength)
.toResponse(request.id);
}
Future getAssists(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
EditGetAssistsParams params = new EditGetAssistsParams.fromRequest(request);
String file = params.file;
int offset = params.offset;
int length = params.length;
List<SourceChange> changes = <SourceChange>[];
//
// Allow plugins to start computing assists.
//
Map<PluginInfo, Future<plugin.Response>> pluginFutures;
plugin.EditGetAssistsParams requestParams =
new plugin.EditGetAssistsParams(file, offset, length);
AnalysisDriver driver = server.getAnalysisDriver(file);
if (driver == null) {
pluginFutures = <PluginInfo, Future<plugin.Response>>{};
} else {
pluginFutures = server.pluginManager
.broadcastRequest(requestParams, contextRoot: driver.contextRoot);
}
//
// Compute fixes associated with server-generated errors.
//
AnalysisResult result = await server.getAnalysisResult(file);
if (result != null) {
CompilationUnit unit = result.unit;
CompilationUnitElement compilationUnitElement =
resolutionMap.elementDeclaredByCompilationUnit(unit);
DartAssistContext dartAssistContext = new _DartAssistContextForValues(
compilationUnitElement.source, offset, length, driver, unit);
try {
AssistProcessor processor = new AssistProcessor(dartAssistContext);
List<Assist> assists = await processor.compute();
assists.sort(Assist.SORT_BY_RELEVANCE);
for (Assist assist in assists) {
changes.add(assist.change);
}
} catch (_) {}
}
//
// Add the fixes produced by plugins to the server-generated fixes.
//
List<plugin.Response> responses =
await waitForResponses(pluginFutures, requestParameters: requestParams);
ResultConverter converter = new ResultConverter();
List<plugin.PrioritizedSourceChange> pluginChanges =
<plugin.PrioritizedSourceChange>[];
for (plugin.Response response in responses) {
plugin.EditGetAssistsResult result =
new plugin.EditGetAssistsResult.fromResponse(response);
pluginChanges.addAll(result.assists);
}
pluginChanges
.sort((first, second) => first.priority.compareTo(second.priority));
changes.addAll(pluginChanges.map(converter.convertPrioritizedSourceChange));
//
// Send the response.
//
server
.sendResponse(new EditGetAssistsResult(changes).toResponse(request.id));
}
Future getFixes(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
EditGetFixesParams params = new EditGetFixesParams.fromRequest(request);
String file = params.file;
int offset = params.offset;
//
// Allow plugins to start computing fixes.
//
Map<PluginInfo, Future<plugin.Response>> pluginFutures;
plugin.EditGetFixesParams requestParams =
new plugin.EditGetFixesParams(file, offset);
AnalysisDriver driver = server.getAnalysisDriver(file);
if (driver == null) {
pluginFutures = <PluginInfo, Future<plugin.Response>>{};
} else {
pluginFutures = server.pluginManager
.broadcastRequest(requestParams, contextRoot: driver.contextRoot);
}
//
// Compute fixes associated with server-generated errors.
//
List<AnalysisErrorFixes> errorFixesList = null;
while (errorFixesList == null) {
try {
errorFixesList = await _computeServerErrorFixes(driver, file, offset);
} on InconsistentAnalysisException {
// Loop around to try again to compute the fixes.
}
}
//
// Add the fixes produced by plugins to the server-generated fixes.
//
List<plugin.Response> responses =
await waitForResponses(pluginFutures, requestParameters: requestParams);
ResultConverter converter = new ResultConverter();
for (plugin.Response response in responses) {
plugin.EditGetFixesResult result =
new plugin.EditGetFixesResult.fromResponse(response);
errorFixesList
.addAll(result.fixes.map(converter.convertAnalysisErrorFixes));
}
//
// Send the response.
//
server.sendResponse(
new EditGetFixesResult(errorFixesList).toResponse(request.id));
}
Future getPostfixCompletion(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
server.options.analytics?.sendEvent('edit', 'getPostfixCompletion');
var params = new EditGetPostfixCompletionParams.fromRequest(request);
SourceChange change;
AnalysisResult result = await server.getAnalysisResult(params.file);
if (result != null) {
CompilationUnit unit = result.unit;
CompilationUnitElement unitElement =
resolutionMap.elementDeclaredByCompilationUnit(unit);
if (unitElement.context != null) {
PostfixCompletionContext context = new PostfixCompletionContext(
params.file,
result.lineInfo,
params.offset,
params.key,
result.driver,
unit,
unitElement,
result.errors);
PostfixCompletionProcessor processor =
new PostfixCompletionProcessor(context);
PostfixCompletion completion = await processor.compute();
change = completion?.change;
}
}
if (change == null) {
change = new SourceChange("", edits: []);
}
Response response =
new EditGetPostfixCompletionResult(change).toResponse(request.id);
server.sendResponse(response);
}
Future getStatementCompletion(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
var params = new EditGetStatementCompletionParams.fromRequest(request);
SourceChange change;
AnalysisResult result = await server.getAnalysisResult(params.file);
if (result != null) {
CompilationUnit unit = result.unit;
CompilationUnitElement unitElement =
resolutionMap.elementDeclaredByCompilationUnit(unit);
if (unitElement.context != null) {
StatementCompletionContext context = new StatementCompletionContext(
params.file,
result.lineInfo,
params.offset,
unit,
unitElement,
result.errors);
StatementCompletionProcessor processor =
new StatementCompletionProcessor(context);
StatementCompletion completion = await processor.compute();
change = completion.change;
}
}
if (change == null) {
change = new SourceChange("", edits: []);
}
Response response = new EditGetStatementCompletionResult(change, false)
.toResponse(request.id);
server.sendResponse(response);
}
@override
Response handleRequest(Request request) {
try {
String requestName = request.method;
if (requestName == EDIT_REQUEST_FORMAT) {
return format(request);
} else if (requestName == EDIT_REQUEST_GET_ASSISTS) {
getAssists(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS) {
return _getAvailableRefactorings(request);
} else if (requestName == EDIT_REQUEST_GET_FIXES) {
getFixes(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_DARTFIX) {
dartfix(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_GET_REFACTORING) {
return _getRefactoring(request);
} else if (requestName == EDIT_REQUEST_IMPORT_ELEMENTS) {
importElements(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_ORGANIZE_DIRECTIVES) {
organizeDirectives(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_SORT_MEMBERS) {
sortMembers(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_GET_STATEMENT_COMPLETION) {
getStatementCompletion(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_IS_POSTFIX_COMPLETION_APPLICABLE) {
isPostfixCompletionApplicable(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == EDIT_REQUEST_GET_POSTFIX_COMPLETION) {
getPostfixCompletion(request);
return Response.DELAYED_RESPONSE;
} else if (requestName ==
EDIT_REQUEST_LIST_POSTFIX_COMPLETION_TEMPLATES) {
listPostfixCompletionTemplates(request);
return Response.DELAYED_RESPONSE;
}
} on RequestFailure catch (exception) {
return exception.response;
}
return null;
}
/**
* Implement the `edit.importElements` request.
*/
Future<void> importElements(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
EditImportElementsParams params =
new EditImportElementsParams.fromRequest(request);
//
// Prepare the resolved unit.
//
AnalysisResult result = await server.getAnalysisResult(params.file);
if (result == null) {
server.sendResponse(new Response.importElementsInvalidFile(request));
}
CompilationUnitElement libraryUnit =
result.libraryElement.definingCompilationUnit;
if (libraryUnit != result.unit.declaredElement) {
// The file in the request is a part of a library. We need to pass the
// defining compilation unit to the computer, not the part.
result = await server.getAnalysisResult(libraryUnit.source.fullName);
if (result == null) {
server.sendResponse(new Response.importElementsInvalidFile(request));
}
}
//
// Compute the edits required to import the required elements.
//
ImportElementsComputer computer =
new ImportElementsComputer(server.resourceProvider, result);
SourceChange change = await computer.createEdits(params.elements);
List<SourceFileEdit> edits = change.edits;
SourceFileEdit edit = edits.isEmpty ? null : edits[0];
//
// Send the response.
//
server.sendResponse(
new EditImportElementsResult(edit: edit).toResponse(request.id));
}
Future isPostfixCompletionApplicable(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
var params = new EditGetPostfixCompletionParams.fromRequest(request);
bool value = false;
AnalysisResult result = await server.getAnalysisResult(params.file);
if (result != null) {
CompilationUnit unit = result.unit;
CompilationUnitElement unitElement =
resolutionMap.elementDeclaredByCompilationUnit(unit);
if (unitElement.context != null) {
PostfixCompletionContext context = new PostfixCompletionContext(
params.file,
result.lineInfo,
params.offset,
params.key,
result.driver,
unit,
unitElement,
result.errors);
PostfixCompletionProcessor processor =
new PostfixCompletionProcessor(context);
value = await processor.isApplicable();
}
}
Response response = new EditIsPostfixCompletionApplicableResult(value)
.toResponse(request.id);
server.sendResponse(response);
}
Future listPostfixCompletionTemplates(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
var templates = DartPostfixCompletion.ALL_TEMPLATES
.map((pfc) =>
new PostfixTemplateDescriptor(pfc.name, pfc.key, pfc.example))
.toList();
Response response = new EditListPostfixCompletionTemplatesResult(templates)
.toResponse(request.id);
server.sendResponse(response);
}
Future<void> organizeDirectives(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
server.options.analytics?.sendEvent('edit', 'organizeDirectives');
var params = new EditOrganizeDirectivesParams.fromRequest(request);
// prepare file
String file = params.file;
if (!engine.AnalysisEngine.isDartFileName(file)) {
server.sendResponse(new Response.fileNotAnalyzed(request, file));
return;
}
// Prepare the file information.
AnalysisResult result = await server.getAnalysisResult(file);
if (result == null) {
server.sendResponse(new Response.fileNotAnalyzed(request, file));
return;
}
int fileStamp = -1;
String code = result.content;
CompilationUnit unit = result.unit;
List<engine.AnalysisError> errors = result.errors;
// check if there are scan/parse errors in the file
int numScanParseErrors = _getNumberOfScanParseErrors(errors);
if (numScanParseErrors != 0) {
server.sendResponse(new Response.organizeDirectivesError(
request, 'File has $numScanParseErrors scan/parse errors.'));
return;
}
// do organize
DirectiveOrganizer sorter = new DirectiveOrganizer(code, unit, errors);
List<SourceEdit> edits = sorter.organize();
SourceFileEdit fileEdit = new SourceFileEdit(file, fileStamp, edits: edits);
server.sendResponse(
new EditOrganizeDirectivesResult(fileEdit).toResponse(request.id));
}
Future<void> sortMembers(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
var params = new EditSortMembersParams.fromRequest(request);
// prepare file
String file = params.file;
if (!engine.AnalysisEngine.isDartFileName(file)) {
server.sendResponse(new Response.sortMembersInvalidFile(request));
return;
}
// Prepare the file information.
AnalysisDriver driver = server.getAnalysisDriver(file);
ParseResult result = await driver?.parseFile(file);
if (result == null) {
server.sendResponse(new Response.fileNotAnalyzed(request, file));
return;
}
int fileStamp = -1;
String code = result.content;
CompilationUnit unit = result.unit;
List<engine.AnalysisError> errors = result.errors;
// Check if there are scan/parse errors in the file.
int numScanParseErrors = _getNumberOfScanParseErrors(errors);
if (numScanParseErrors != 0) {
server.sendResponse(
new Response.sortMembersParseErrors(request, numScanParseErrors));
return;
}
// Do sort.
MemberSorter sorter = new MemberSorter(code, unit);
List<SourceEdit> edits = sorter.sort();
SourceFileEdit fileEdit = new SourceFileEdit(file, fileStamp, edits: edits);
server.sendResponse(
new EditSortMembersResult(fileEdit).toResponse(request.id));
}
/**
* Compute and return the fixes associated with server-generated errors.
*/
Future<List<AnalysisErrorFixes>> _computeServerErrorFixes(
AnalysisDriver driver, String file, int offset) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
List<AnalysisErrorFixes> errorFixesList = <AnalysisErrorFixes>[];
AnalysisResult result = await server.getAnalysisResult(file);
if (result != null) {
CompilationUnit unit = result.unit;
LineInfo lineInfo = result.lineInfo;
int requestLine = lineInfo.getLocation(offset).lineNumber;
List<engine.AnalysisError> errorsCopy = new List.from(result.errors);
for (engine.AnalysisError error in result.errors) {
int errorLine = lineInfo.getLocation(error.offset).lineNumber;
if (errorLine == requestLine) {
AstProvider astProvider = new AstProviderForDriver(driver);
DartFixContext context = new _DartFixContextImpl(
server.resourceProvider,
result.driver,
astProvider,
unit,
error,
errorsCopy);
List<Fix> fixes =
await new DefaultFixContributor().internalComputeFixes(context);
if (fixes.isNotEmpty) {
fixes.sort(Fix.SORT_BY_RELEVANCE);
AnalysisError serverError =
newAnalysisError_fromEngine(lineInfo, error);
AnalysisErrorFixes errorFixes = new AnalysisErrorFixes(serverError);
errorFixesList.add(errorFixes);
fixes.forEach((fix) {
errorFixes.fixes.add(fix.change);
});
}
}
}
}
return errorFixesList;
}
Response _getAvailableRefactorings(Request request) {
_getAvailableRefactoringsImpl(request);
return Response.DELAYED_RESPONSE;
}
Future _getAvailableRefactoringsImpl(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
// prepare parameters
var params = new EditGetAvailableRefactoringsParams.fromRequest(request);
String file = params.file;
int offset = params.offset;
int length = params.length;
// add refactoring kinds
List<RefactoringKind> kinds = <RefactoringKind>[];
// Check nodes.
{
var analysisResult = await server.getAnalysisResult(file);
if (analysisResult != null) {
// Try EXTRACT_LOCAL_VARIABLE.
if (new ExtractLocalRefactoring(analysisResult, offset, length)
.isAvailable()) {
kinds.add(RefactoringKind.EXTRACT_LOCAL_VARIABLE);
}
// Try EXTRACT_METHOD.
if (new ExtractMethodRefactoring(searchEngine,
server.getAstProvider(file), analysisResult, offset, length)
.isAvailable()) {
kinds.add(RefactoringKind.EXTRACT_METHOD);
}
// Try EXTRACT_WIDGETS.
if (new ExtractWidgetRefactoring(
searchEngine, analysisResult, offset, length)
.isAvailable()) {
kinds.add(RefactoringKind.EXTRACT_WIDGET);
}
}
}
// check elements
{
Element element = await server.getElementAtOffset(file, offset);
if (element != null) {
// try CONVERT_METHOD_TO_GETTER
if (element is ExecutableElement) {
Refactoring refactoring = new ConvertMethodToGetterRefactoring(
searchEngine, server.getAstProvider(file), element);
RefactoringStatus status = await refactoring.checkInitialConditions();
if (!status.hasFatalError) {
kinds.add(RefactoringKind.CONVERT_METHOD_TO_GETTER);
}
}
// try RENAME
{
RenameRefactoring renameRefactoring = new RenameRefactoring(
refactoringWorkspace, server.getAstProvider(file), element);
if (renameRefactoring != null) {
kinds.add(RefactoringKind.RENAME);
}
}
}
}
// respond
var result = new EditGetAvailableRefactoringsResult(kinds);
server.sendResponse(result.toResponse(request.id));
}
Response _getRefactoring(Request request) {
if (refactoringManager.hasPendingRequest) {
refactoringManager.cancel();
_newRefactoringManager();
}
refactoringManager.getRefactoring(request);
return Response.DELAYED_RESPONSE;
}
/**
* Initializes [refactoringManager] with a new instance.
*/
void _newRefactoringManager() {
refactoringManager = new _RefactoringManager(server, refactoringWorkspace);
}
static int _getNumberOfScanParseErrors(List<engine.AnalysisError> errors) {
int numScanParseErrors = 0;
for (engine.AnalysisError error in errors) {
if (error.errorCode is engine.ScannerErrorCode ||
error.errorCode is engine.ParserErrorCode) {
numScanParseErrors++;
}
}
return numScanParseErrors;
}
}
/**
* Implementation of [DartAssistContext] that is based on the values passed
* in the constructor, as opposite to be partially based on [AssistContext].
*/
class _DartAssistContextForValues implements DartAssistContext {
@override
final Source source;
@override
final int selectionOffset;
@override
final int selectionLength;
@override
final AnalysisDriver analysisDriver;
@override
final CompilationUnit unit;
_DartAssistContextForValues(this.source, this.selectionOffset,
this.selectionLength, this.analysisDriver, this.unit);
}
/**
* And implementation of [DartFixContext].
*/
class _DartFixContextImpl implements DartFixContext {
@override
final ResourceProvider resourceProvider;
@override
final AnalysisDriver analysisDriver;
@override
final AstProvider astProvider;
@override
final CompilationUnit unit;
@override
final engine.AnalysisError error;
@override
final List<engine.AnalysisError> errors;
_DartFixContextImpl(this.resourceProvider, this.analysisDriver,
this.astProvider, this.unit, this.error, this.errors);
@override
GetTopLevelDeclarations get getTopLevelDeclarations =>
analysisDriver.getTopLevelNameDeclarations;
}
/**
* An object managing a single [Refactoring] instance.
*
* The instance is identified by its kind, file, offset and length.
* It is initialized when the a set of parameters is given for the first time.
* All subsequent requests are performed on this [Refactoring] instance.
*
* Once new set of parameters is received, the previous [Refactoring] instance
* is invalidated and a new one is created and initialized.
*/
class _RefactoringManager {
static const List<RefactoringProblem> EMPTY_PROBLEM_LIST =
const <RefactoringProblem>[];
final AnalysisServer server;
final RefactoringWorkspace refactoringWorkspace;
final SearchEngine searchEngine;
StreamSubscription subscriptionToReset;
RefactoringKind kind;
String file;
int offset;
int length;
Refactoring refactoring;
RefactoringFeedback feedback;
RefactoringStatus initStatus;
RefactoringStatus optionsStatus;
RefactoringStatus finalStatus;
Request request;
EditGetRefactoringResult result;
_RefactoringManager(this.server, this.refactoringWorkspace)
: searchEngine = refactoringWorkspace.searchEngine {
_reset();
}
/**
* Returns `true` if a response for the current request has not yet been sent.
*/
bool get hasPendingRequest => request != null;
bool get _hasFatalError {
return initStatus.hasFatalError ||
optionsStatus.hasFatalError ||
finalStatus.hasFatalError;
}
/**
* Checks if [refactoring] requires options.
*/
bool get _requiresOptions {
return refactoring is ExtractLocalRefactoring ||
refactoring is ExtractMethodRefactoring ||
refactoring is ExtractWidgetRefactoring ||
refactoring is InlineMethodRefactoring ||
refactoring is MoveFileRefactoring ||
refactoring is RenameRefactoring;
}
/**
* Cancels processing of the current request and cleans up.
*/
void cancel() {
if (request != null) {
server.sendResponse(new Response.refactoringRequestCancelled(request));
request = null;
}
_reset();
}
void getRefactoring(Request _request) {
// prepare for processing the request
request = _request;
result = new EditGetRefactoringResult(
EMPTY_PROBLEM_LIST, EMPTY_PROBLEM_LIST, EMPTY_PROBLEM_LIST);
// process the request
var params = new EditGetRefactoringParams.fromRequest(_request);
if (params.kind != null) {
server.options.analytics
?.sendEvent('refactor', params.kind.name.toLowerCase());
}
runZoned(() async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
await _init(params.kind, params.file, params.offset, params.length);
if (initStatus.hasFatalError) {
feedback = null;
_sendResultResponse();
return;
}
// set options
if (_requiresOptions) {
if (params.options == null) {
optionsStatus = new RefactoringStatus();
_sendResultResponse();
return;
}
optionsStatus = _setOptions(params);
if (_hasFatalError) {
_sendResultResponse();
return;
}
}
// done if just validation
if (params.validateOnly) {
finalStatus = new RefactoringStatus();
_sendResultResponse();
return;
}
// simulate an exception
if (test_simulateRefactoringException_final) {
throw 'A simulated refactoring exception - final.';
}
// validation and create change
finalStatus = await refactoring.checkFinalConditions();
_checkForReset_afterFinalConditions();
if (_hasFatalError) {
_sendResultResponse();
return;
}
// simulate an exception
if (test_simulateRefactoringException_change) {
throw 'A simulated refactoring exception - change.';
}
// create change
result.change = await refactoring.createChange();
result.potentialEdits = nullIfEmpty(refactoring.potentialEditIds);
_checkForReset_afterCreateChange();
_sendResultResponse();
}, onError: (exception, stackTrace) {
if (exception is _ResetError) {
cancel();
} else {
server.instrumentationService.logException(exception, stackTrace);
server.sendResponse(
new Response.serverError(_request, exception, stackTrace));
}
_reset();
});
}
void _checkForReset_afterCreateChange() {
if (test_simulateRefactoringReset_afterCreateChange) {
_reset();
}
if (refactoring == null) {
throw new _ResetError();
}
}
void _checkForReset_afterFinalConditions() {
if (test_simulateRefactoringReset_afterFinalConditions) {
_reset();
}
if (refactoring == null) {
throw new _ResetError();
}
}
void _checkForReset_afterInitialConditions() {
if (test_simulateRefactoringReset_afterInitialConditions) {
_reset();
}
if (refactoring == null) {
throw new _ResetError();
}
}
/**
* Initializes this context to perform a refactoring with the specified
* parameters. The existing [Refactoring] is reused or created as needed.
*/
Future _init(
RefactoringKind kind, String file, int offset, int length) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
// check if we can continue with the existing Refactoring instance
if (this.kind == kind &&
this.file == file &&
this.offset == offset &&
this.length == length) {
return;
}
_reset();
_resetOnAnalysisSetChanged();
this.kind = kind;
this.file = file;
this.offset = offset;
this.length = length;
// simulate an exception
if (test_simulateRefactoringException_init) {
throw 'A simulated refactoring exception - init.';
}
// create a new Refactoring instance
if (kind == RefactoringKind.CONVERT_GETTER_TO_METHOD) {
Element element = await server.getElementAtOffset(file, offset);
if (element != null) {
if (element is ExecutableElement) {
refactoring = new ConvertGetterToMethodRefactoring(
searchEngine, server.getAstProvider(file), element);
}
}
}
if (kind == RefactoringKind.CONVERT_METHOD_TO_GETTER) {
Element element = await server.getElementAtOffset(file, offset);
if (element != null) {
if (element is ExecutableElement) {
refactoring = new ConvertMethodToGetterRefactoring(
searchEngine, server.getAstProvider(file), element);
}
}
}
if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) {
var analysisResult = await server.getAnalysisResult(file);
if (analysisResult != null) {
refactoring =
new ExtractLocalRefactoring(analysisResult, offset, length);
feedback = new ExtractLocalVariableFeedback(
<String>[], <int>[], <int>[],
coveringExpressionOffsets: <int>[],
coveringExpressionLengths: <int>[]);
}
}
if (kind == RefactoringKind.EXTRACT_METHOD) {
var analysisResult = await server.getAnalysisResult(file);
if (analysisResult != null) {
refactoring = new ExtractMethodRefactoring(searchEngine,
server.getAstProvider(file), analysisResult, offset, length);
feedback = new ExtractMethodFeedback(offset, length, '', <String>[],
false, <RefactoringMethodParameter>[], <int>[], <int>[]);
}
}
if (kind == RefactoringKind.EXTRACT_WIDGET) {
var analysisResult = await server.getAnalysisResult(file);
if (analysisResult != null) {
refactoring = new ExtractWidgetRefactoring(
searchEngine, analysisResult, offset, length);
feedback = new ExtractWidgetFeedback();
}
}
if (kind == RefactoringKind.INLINE_LOCAL_VARIABLE) {
var analysisResult = await server.getAnalysisResult(file);
if (analysisResult != null) {
refactoring = new InlineLocalRefactoring(
searchEngine, server.getAstProvider(file), analysisResult, offset);
}
}
if (kind == RefactoringKind.INLINE_METHOD) {
var analysisResult = await server.getAnalysisResult(file);
if (analysisResult != null) {
refactoring = new InlineMethodRefactoring(
searchEngine, server.getAstProvider(file), analysisResult, offset);
}
}
if (kind == RefactoringKind.MOVE_FILE) {
// TODO(brianwilkerson) Re-implement this refactoring under the new analysis driver
// _resetOnAnalysisStarted();
// ContextSourcePair contextSource = server.getContextSourcePair(file);
// engine.AnalysisContext context = contextSource.context;
// Source source = contextSource.source;
// refactoring = new MoveFileRefactoring(
// server.resourceProvider, searchEngine, context, source, file);
}
if (kind == RefactoringKind.RENAME) {
AstNode node = await server.getNodeAtOffset(file, offset);
Element element = server.getElementOfNode(node);
if (node != null && element != null) {
int feedbackOffset = node.offset;
int feedbackLength = node.length;
if (element is FieldFormalParameterElement) {
element = (element as FieldFormalParameterElement).field;
}
// Use the prefix offset/length when renaming an import directive.
if (node is ImportDirective && element is ImportElement) {
if (node.prefix != null) {
feedbackOffset = node.prefix.offset;
feedbackLength = node.prefix.length;
} else {
feedbackOffset = -1;
feedbackLength = 0;
}
}
// Rename the class when on `new` in an instance creation.
if (node is InstanceCreationExpression) {
InstanceCreationExpression creation = node;
var typeIdentifier = creation.constructorName.type.name;
element = typeIdentifier.staticElement;
feedbackOffset = typeIdentifier.offset;
feedbackLength = typeIdentifier.length;
}
// do create the refactoring
refactoring = new RenameRefactoring(
refactoringWorkspace, server.getAstProvider(file), element);
feedback = new RenameFeedback(
feedbackOffset, feedbackLength, 'kind', 'oldName');
}
}
if (refactoring == null) {
initStatus =
new RefactoringStatus.fatal('Unable to create a refactoring');
return;
}
// check initial conditions
initStatus = await refactoring.checkInitialConditions();
_checkForReset_afterInitialConditions();
if (refactoring is ExtractLocalRefactoring) {
ExtractLocalRefactoring refactoring = this.refactoring;
ExtractLocalVariableFeedback feedback = this.feedback;
feedback.names = refactoring.names;
feedback.offsets = refactoring.offsets;
feedback.lengths = refactoring.lengths;
feedback.coveringExpressionOffsets =
refactoring.coveringExpressionOffsets;
feedback.coveringExpressionLengths =
refactoring.coveringExpressionLengths;
}
if (refactoring is ExtractMethodRefactoring) {
ExtractMethodRefactoring refactoring = this.refactoring;
ExtractMethodFeedback feedback = this.feedback;
feedback.canCreateGetter = refactoring.canCreateGetter;
feedback.returnType = refactoring.returnType;
feedback.names = refactoring.names;
feedback.parameters = refactoring.parameters;
feedback.offsets = refactoring.offsets;
feedback.lengths = refactoring.lengths;
}
if (refactoring is InlineLocalRefactoring) {
InlineLocalRefactoring refactoring = this.refactoring;
if (!initStatus.hasFatalError) {
feedback = new InlineLocalVariableFeedback(
refactoring.variableName, refactoring.referenceCount);
}
}
if (refactoring is InlineMethodRefactoring) {
InlineMethodRefactoring refactoring = this.refactoring;
if (!initStatus.hasFatalError) {
feedback = new InlineMethodFeedback(
refactoring.methodName, refactoring.isDeclaration,
className: refactoring.className);
}
}
if (refactoring is RenameRefactoring) {
RenameRefactoring refactoring = this.refactoring;
RenameFeedback feedback = this.feedback;
feedback.elementKindName = refactoring.elementKindName;
feedback.oldName = refactoring.oldName;
}
}
void _reset() {
test_resetCount++;
kind = null;
offset = null;
length = null;
refactoring = null;
feedback = null;
initStatus = new RefactoringStatus();
optionsStatus = new RefactoringStatus();
finalStatus = new RefactoringStatus();
subscriptionToReset?.cancel();
subscriptionToReset = null;
}
void _resetOnAnalysisSetChanged() {
subscriptionToReset?.cancel();
subscriptionToReset = server.onAnalysisSetChanged.listen((_) {
_reset();
});
}
void _sendResultResponse() {
// ignore if was cancelled
if (request == null) {
return;
}
// set feedback
result.feedback = feedback;
// set problems
result.initialProblems = initStatus.problems;
result.optionsProblems = optionsStatus.problems;
result.finalProblems = finalStatus.problems;
// send the response
server.sendResponse(result.toResponse(request.id));
// done with this request
request = null;
result = null;
}
RefactoringStatus _setOptions(EditGetRefactoringParams params) {
if (refactoring is ExtractLocalRefactoring) {
ExtractLocalRefactoring extractRefactoring = refactoring;
ExtractLocalVariableOptions extractOptions = params.options;
extractRefactoring.name = extractOptions.name;
extractRefactoring.extractAll = extractOptions.extractAll;
return extractRefactoring.checkName();
}
if (refactoring is ExtractMethodRefactoring) {
ExtractMethodRefactoring extractRefactoring = this.refactoring;
ExtractMethodOptions extractOptions = params.options;
extractRefactoring.createGetter = extractOptions.createGetter;
extractRefactoring.extractAll = extractOptions.extractAll;
extractRefactoring.name = extractOptions.name;
if (extractOptions.parameters != null) {
extractRefactoring.parameters = extractOptions.parameters;
}
extractRefactoring.returnType = extractOptions.returnType;
return extractRefactoring.checkName();
}
if (refactoring is ExtractWidgetRefactoring) {
ExtractWidgetRefactoring extractRefactoring = this.refactoring;
ExtractWidgetOptions extractOptions = params.options;
extractRefactoring.name = extractOptions.name;
return extractRefactoring.checkName();
}
if (refactoring is InlineMethodRefactoring) {
InlineMethodRefactoring inlineRefactoring = this.refactoring;
InlineMethodOptions inlineOptions = params.options;
inlineRefactoring.deleteSource = inlineOptions.deleteSource;
inlineRefactoring.inlineAll = inlineOptions.inlineAll;
return new RefactoringStatus();
}
if (refactoring is MoveFileRefactoring) {
MoveFileRefactoring moveRefactoring = this.refactoring;
MoveFileOptions moveOptions = params.options;
moveRefactoring.newFile = moveOptions.newFile;
return new RefactoringStatus();
}
if (refactoring is RenameRefactoring) {
RenameRefactoring renameRefactoring = refactoring;
RenameOptions renameOptions = params.options;
renameRefactoring.newName = renameOptions.newName;
return renameRefactoring.checkNewName();
}
return new RefactoringStatus();
}
}
/**
* [_RefactoringManager] throws instances of this class internally to stop
* processing in a manager that was reset.
*/
class _ResetError {}