// Copyright (c) 2015, 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 analyzer.task.model; import 'dart:collection'; import 'dart:developer'; import 'package:analyzer/error/error.dart' show AnalysisError; import 'package:analyzer/exception/exception.dart'; import 'package:analyzer/src/generated/engine.dart'; import 'package:analyzer/src/generated/source.dart'; import 'package:analyzer/src/generated/utilities_general.dart'; import 'package:analyzer/src/task/driver.dart'; import 'package:analyzer/src/task/model.dart'; import 'package:front_end/src/base/analysis_target.dart'; export 'package:front_end/src/base/analysis_target.dart' show AnalysisTarget; /** * A function that converts the given [key] and [value] into a [TaskInput]. */ typedef TaskInput BinaryFunction(K key, V value); /** * A function that takes an analysis [context] and an analysis [target] and * returns an analysis task. Such functions are passed to a [TaskDescriptor] to * be used to create the described task. */ typedef AnalysisTask BuildTask(AnalysisContext context, AnalysisTarget target); /** * A function that takes the target for which a task will produce results and * returns a map from input names to descriptions of the analysis results needed * by the task in order for the task to be performed. Such functions are passed * to a [TaskDescriptor] to be used to determine the inputs needed by the task. */ typedef Map CreateTaskInputs(AnalysisTarget target); /** * A function that takes the target for which a task will produce results and * returns an indication of how suitable the task is for the target. Such * functions are passed to a [TaskDescriptor] to be used to determine their * suitability for computing results. */ typedef TaskSuitability SuitabilityFor(AnalysisTarget target); /** * A function that converts an object of the type [B] into a [TaskInput]. * This is used, for example, by a [ListTaskInput] to create task inputs * for each value in a list of values. */ typedef TaskInput UnaryFunction(B object); /** * An [AnalysisTarget] wrapper for an [AnalysisContext]. */ class AnalysisContextTarget implements AnalysisTarget { static final AnalysisContextTarget request = new AnalysisContextTarget(null); final AnalysisContext context; AnalysisContextTarget(this.context); @override Source get librarySource => null; @override Source get source => null; } /** * An object used to compute one or more analysis results associated with a * single target. * * Clients must extend this class when creating new tasks. */ abstract class AnalysisTask { /** * A queue storing the last 10 task descriptions for diagnostic purposes. */ static final LimitedQueue LAST_TASKS = new LimitedQueue(10); /** * A table mapping the types of analysis tasks to the number of times each * kind of task has been performed. */ static final Map countMap = new HashMap(); /** * A table mapping the types of analysis tasks to user tags used to collect * timing data for the Observatory. */ static Map tagMap = new HashMap(); /** * A table mapping the types of analysis tasks to stopwatches used to compute * how much time was spent executing each kind of task. */ static final Map stopwatchMap = new HashMap(); /** * The context in which the task is to be performed. */ final AnalysisContext context; /** * The target for which result values are being produced. */ final AnalysisTarget target; /** * A table mapping input names to input values. */ Map inputs; /** * A table mapping result descriptors whose values are produced by this task * to the values that were produced. */ Map outputs = new HashMap(); /** * The exception that was thrown while performing this task, or `null` if the * task completed successfully. */ CaughtException caughtException; /** * If a dependency cycle was found while computing the inputs for the task, * the set of [WorkItem]s contained in the cycle (if there are overlapping * cycles, this is the set of all [WorkItem]s in the entire strongly * connected component). Otherwise, `null`. */ List dependencyCycle; /** * Initialize a newly created task to perform analysis within the given * [context] in order to produce results for the given [target]. */ AnalysisTask(this.context, this.target); /** * Return a textual description of this task. */ String get description; /** * Return the descriptor that describes this task. */ TaskDescriptor get descriptor; /** * Indicates whether the task is capable of handling dependency cycles. A * task that overrides this getter to return `true` must be prepared for the * possibility that it will be invoked with a non-`null` value of * [dependencyCycle], and with not all of its inputs computed. */ bool get handlesDependencyCycles => false; /** * Return the value of the input with the given [name], or `null` if the input * value is not defined. */ Object getOptionalInput(String name) { if (inputs == null || !inputs.containsKey(name)) { return null; } return inputs[name]; } /** * Return the value of the input with the given [name]. Throw an exception if * the input value is not defined. */ Object/*=E*/ getRequiredInput/**/(String name) { if (inputs == null || !inputs.containsKey(name)) { throw new AnalysisException("Could not $description: missing $name"); } return inputs[name] as Object/*=E*/; } /** * Return the source associated with the target. Throw an exception if * the target is not associated with a source. */ Source getRequiredSource() { Source source = target.source; if (source == null) { throw new AnalysisException("Could not $description: missing source"); } return source; } /** * Perform this analysis task, protected by an exception handler. * * This method should throw an [AnalysisException] if an exception occurs * while performing the task. If other kinds of exceptions are thrown they * will be wrapped in an [AnalysisException]. * * If no exception is thrown, this method must fully populate the [outputs] * map (have a key/value pair for each result that this task is expected to * produce). */ void internalPerform(); /** * Perform this analysis task. When this method returns, either the [outputs] * map should be fully populated (have a key/value pair for each result that * this task is expected to produce) or the [caughtException] should be set. * * Clients may not override this method. */ void perform() { try { _safelyPerform(); } on AnalysisException catch (exception, stackTrace) { caughtException = new CaughtException(exception, stackTrace); AnalysisEngine.instance.logger .logError("Task failed: $description", caughtException); } } @override String toString() => description; /** * Given a strongly connected component, find and return a list of * [TargetedResult]s that describes a cyclic path within the cycle. Returns * null if no cyclic path is found. */ List _findCyclicPath(List cycle) { WorkItem findInCycle(AnalysisTarget target, ResultDescriptor descriptor) { for (WorkItem item in cycle) { if (target == item.target && descriptor == item.spawningResult) { return item; } } return null; } HashSet active = new HashSet(); List path = null; bool traverse(WorkItem item) { if (!active.add(item)) { // We've found a cycle path = []; return true; } for (TargetedResult result in item.inputTargetedResults) { WorkItem item = findInCycle(result.target, result.result); // Ignore edges that leave the cycle. if (item != null) { if (traverse(item)) { // This edge is in a cycle (or leads to a cycle) so add it to the // path path.add(result); return true; } } } // There was no cycle. return false; } if (cycle.length > 0) { traverse(cycle[0]); } return path; } /** * Perform this analysis task, ensuring that all exceptions are wrapped in an * [AnalysisException]. * * Clients may not override this method. */ void _safelyPerform() { try { // // Store task description for diagnostics. // LAST_TASKS.add(description); // // Report that this task is being performed. // String contextName = context.name; if (contextName == null) { contextName = 'unnamed'; } AnalysisEngine.instance.instrumentationService .logAnalysisTask(contextName, this); // // Gather statistics on the performance of the task. // int count = countMap[runtimeType]; countMap[runtimeType] = count == null ? 1 : count + 1; // UserTag tag = tagMap.putIfAbsent( // runtimeType, () => new UserTag(runtimeType.toString())); Stopwatch stopwatch = stopwatchMap[runtimeType]; if (stopwatch == null) { stopwatch = new Stopwatch(); stopwatchMap[runtimeType] = stopwatch; } // UserTag previousTag = tag.makeCurrent(); // try { stopwatch.start(); // // Actually perform the task. // try { if (dependencyCycle != null && !handlesDependencyCycles) { throw new InfiniteTaskLoopException( this, dependencyCycle, _findCyclicPath(dependencyCycle)); } internalPerform(); } finally { stopwatch.stop(); } // } finally { // previousTag.makeCurrent(); // } } on AnalysisException { rethrow; } on ModificationTimeMismatchError { rethrow; } catch (exception, stackTrace) { throw new AnalysisException( 'Unexpected exception while performing $description', new CaughtException(exception, stackTrace)); } } } /** * A description of a [List]-based analysis result that can be computed by an * [AnalysisTask]. * * Clients may not extend, implement or mix-in this class. */ abstract class ListResultDescriptor implements ResultDescriptor> { /** * Initialize a newly created analysis result to have the given [name] and * [defaultValue]. If a [cachingPolicy] is provided, it will control how long * values associated with this result will remain in the cache. */ factory ListResultDescriptor(String name, List defaultValue, {ResultCachingPolicy> cachingPolicy}) = ListResultDescriptorImpl; @override ListTaskInput of(AnalysisTarget target, {bool flushOnAccess: false}); } /** * A description of an input to an [AnalysisTask] that can be used to compute * that input. * * Clients may not extend, implement or mix-in this class. */ abstract class ListTaskInput implements TaskInput> { /** * Return a task input that can be used to compute a flatten list whose * elements are combined [subListResult]'s associated with those elements. */ ListTaskInput/**/ toFlattenListOf/**/( ListResultDescriptor/**/ subListResult); /** * Return a task input that can be used to compute a list whose elements are * the result of passing the elements of this input to the [mapper] function. */ ListTaskInput/**/ toList/**/(UnaryFunction mapper); /** * Return a task input that can be used to compute a list whose elements are * [valueResult]'s associated with those elements. */ ListTaskInput/**/ toListOf/**/(ResultDescriptor/**/ valueResult); /** * Return a task input that can be used to compute a map whose keys are the * elements of this input and whose values are the result of passing the * corresponding key to the [mapper] function. */ MapTaskInput toMap/**/( UnaryFunction mapper); /** * Return a task input that can be used to compute a map whose keys are the * elements of this input and whose values are the [valueResult]'s associated * with those elements. */ MapTaskInput toMapOf/**/( ResultDescriptor/**/ valueResult); } /** * A description of an input with a [Map] based values. * * Clients may not extend, implement or mix-in this class. */ abstract class MapTaskInput implements TaskInput> { /** * [V] must be a [List]. * Return a task input that can be used to compute a list whose elements are * the result of passing keys [K] and the corresponding elements of [V] to * the [mapper] function. */ TaskInput*/ > toFlattenList/**/( BinaryFunction mapper); } /** * Instances of this class are thrown when a task detects that the modification * time of a cache entry is not the same as the actual modification time. This * means that any analysis results based on the content of the target cannot be * used anymore and must be invalidated. */ class ModificationTimeMismatchError { final Source source; ModificationTimeMismatchError(this.source); } /** * A policy object that can compute sizes of results and provide the maximum * active and idle sizes that can be kept in the cache. * * All the [ResultDescriptor]s with the same [ResultCachingPolicy] instance * share the same total size in a cache. * * Clients may implement this class when implementing plugins. */ abstract class ResultCachingPolicy { /** * Return the maximum total size of results that can be kept in the cache * while analysis is in progress. */ int get maxActiveSize; /** * Return the maximum total size of results that can be kept in the cache * while analysis is idle. */ int get maxIdleSize; /** * Return the size of the given [object]. */ int measure(T object); } /** * A description of an analysis result that can be computed by an [AnalysisTask]. * * Clients may not extend, implement or mix-in this class. */ abstract class ResultDescriptor { /** * A comparator that can be used to sort result descriptors by their name. */ static final Comparator SORT_BY_NAME = (ResultDescriptor first, ResultDescriptor second) => first.name.compareTo(second.name); /** * Initialize a newly created analysis result to have the given [name] and * [defaultValue]. * * The given [cachingPolicy] is used to limit the total size of results * described by this descriptor. If no policy is specified, the results are * never evicted from the cache, and removed only when they are invalidated. */ factory ResultDescriptor(String name, V defaultValue, {ResultCachingPolicy cachingPolicy}) = ResultDescriptorImpl; /** * Return the caching policy for results described by this descriptor. */ ResultCachingPolicy get cachingPolicy; /** * Return the default value for results described by this descriptor. */ V get defaultValue; /** * Return the name of this descriptor. */ String get name; /** * Return a task input that can be used to compute this result for the given * [target]. If [flushOnAccess] is `true` then the value of this result that * is associated with the [target] will be flushed when it is accessed. */ TaskInput of(AnalysisTarget target, {bool flushOnAccess: false}); } /** * A specification of the given [result] for the given [target]. * * Clients may not extend, implement or mix-in this class. */ class TargetedResult { /** * An empty list of results. */ static final List EMPTY_LIST = const []; /** * The target with which the result is associated. */ final AnalysisTarget target; /** * The result associated with the target. */ final ResultDescriptor result; /** * Initialize a new targeted result. */ TargetedResult(this.target, this.result); @override int get hashCode { return JenkinsSmiHash.combine(target.hashCode, result.hashCode); } @override bool operator ==(other) { return other is TargetedResult && other.target == target && other.result == result; } @override String toString() => '$result for $target'; } /** * A description of an [AnalysisTask]. * * Clients may not extend, implement or mix-in this class. */ abstract class TaskDescriptor { /** * Initialize a newly created task descriptor to have the given [name] and to * describe a task that takes the inputs built using the given [inputBuilder], * and produces the given [results]. The [buildTask] function will be used to * create the instance of [AnalysisTask] being described. If provided, the * [isAppropriateFor] function will be used to determine whether the task can * be used on a specific target. */ factory TaskDescriptor(String name, BuildTask buildTask, CreateTaskInputs inputBuilder, List results, {SuitabilityFor suitabilityFor}) = TaskDescriptorImpl; /** * Return the builder used to build the inputs to the task. */ CreateTaskInputs get createTaskInputs; /** * Return the name of the task being described. */ String get name; /** * Return a list of the analysis results that will be computed by this task. */ List get results; /** * Create and return a task that is described by this descriptor that can be * used to compute results based on the given [inputs]. */ AnalysisTask createTask(AnalysisContext context, AnalysisTarget target, Map inputs); /** * Return an indication of how suitable this task is for the given [target]. */ TaskSuitability suitabilityFor(AnalysisTarget target); } /** * A description of an input to an [AnalysisTask] that can be used to compute * that input. * * Clients may not extend, implement or mix-in this class. */ abstract class TaskInput { /** * Create and return a builder that can be used to build this task input. */ TaskInputBuilder createBuilder(); /** * Return a task input that can be used to compute a list whose elements are * the result of passing the result of this input to the [mapper] function. */ ListTaskInput/**/ mappedToList/**/(List/**/ mapper(V value)); } /** * An object used to build the value associated with a single [TaskInput]. * * All builders work by requesting one or more results (each result being * associated with a target). The interaction pattern is modeled after the class * [Iterator], in which the method [moveNext] is invoked to move from one result * request to the next. The getters [currentResult] and [currentTarget] are used * to get the result and target of the current request. The value of the result * must be supplied using the [currentValue] setter before [moveNext] can be * invoked to move to the next request. When [moveNext] returns `false`, * indicating that there are no more requests, the method [inputValue] can be * used to access the value of the input that was built. * * Clients may not extend, implement or mix-in this class. */ abstract class TaskInputBuilder { /** * Return the result that needs to be computed, or `null` if [moveNext] has * not been invoked or if the last invocation of [moveNext] returned `false`. */ ResultDescriptor get currentResult; /** * Return the target for which the result needs to be computed, or `null` if * [moveNext] has not been invoked or if the last invocation of [moveNext] * returned `false`. */ AnalysisTarget get currentTarget; /** * Set the [value] that was computed for the current result. * * Throws a [StateError] if [moveNext] has not been invoked or if the last * invocation of [moveNext] returned `false`. */ void set currentValue(Object value); /** * Return `true` if the value accessed by this input builder should be flushed * from the cache at the time it is retrieved. */ bool get flushOnAccess; /** * Return the [value] that was computed by this builder. * * Throws a [StateError] if [moveNext] has not been invoked or if the last * invocation of [moveNext] returned `true`. * * Returns `null` if no value could be computed due to a circular dependency. */ V get inputValue; /** * Record that no value is available for the current result, due to a * circular dependency. * * Throws a [StateError] if [moveNext] has not been invoked or if the last * invocation of [moveNext] returned `false`. */ void currentValueNotAvailable(); /** * Move to the next result that needs to be computed in order to build the * inputs for a task. Return `true` if there is another result that needs to * be computed, or `false` if the inputs have been computed. * * It is safe to invoke [moveNext] after it has returned `false`. In this case * [moveNext] has no effect and will again return `false`. * * Throws a [StateError] if the value of the current result has not been * provided using [currentValue]. */ bool moveNext(); } /** * An indication of how suitable a task is for a given target. */ enum TaskSuitability { NONE, LOWEST, HIGHEST } /** * [WorkManager]s are used to drive analysis. * * They know specific of the targets and results they care about, * so they can request analysis results in optimal order. * * Clients may implement this class when implementing plugins. */ abstract class WorkManager { /** * Notifies the manager about changes in the explicit source list. */ void applyChange(List addedSources, List changedSources, List removedSources); /** * Notifies the managers that the given set of priority [targets] was set. */ void applyPriorityTargets(List targets); /** * Return a list of all of the errors associated with the given [source]. * The list of errors will be empty if the source is not known to the context * or if there are no errors in the source. The errors contained in the list * can be incomplete. */ List getErrors(Source source); /** * Return the next [TargetedResult] that this work manager wants to be * computed, or `null` if this manager doesn't need any new results. * * Note, that it is not guaranteed that this result will be computed, it is * up to the work manager to check whether the result is already computed * (for example during the next [getNextResult] invocation) or computation * of the same result should be requested again. */ TargetedResult getNextResult(); /** * Return the priority if the next work order this work manager want to be * computed. The [AnalysisDriver] will perform the work order with * the highest priority. * * Even if the returned value is [WorkOrderPriority.NONE], it still does not * guarantee that [getNextResult] will return not `null`. */ WorkOrderPriority getNextResultPriority(); /** * Notifies the manager about analysis options changes. */ void onAnalysisOptionsChanged(); /** * Notifies the manager about [SourceFactory] changes. */ void onSourceFactoryChanged(); /** * Notifies the manager that the given [outputs] were produced for * the given [target]. */ void resultsComputed( AnalysisTarget target, Map outputs); } /** * The priorities of work orders returned by [WorkManager]s. * * New priorities may be added with time, clients need to tolerate this. */ enum WorkOrderPriority { /** * Responding to an user's action. */ INTERACTIVE, /** * Computing information for priority sources. */ PRIORITY, /** * A work should be done, but without any special urgency. */ NORMAL, /** * Nothing to do. */ NONE }