| // Copyright (c) 2022, 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/src/handler/legacy/legacy_handler.dart'; | |
| import 'package:analysis_server/src/legacy_analysis_server.dart'; | |
| import 'package:analysis_server/src/plugin/result_converter.dart'; | |
| import 'package:analysis_server/src/protocol_server.dart'; | |
| import 'package:analysis_server/src/request_handler_mixin.dart'; | |
| import 'package:analysis_server/src/services/correction/fix/analysis_options/fix_generator.dart'; | |
| import 'package:analysis_server/src/services/correction/fix/pubspec/fix_generator.dart'; | |
| import 'package:analysis_server/src/services/correction/fix_performance.dart'; | |
| import 'package:analysis_server_plugin/edit/fix/dart_fix_context.dart'; | |
| import 'package:analysis_server_plugin/edit/fix/fix.dart'; | |
| import 'package:analysis_server_plugin/src/correction/dart_change_workspace.dart'; | |
| import 'package:analysis_server_plugin/src/correction/fix_processor.dart'; | |
| import 'package:analyzer/dart/analysis/results.dart'; | |
| import 'package:analyzer/dart/analysis/session.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/source/file_source.dart'; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:analyzer/src/analysis_options/analysis_options.dart'; | |
| import 'package:analyzer/src/analysis_options/analysis_options_parser.dart'; | |
| import 'package:analyzer/src/dart/analysis/results.dart' as engine; | |
| import 'package:analyzer/src/exception/exception.dart'; | |
| import 'package:analyzer/src/pubspec/pubspec_validator.dart'; | |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; | |
| import 'package:analyzer/src/workspace/pub.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; | |
| import 'package:yaml/yaml.dart'; | |
| /// The handler for the `edit.getFixes` request. | |
| class EditGetFixesHandler extends LegacyHandler | |
| with RequestHandlerMixin<LegacyAnalysisServer> { | |
| /// Initialize a newly created handler to be able to service requests for the | |
| /// [server]. | |
| new(super.server, super.request, super.cancellationToken, super.performance); | |
| @override | |
| Future<void> handle() async { | |
| var params = EditGetFixesParams.fromRequest( | |
| request, | |
| clientUriConverter: server.uriConverter, | |
| ); | |
| var file = params.file; | |
| var offset = params.offset; | |
| if (server.sendResponseErrorIfInvalidFilePath(request, file)) { | |
| return; | |
| } | |
| if (!server.isAnalyzed(file)) { | |
| server.sendResponse(Response.getFixesInvalidFile(request)); | |
| return; | |
| } | |
| // | |
| // Allow plugins to start computing fixes. | |
| // | |
| var requestParams = plugin.EditGetFixesParams(file, offset); | |
| var driver = server.getAnalysisDriver(file); | |
| var pluginFutures = server.broadcastRequestToPlugins(requestParams, driver); | |
| // | |
| // Compute fixes associated with server-generated errors. | |
| // | |
| List<AnalysisErrorFixes>? errorFixesList; | |
| while (errorFixesList == null) { | |
| try { | |
| errorFixesList = await _computeServerErrorFixes(request, file, offset); | |
| } on InconsistentAnalysisException { | |
| // Loop around to try again to compute the fixes. | |
| } | |
| } | |
| // | |
| // Add the fixes produced by plugins to the server-generated fixes. | |
| // | |
| var responses = await waitForResponses( | |
| pluginFutures, | |
| requestParameters: requestParams, | |
| ); | |
| server.requestStatistics?.addItemTimeNow(request, 'pluginResponses'); | |
| var converter = ResultConverter(); | |
| for (var response in responses) { | |
| var result = plugin.EditGetFixesResult.fromResponse(response); | |
| errorFixesList.addAll( | |
| result.fixes.map(converter.convertAnalysisErrorFixes), | |
| ); | |
| } | |
| // | |
| // Send the response. | |
| // | |
| sendResult(EditGetFixesResult(errorFixesList)); | |
| } | |
| /// Compute and return the fixes associated with server-generated errors in | |
| /// analysis options files. | |
| Future<List<AnalysisErrorFixes>> _computeAnalysisOptionsFixes( | |
| String file, | |
| int offset, | |
| ) async { | |
| var errorFixesList = <AnalysisErrorFixes>[]; | |
| var resourceProvider = server.resourceProvider; | |
| var optionsFile = resourceProvider.getFile(file); | |
| var driver = server.getAnalysisDriver(file); | |
| if (driver == null) { | |
| return errorFixesList; | |
| } | |
| await driver.applyPendingFileChanges(); | |
| var session = driver.currentSession; | |
| var sourceFactory = driver.sourceFactory; | |
| var analysisContext = session.analysisContext; | |
| var package = analysisContext.contextRoot.workspace.findPackageFor( | |
| optionsFile.path, | |
| ); | |
| var sdkVersionConstraint = (package is PubPackage) | |
| ? package.sdkVersionConstraint | |
| : null; | |
| var parseResult = AnalysisOptionsParseSession().parse( | |
| sourceFactory: sourceFactory, | |
| contextRoot: analysisContext.contextRoot.root, | |
| file: optionsFile, | |
| sdkVersionConstraint: sdkVersionConstraint, | |
| ); | |
| var fileContent = parseResult.content; | |
| var yamlMap = fileContent?.yamlMap; | |
| if (fileContent == null || yamlMap == null) { | |
| return errorFixesList; | |
| } | |
| var lineInfo = fileContent.lineInfo; | |
| var diagnostics = parseResult.diagnostics; | |
| for (var diagnostic in diagnostics) { | |
| var generator = AnalysisOptionsFixGenerator( | |
| resourceProvider, | |
| diagnostic, | |
| fileContent.text, | |
| yamlMap, | |
| ); | |
| var fixes = await generator.computeFixes(); | |
| if (fixes.isNotEmpty) { | |
| fixes.sort(Fix.compareFixes); | |
| // Options are not used in the analysis *of options* so associating | |
| // an empty set is accurate if not ideal. | |
| var analysisOptions = AnalysisOptionsImpl(); | |
| var result = engine.ErrorsResultImpl( | |
| session: session, | |
| file: optionsFile, | |
| content: fileContent.text, | |
| uri: optionsFile.toUri(), | |
| lineInfo: lineInfo, | |
| isLibrary: true, | |
| isPart: false, | |
| diagnostics: diagnostics, | |
| analysisOptions: analysisOptions, | |
| ); | |
| var serverError = newAnalysisError_fromEngine(result, diagnostic); | |
| var errorFixes = AnalysisErrorFixes(serverError); | |
| errorFixesList.add(errorFixes); | |
| for (var fix in fixes) { | |
| errorFixes.fixes.add(fix.change); | |
| } | |
| } | |
| } | |
| return errorFixesList; | |
| } | |
| /// Compute and return the fixes associated with server-generated errors in | |
| /// Dart files. | |
| Future<List<AnalysisErrorFixes>> _computeDartFixes( | |
| Request request, | |
| String file, | |
| int offset, | |
| ) async { | |
| var errorFixesList = <AnalysisErrorFixes>[]; | |
| var libraryResult = await server.getResolvedLibrary(file); | |
| server.requestStatistics?.addItemTimeNow(request, 'resolvedUnit'); | |
| if (libraryResult != null) { | |
| var unitResult = libraryResult.unitWithPath(file)!; | |
| var lineInfo = unitResult.lineInfo; | |
| var requestLine = lineInfo.getLocation(offset).lineNumber; | |
| for (var diagnostic in unitResult.diagnostics) { | |
| var errorLine = lineInfo.getLocation(diagnostic.offset).lineNumber; | |
| if (errorLine == requestLine) { | |
| var workspace = DartChangeWorkspace(await server.currentSessions); | |
| var context = DartFixContext( | |
| instrumentationService: server.instrumentationService, | |
| workspace: workspace, | |
| libraryResult: libraryResult, | |
| unitResult: unitResult, | |
| error: diagnostic, | |
| ); | |
| List<Fix> fixes; | |
| try { | |
| var performanceTracker = FixPerformance(performance); | |
| fixes = await computeFixes( | |
| context, | |
| performance: performanceTracker, | |
| ); | |
| server.recentPerformance.getFixes.add( | |
| GetFixesPerformance( | |
| performance: performance, | |
| path: file, | |
| content: unitResult.content, | |
| offset: offset, | |
| requestLatency: performanceTracker.computeTime!.inMilliseconds, | |
| producerTimings: performanceTracker.producerTimings, | |
| ), | |
| ); | |
| } on InconsistentAnalysisException { | |
| fixes = []; | |
| } catch (exception, stackTrace) { | |
| var parametersFile = | |
| ''' | |
| offset: $offset | |
| error: $diagnostic | |
| error.errorCode: ${diagnostic.diagnosticCode} | |
| '''; | |
| throw CaughtExceptionWithFiles(exception, stackTrace, { | |
| file: unitResult.content, | |
| 'parameters': parametersFile, | |
| }); | |
| } | |
| if (fixes.isNotEmpty) { | |
| fixes.sort(Fix.compareFixes); | |
| var serverError = newAnalysisError_fromEngine( | |
| unitResult, | |
| diagnostic, | |
| ); | |
| var errorFixes = AnalysisErrorFixes(serverError); | |
| errorFixesList.add(errorFixes); | |
| for (var fix in fixes) { | |
| errorFixes.fixes.add(fix.change); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| server.requestStatistics?.addItemTimeNow(request, 'computedFixes'); | |
| return errorFixesList; | |
| } | |
| /// Compute and return the fixes associated with server-generated errors in | |
| /// pubspec.yaml files. | |
| Future<List<AnalysisErrorFixes>> _computePubspecFixes( | |
| String file, | |
| int offset, | |
| ) async { | |
| var errorFixesList = <AnalysisErrorFixes>[]; | |
| var resourceProvider = server.resourceProvider; | |
| var pubspecFile = resourceProvider.getFile(file); | |
| var content = _safelyRead(pubspecFile); | |
| if (content == null) { | |
| return errorFixesList; | |
| } | |
| var session = await server.getAnalysisSession(file); | |
| if (session == null) { | |
| return errorFixesList; | |
| } | |
| YamlNode node; | |
| try { | |
| node = loadYamlNode(content, sourceUrl: pubspecFile.toUri()); | |
| } catch (exception) { | |
| return errorFixesList; | |
| } | |
| if (node is! YamlMap) { | |
| return errorFixesList; | |
| } | |
| var fileResult = session.getFile(pubspecFile.path); | |
| if (fileResult is! FileResult) { | |
| return errorFixesList; | |
| } | |
| var analysisOptions = fileResult.analysisOptions; | |
| var diagnostics = validatePubspec( | |
| contents: node, | |
| source: FileSource(pubspecFile), | |
| provider: resourceProvider, | |
| analysisOptions: analysisOptions, | |
| ); | |
| for (var diagnostic in diagnostics) { | |
| var generator = PubspecFixGenerator( | |
| resourceProvider, | |
| diagnostic, | |
| content, | |
| node, | |
| defaultEol: server.defaultEol, | |
| ); | |
| var fixes = await generator.computeFixes(); | |
| if (fixes.isNotEmpty) { | |
| fixes.sort(Fix.compareFixes); | |
| var lineInfo = LineInfo.fromContent(content); | |
| // TODO(pq): package:analyzer results are specific to *.dart files and we | |
| // shouldn't use them to represent errors in non-Dart files. | |
| // see: https://dart-review.googlesource.com/c/sdk/+/333588 | |
| var result = engine.ErrorsResultImpl( | |
| session: session, | |
| file: pubspecFile, | |
| content: content, | |
| uri: pubspecFile.toUri(), | |
| lineInfo: lineInfo, | |
| isLibrary: true, | |
| isPart: false, | |
| diagnostics: diagnostics, | |
| analysisOptions: analysisOptions, | |
| ); | |
| var serverError = newAnalysisError_fromEngine(result, diagnostic); | |
| var errorFixes = AnalysisErrorFixes(serverError); | |
| errorFixesList.add(errorFixes); | |
| for (var fix in fixes) { | |
| errorFixes.fixes.add(fix.change); | |
| } | |
| } | |
| } | |
| return errorFixesList; | |
| } | |
| /// Compute and return the fixes associated with server-generated errors. | |
| Future<List<AnalysisErrorFixes>> _computeServerErrorFixes( | |
| Request request, | |
| String file, | |
| int offset, | |
| ) async { | |
| var pathContext = server.resourceProvider.pathContext; | |
| if (file_paths.isDart(pathContext, file)) { | |
| return _computeDartFixes(request, file, offset); | |
| } else if (file_paths.isAnalysisOptionsYaml(pathContext, file)) { | |
| return _computeAnalysisOptionsFixes(file, offset); | |
| } else if (file_paths.isPubspecYaml(pathContext, file)) { | |
| return _computePubspecFixes(file, offset); | |
| } | |
| return <AnalysisErrorFixes>[]; | |
| } | |
| /// Return the contents of the [file], or `null` if the file does not exist or | |
| /// cannot be read. | |
| String? _safelyRead(File file) { | |
| try { | |
| return file.readAsStringSync(); | |
| } on FileSystemException { | |
| return null; | |
| } | |
| } | |
| } |