| // Copyright (c) 2017, 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:collection'; |
| |
| import 'package:analysis_server/protocol/protocol_generated.dart' as server; |
| import 'package:analysis_server/src/channel/channel.dart'; |
| import 'package:analysis_server/src/plugin/result_collector.dart'; |
| import 'package:analysis_server/src/plugin/result_converter.dart'; |
| import 'package:analysis_server/src/plugin/result_merger.dart'; |
| import 'package:analyzer_plugin/protocol/protocol.dart' as plugin; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_constants.dart' as plugin; |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; |
| import 'package:path/path.dart'; |
| |
| /// The object used to coordinate the results of notifications from the analysis |
| /// server and multiple plugins. |
| abstract class AbstractNotificationManager { |
| /// The identifier used to identify results from the server. |
| static const String serverId = 'server'; |
| |
| /// The path context. |
| final Context pathContext; |
| |
| /// A list of the paths of files and directories that are included for |
| /// analysis. |
| List<String> includedPaths = <String>[]; |
| |
| /// A list of the paths of files and directories that are excluded from |
| /// analysis. |
| List<String> excludedPaths = <String>[]; |
| |
| /// The current set of subscriptions to which the client has subscribed. |
| Map<server.AnalysisService, Set<String>> currentSubscriptions = |
| <server.AnalysisService, Set<String>>{}; |
| |
| /// The collector being used to collect the analysis errors from the plugins. |
| // TODO(brianwilkerson) Consider the possibility of not passing the predicate |
| // in to the collector, but instead to the testing in this class. |
| late ResultCollector<List<AnalysisError>> errors = |
| ResultCollector<List<AnalysisError>>(serverId, predicate: _isIncluded); |
| |
| /// The collector being used to collect the folding regions from the plugins. |
| ResultCollector<List<FoldingRegion>> folding; |
| |
| /// The collector being used to collect the highlight regions from the |
| /// plugins. |
| ResultCollector<List<HighlightRegion>> highlights; |
| |
| /// The collector being used to collect the navigation parameters from the |
| /// plugins. |
| ResultCollector<server.AnalysisNavigationParams> navigation; |
| |
| /// The collector being used to collect the occurrences from the plugins. |
| ResultCollector<List<Occurrences>> occurrences; |
| |
| /// The collector being used to collect the outlines from the plugins. |
| ResultCollector<List<Outline>> outlines; |
| |
| /// The object used to convert results. |
| final ResultConverter converter = ResultConverter(); |
| |
| /// The object used to merge results. |
| final ResultMerger merger = ResultMerger(); |
| |
| /// Initialize a newly created notification manager. |
| AbstractNotificationManager(this.pathContext) |
| : |
| // errors = |
| // ResultCollector<List<AnalysisError>>(serverId, predicate: _isIncluded), |
| folding = ResultCollector<List<FoldingRegion>>(serverId), |
| highlights = ResultCollector<List<HighlightRegion>>(serverId), |
| navigation = ResultCollector<server.AnalysisNavigationParams>(serverId), |
| occurrences = ResultCollector<List<Occurrences>>(serverId), |
| outlines = ResultCollector<List<Outline>>(serverId); |
| |
| /// Handle the given [notification] from the plugin with the given [pluginId]. |
| void handlePluginNotification( |
| String pluginId, plugin.Notification notification) { |
| var event = notification.event; |
| switch (event) { |
| case plugin.ANALYSIS_NOTIFICATION_ERRORS: |
| var params = plugin.AnalysisErrorsParams.fromNotification(notification); |
| recordAnalysisErrors(pluginId, params.file, params.errors); |
| break; |
| case plugin.ANALYSIS_NOTIFICATION_FOLDING: |
| var params = |
| plugin.AnalysisFoldingParams.fromNotification(notification); |
| recordFoldingRegions(pluginId, params.file, params.regions); |
| break; |
| case plugin.ANALYSIS_NOTIFICATION_HIGHLIGHTS: |
| var params = |
| plugin.AnalysisHighlightsParams.fromNotification(notification); |
| recordHighlightRegions(pluginId, params.file, params.regions); |
| break; |
| case plugin.ANALYSIS_NOTIFICATION_NAVIGATION: |
| var params = |
| plugin.AnalysisNavigationParams.fromNotification(notification); |
| recordNavigationParams(pluginId, params.file, |
| converter.convertAnalysisNavigationParams(params)); |
| break; |
| case plugin.ANALYSIS_NOTIFICATION_OCCURRENCES: |
| var params = |
| plugin.AnalysisOccurrencesParams.fromNotification(notification); |
| recordOccurrences(pluginId, params.file, params.occurrences); |
| break; |
| case plugin.ANALYSIS_NOTIFICATION_OUTLINE: |
| var params = |
| plugin.AnalysisOutlineParams.fromNotification(notification); |
| recordOutlines(pluginId, params.file, params.outline); |
| break; |
| case plugin.PLUGIN_NOTIFICATION_ERROR: |
| sendPluginErrorNotification(notification); |
| break; |
| } |
| } |
| |
| /// Record error information from the plugin with the given [pluginId] for the |
| /// file with the given [filePath]. |
| void recordAnalysisErrors( |
| String pluginId, String filePath, List<AnalysisError> errorData) { |
| if (errors.isCollectingFor(filePath)) { |
| errors.putResults(filePath, pluginId, errorData); |
| var unmergedErrors = errors.getResults(filePath); |
| var mergedErrors = merger.mergeAnalysisErrors(unmergedErrors); |
| sendAnalysisErrors(filePath, mergedErrors); |
| } |
| } |
| |
| /// Record folding information from the plugin with the given [pluginId] for |
| /// the file with the given [filePath]. |
| void recordFoldingRegions( |
| String pluginId, String filePath, List<FoldingRegion> foldingData) { |
| if (folding.isCollectingFor(filePath)) { |
| folding.putResults(filePath, pluginId, foldingData); |
| var unmergedFolding = folding.getResults(filePath); |
| var mergedFolding = merger.mergeFoldingRegions(unmergedFolding); |
| sendFoldingRegions(filePath, mergedFolding); |
| } |
| } |
| |
| /// Record highlight information from the plugin with the given [pluginId] for |
| /// the file with the given [filePath]. |
| void recordHighlightRegions( |
| String pluginId, String filePath, List<HighlightRegion> highlightData) { |
| if (highlights.isCollectingFor(filePath)) { |
| highlights.putResults(filePath, pluginId, highlightData); |
| var unmergedHighlights = highlights.getResults(filePath); |
| var mergedHighlights = merger.mergeHighlightRegions(unmergedHighlights); |
| sendHighlightRegions(filePath, mergedHighlights); |
| } |
| } |
| |
| /// Record navigation information from the plugin with the given [pluginId] |
| /// for the file with the given [filePath]. |
| void recordNavigationParams(String pluginId, String filePath, |
| server.AnalysisNavigationParams navigationData) { |
| if (navigation.isCollectingFor(filePath)) { |
| navigation.putResults(filePath, pluginId, navigationData); |
| var unmergedNavigations = navigation.getResults(filePath); |
| var mergedNavigations = merger.mergeNavigation(unmergedNavigations); |
| if (mergedNavigations != null) { |
| sendNavigations(mergedNavigations); |
| } |
| } |
| } |
| |
| /// Record occurrences information from the plugin with the given [pluginId] |
| /// for the file with the given [filePath]. |
| void recordOccurrences( |
| String pluginId, String filePath, List<Occurrences> occurrencesData) { |
| if (occurrences.isCollectingFor(filePath)) { |
| occurrences.putResults(filePath, pluginId, occurrencesData); |
| var unmergedOccurrences = occurrences.getResults(filePath); |
| var mergedOccurrences = merger.mergeOccurrences(unmergedOccurrences); |
| sendOccurrences(filePath, mergedOccurrences); |
| } |
| } |
| |
| /// Record outline information from the plugin with the given [pluginId] for |
| /// the file with the given [filePath]. |
| void recordOutlines( |
| String pluginId, String filePath, List<Outline> outlineData) { |
| if (outlines.isCollectingFor(filePath)) { |
| outlines.putResults(filePath, pluginId, outlineData); |
| var unmergedOutlines = outlines.getResults(filePath); |
| var mergedOutlines = merger.mergeOutline(unmergedOutlines); |
| sendOutlines(filePath, mergedOutlines); |
| } |
| } |
| |
| /// Sends errors for a file to the client. |
| void sendAnalysisErrors(String filePath, List<AnalysisError> mergedErrors); |
| |
| /// Sends folding regions for a file to the client. |
| void sendFoldingRegions(String filePath, List<FoldingRegion> mergedFolding); |
| |
| /// Sends highlight regions for a file to the client. |
| void sendHighlightRegions( |
| String filePath, List<HighlightRegion> mergedHighlights); |
| |
| /// Sends navigation regions for a file to the client. |
| void sendNavigations(server.AnalysisNavigationParams mergedNavigations); |
| |
| /// Sends occurrences for a file to the client. |
| void sendOccurrences(String filePath, List<Occurrences> mergedOccurrences); |
| |
| /// Sends outlines for a file to the client. |
| void sendOutlines(String filePath, List<Outline> mergedOutlines); |
| |
| /// Sends plugin errors to the client. |
| void sendPluginErrorNotification(plugin.Notification notification); |
| |
| /// Set the lists of [included] and [excluded] files. |
| void setAnalysisRoots(List<String> included, List<String> excluded) { |
| includedPaths = included; |
| excludedPaths = excluded; |
| } |
| |
| /// Set the current subscriptions to the given set of [newSubscriptions]. |
| void setSubscriptions( |
| Map<server.AnalysisService, Set<String>> newSubscriptions) { |
| /// Return the collector associated with the given service, or `null` if the |
| /// service is not handled by this manager. |
| ResultCollector? collectorFor(server.AnalysisService service) { |
| switch (service) { |
| case server.AnalysisService.FOLDING: |
| return folding; |
| case server.AnalysisService.HIGHLIGHTS: |
| return highlights; |
| case server.AnalysisService.NAVIGATION: |
| return navigation; |
| case server.AnalysisService.OCCURRENCES: |
| return occurrences; |
| case server.AnalysisService.OUTLINE: |
| return outlines; |
| } |
| return null; |
| } |
| |
| Set<server.AnalysisService> services = HashSet<server.AnalysisService>(); |
| services.addAll(currentSubscriptions.keys); |
| services.addAll(newSubscriptions.keys); |
| services.forEach((server.AnalysisService service) { |
| var collector = collectorFor(service); |
| if (collector != null) { |
| var currentPaths = currentSubscriptions[service]; |
| var newPaths = newSubscriptions[service]; |
| if (currentPaths == null) { |
| if (newPaths == null) { |
| // This should not happen. |
| return; |
| } |
| // All of the [newPaths] need to be added. |
| newPaths.forEach((String filePath) { |
| collector.startCollectingFor(filePath); |
| }); |
| } else if (newPaths == null) { |
| // All of the [currentPaths] need to be removed. |
| currentPaths.forEach((String filePath) { |
| collector.stopCollectingFor(filePath); |
| }); |
| } else { |
| // Compute the difference of the two sets. |
| newPaths.forEach((String filePath) { |
| if (!currentPaths.contains(filePath)) { |
| collector.startCollectingFor(filePath); |
| } |
| }); |
| currentPaths.forEach((String filePath) { |
| if (!newPaths.contains(filePath)) { |
| collector.stopCollectingFor(filePath); |
| } |
| }); |
| } |
| } |
| }); |
| currentSubscriptions = newSubscriptions; |
| } |
| |
| /// Return `true` if errors should be collected for the file with the given |
| /// [path] (because it is being analyzed). |
| bool _isIncluded(String path) { |
| bool isIncluded() { |
| for (var includedPath in includedPaths) { |
| if (pathContext.isWithin(includedPath, path) || |
| pathContext.equals(includedPath, path)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool isExcluded() { |
| for (var excludedPath in excludedPaths) { |
| if (pathContext.isWithin(excludedPath, path)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // TODO(brianwilkerson) Return false if error notifications are globally |
| // disabled. |
| return isIncluded() && !isExcluded(); |
| } |
| } |
| |
| class NotificationManager extends AbstractNotificationManager { |
| /// The identifier used to identify results from the server. |
| static const String serverId = AbstractNotificationManager.serverId; |
| |
| /// The channel used to send notifications to the client. |
| final ServerCommunicationChannel channel; |
| |
| /// Initialize a newly created notification manager. |
| NotificationManager(this.channel, Context pathContext) : super(pathContext); |
| |
| /// Sends errors for a file to the client. |
| @override |
| void sendAnalysisErrors(String filePath, List<AnalysisError> mergedErrors) { |
| channel.sendNotification( |
| server.AnalysisErrorsParams(filePath, mergedErrors).toNotification()); |
| } |
| |
| /// Sends folding regions for a file to the client. |
| @override |
| void sendFoldingRegions(String filePath, List<FoldingRegion> mergedFolding) { |
| channel.sendNotification( |
| server.AnalysisFoldingParams(filePath, mergedFolding).toNotification()); |
| } |
| |
| /// Sends highlight regions for a file to the client. |
| @override |
| void sendHighlightRegions( |
| String filePath, List<HighlightRegion> mergedHighlights) { |
| channel.sendNotification( |
| server.AnalysisHighlightsParams(filePath, mergedHighlights) |
| .toNotification()); |
| } |
| |
| /// Sends navigation regions for a file to the client. |
| @override |
| void sendNavigations(server.AnalysisNavigationParams mergedNavigations) { |
| channel.sendNotification(mergedNavigations.toNotification()); |
| } |
| |
| /// Sends occurrences for a file to the client. |
| @override |
| void sendOccurrences(String filePath, List<Occurrences> mergedOccurrences) { |
| channel.sendNotification( |
| server.AnalysisOccurrencesParams(filePath, mergedOccurrences) |
| .toNotification()); |
| } |
| |
| /// Sends outlines for a file to the client. |
| @override |
| void sendOutlines(String filePath, List<Outline> mergedOutlines) { |
| channel.sendNotification(server.AnalysisOutlineParams( |
| filePath, server.FileKind.LIBRARY, mergedOutlines[0]) |
| .toNotification()); |
| } |
| |
| /// Sends plugin errors to the client. |
| @override |
| void sendPluginErrorNotification(plugin.Notification notification) { |
| var params = plugin.PluginErrorParams.fromNotification(notification); |
| // TODO(brianwilkerson) There is no indication for the client as to the |
| // fact that the error came from a plugin, let alone which plugin it |
| // came from. We should consider whether we really want to send them to |
| // the client. |
| channel.sendNotification(server.ServerErrorParams( |
| params.isFatal, params.message, params.stackTrace) |
| .toNotification()); |
| } |
| } |