b10a592341
This paves the way for allowing analyzer and analysis server clients to format diagnostic message URLs in a special way (e.g. to make them clickable). Note that DiagnosticMessage is part of the public API of the analyzer, so I've retained the old behavior in a deprecated fashion to avoid breaking clients that don't yet handle diagnostic messages containing URLs. See https://dart-review.googlesource.com/c/sdk/+/193749/comment/86d1ce4b_77a60b1e/ for additional discussion. Change-Id: Iae9d43a2be7dbc67cb7cb82afe0a7824043d6113 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/196101 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
413 lines
16 KiB
Dart
413 lines
16 KiB
Dart
// 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 'package:analyzer/dart/element/element.dart' as analyzer;
|
|
import 'package:analyzer/dart/element/type.dart';
|
|
import 'package:analyzer/diagnostic/diagnostic.dart' as analyzer;
|
|
import 'package:analyzer/error/error.dart' as analyzer;
|
|
import 'package:analyzer/exception/exception.dart' as analyzer;
|
|
import 'package:analyzer/source/error_processor.dart' as analyzer;
|
|
import 'package:analyzer/src/generated/engine.dart' as analyzer;
|
|
import 'package:analyzer/src/generated/source.dart' as analyzer;
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
|
|
|
|
/// An object used to convert between objects defined by the 'analyzer' package
|
|
/// and those defined by the plugin protocol.
|
|
///
|
|
/// Clients may not extend, implement or mix-in this class.
|
|
class AnalyzerConverter {
|
|
/// Convert the analysis [error] from the 'analyzer' package to an analysis
|
|
/// error defined by the plugin API. If a [lineInfo] is provided then the
|
|
/// error's location will have a start line and start column. If a [severity]
|
|
/// is provided, then it will override the severity defined by the error.
|
|
plugin.AnalysisError convertAnalysisError(analyzer.AnalysisError error,
|
|
{analyzer.LineInfo? lineInfo, analyzer.ErrorSeverity? severity}) {
|
|
var errorCode = error.errorCode;
|
|
severity ??= errorCode.errorSeverity;
|
|
var offset = error.offset;
|
|
var startLine = -1;
|
|
var startColumn = -1;
|
|
var endLine = -1;
|
|
var endColumn = -1;
|
|
if (lineInfo != null) {
|
|
var startLocation = lineInfo.getLocation(offset);
|
|
startLine = startLocation.lineNumber;
|
|
startColumn = startLocation.columnNumber;
|
|
var endLocation = lineInfo.getLocation(offset + error.length);
|
|
endLine = endLocation.lineNumber;
|
|
endColumn = endLocation.columnNumber;
|
|
}
|
|
List<plugin.DiagnosticMessage>? contextMessages;
|
|
if (error.contextMessages.isNotEmpty) {
|
|
contextMessages = error.contextMessages
|
|
.map((message) =>
|
|
convertDiagnosticMessage(message, lineInfo: lineInfo))
|
|
.toList();
|
|
}
|
|
return plugin.AnalysisError(
|
|
convertErrorSeverity(severity),
|
|
convertErrorType(errorCode.type),
|
|
plugin.Location(error.source.fullName, offset, error.length, startLine,
|
|
startColumn, endLine, endColumn),
|
|
error.message,
|
|
errorCode.name.toLowerCase(),
|
|
contextMessages: contextMessages,
|
|
correction: error.correction,
|
|
hasFix: true);
|
|
}
|
|
|
|
/// Convert the list of analysis [errors] from the 'analyzer' package to a
|
|
/// list of analysis errors defined by the plugin API. If a [lineInfo] is
|
|
/// provided then the resulting errors locations will have a start line and
|
|
/// start column. If an analysis [options] is provided then the severities of
|
|
/// the errors will be altered based on those options.
|
|
List<plugin.AnalysisError> convertAnalysisErrors(
|
|
List<analyzer.AnalysisError> errors,
|
|
{analyzer.LineInfo? lineInfo,
|
|
analyzer.AnalysisOptions? options}) {
|
|
var serverErrors = <plugin.AnalysisError>[];
|
|
for (var error in errors) {
|
|
var processor = analyzer.ErrorProcessor.getProcessor(options, error);
|
|
if (processor != null) {
|
|
var severity = processor.severity;
|
|
// Errors with null severity are filtered out.
|
|
if (severity != null) {
|
|
// Specified severities override.
|
|
serverErrors.add(convertAnalysisError(error,
|
|
lineInfo: lineInfo, severity: severity));
|
|
}
|
|
} else {
|
|
serverErrors.add(convertAnalysisError(error, lineInfo: lineInfo));
|
|
}
|
|
}
|
|
return serverErrors;
|
|
}
|
|
|
|
/// Convert the diagnostic [message] from the 'analyzer' package to an
|
|
/// analysis error defined by the plugin API. If a [lineInfo] is provided then
|
|
/// the error's location will have a start line and start column.
|
|
plugin.DiagnosticMessage convertDiagnosticMessage(
|
|
analyzer.DiagnosticMessage message,
|
|
{analyzer.LineInfo? lineInfo}) {
|
|
var file = message.filePath;
|
|
var offset = message.offset;
|
|
var length = message.length;
|
|
var startLine = -1;
|
|
var startColumn = -1;
|
|
var endLine = -1;
|
|
var endColumn = -1;
|
|
if (lineInfo != null) {
|
|
var lineLocation = lineInfo.getLocation(offset);
|
|
startLine = lineLocation.lineNumber;
|
|
startColumn = lineLocation.columnNumber;
|
|
var endLocation = lineInfo.getLocation(offset + length);
|
|
endLine = endLocation.lineNumber;
|
|
endColumn = endLocation.columnNumber;
|
|
}
|
|
return plugin.DiagnosticMessage(
|
|
message.messageText(includeUrl: true),
|
|
plugin.Location(
|
|
file, offset, length, startLine, startColumn, endLine, endColumn));
|
|
}
|
|
|
|
/// Convert the given [element] from the 'analyzer' package to an element
|
|
/// defined by the plugin API.
|
|
plugin.Element convertElement(analyzer.Element element) {
|
|
var kind = _convertElementToElementKind(element);
|
|
return plugin.Element(
|
|
kind,
|
|
element.displayName,
|
|
plugin.Element.makeFlags(
|
|
isPrivate: element.isPrivate,
|
|
isDeprecated: element.hasDeprecated,
|
|
isAbstract: _isAbstract(element),
|
|
isConst: _isConst(element),
|
|
isFinal: _isFinal(element),
|
|
isStatic: _isStatic(element),
|
|
),
|
|
location: locationFromElement(element),
|
|
typeParameters: _getTypeParametersString(element),
|
|
aliasedType: _getAliasedTypeString(element),
|
|
parameters: _getParametersString(element),
|
|
returnType: _getReturnTypeString(element),
|
|
);
|
|
}
|
|
|
|
/// Convert the element [kind] from the 'analyzer' package to an element kind
|
|
/// defined by the plugin API.
|
|
///
|
|
/// This method does not take into account that an instance of [ClassElement]
|
|
/// can be an enum and an instance of [FieldElement] can be an enum constant.
|
|
/// Use [_convertElementToElementKind] where possible.
|
|
plugin.ElementKind convertElementKind(analyzer.ElementKind kind) {
|
|
if (kind == analyzer.ElementKind.CLASS) {
|
|
return plugin.ElementKind.CLASS;
|
|
} else if (kind == analyzer.ElementKind.COMPILATION_UNIT) {
|
|
return plugin.ElementKind.COMPILATION_UNIT;
|
|
} else if (kind == analyzer.ElementKind.CONSTRUCTOR) {
|
|
return plugin.ElementKind.CONSTRUCTOR;
|
|
} else if (kind == analyzer.ElementKind.FIELD) {
|
|
return plugin.ElementKind.FIELD;
|
|
} else if (kind == analyzer.ElementKind.FUNCTION) {
|
|
return plugin.ElementKind.FUNCTION;
|
|
} else if (kind == analyzer.ElementKind.FUNCTION_TYPE_ALIAS) {
|
|
return plugin.ElementKind.FUNCTION_TYPE_ALIAS;
|
|
} else if (kind == analyzer.ElementKind.GENERIC_FUNCTION_TYPE) {
|
|
return plugin.ElementKind.FUNCTION_TYPE_ALIAS;
|
|
} else if (kind == analyzer.ElementKind.GETTER) {
|
|
return plugin.ElementKind.GETTER;
|
|
} else if (kind == analyzer.ElementKind.LABEL) {
|
|
return plugin.ElementKind.LABEL;
|
|
} else if (kind == analyzer.ElementKind.LIBRARY) {
|
|
return plugin.ElementKind.LIBRARY;
|
|
} else if (kind == analyzer.ElementKind.LOCAL_VARIABLE) {
|
|
return plugin.ElementKind.LOCAL_VARIABLE;
|
|
} else if (kind == analyzer.ElementKind.METHOD) {
|
|
return plugin.ElementKind.METHOD;
|
|
} else if (kind == analyzer.ElementKind.PARAMETER) {
|
|
return plugin.ElementKind.PARAMETER;
|
|
} else if (kind == analyzer.ElementKind.PREFIX) {
|
|
return plugin.ElementKind.PREFIX;
|
|
} else if (kind == analyzer.ElementKind.SETTER) {
|
|
return plugin.ElementKind.SETTER;
|
|
} else if (kind == analyzer.ElementKind.TOP_LEVEL_VARIABLE) {
|
|
return plugin.ElementKind.TOP_LEVEL_VARIABLE;
|
|
} else if (kind == analyzer.ElementKind.TYPE_ALIAS) {
|
|
return plugin.ElementKind.TYPE_ALIAS;
|
|
} else if (kind == analyzer.ElementKind.TYPE_PARAMETER) {
|
|
return plugin.ElementKind.TYPE_PARAMETER;
|
|
}
|
|
return plugin.ElementKind.UNKNOWN;
|
|
}
|
|
|
|
/// Convert the error [severity] from the 'analyzer' package to an analysis
|
|
/// error severity defined by the plugin API.
|
|
plugin.AnalysisErrorSeverity convertErrorSeverity(
|
|
analyzer.ErrorSeverity severity) =>
|
|
plugin.AnalysisErrorSeverity(severity.name);
|
|
|
|
///Convert the error [type] from the 'analyzer' package to an analysis error
|
|
/// type defined by the plugin API.
|
|
plugin.AnalysisErrorType convertErrorType(analyzer.ErrorType type) =>
|
|
plugin.AnalysisErrorType(type.name);
|
|
|
|
/// Create a location based on an the given [element].
|
|
plugin.Location? locationFromElement(analyzer.Element? element,
|
|
{int? offset, int? length}) {
|
|
if (element == null || element.source == null) {
|
|
return null;
|
|
}
|
|
offset ??= element.nameOffset;
|
|
length ??= element.nameLength;
|
|
if (element is analyzer.CompilationUnitElement ||
|
|
(element is analyzer.LibraryElement && offset < 0)) {
|
|
offset = 0;
|
|
length = 0;
|
|
}
|
|
var unitElement = _getUnitElement(element);
|
|
var range = analyzer.SourceRange(offset, length);
|
|
return _locationForArgs(unitElement, range);
|
|
}
|
|
|
|
/// Convert the element kind of the [element] from the 'analyzer' package to
|
|
/// an element kind defined by the plugin API.
|
|
plugin.ElementKind _convertElementToElementKind(analyzer.Element element) {
|
|
if (element is analyzer.ClassElement && element.isEnum) {
|
|
return plugin.ElementKind.ENUM;
|
|
} else if (element is analyzer.FieldElement &&
|
|
element.isEnumConstant &&
|
|
// MyEnum.values and MyEnum.one.index return isEnumConstant = true
|
|
// so these additional checks are necessary.
|
|
// TODO(danrubel) MyEnum.values is constant, but is a list
|
|
// so should it return isEnumConstant = true?
|
|
// MyEnum.one.index is final but *not* constant
|
|
// so should it return isEnumConstant = true?
|
|
// Or should we return ElementKind.ENUM_CONSTANT here
|
|
// in either or both of these cases?
|
|
element.type.element == element.enclosingElement) {
|
|
return plugin.ElementKind.ENUM_CONSTANT;
|
|
}
|
|
return convertElementKind(element.kind);
|
|
}
|
|
|
|
String? _getAliasedTypeString(analyzer.Element element) {
|
|
if (element is analyzer.TypeAliasElement) {
|
|
var aliasedType = element.aliasedType;
|
|
return aliasedType.getDisplayString(withNullability: false);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Return a textual representation of the parameters of the given [element],
|
|
/// or `null` if the element does not have any parameters.
|
|
String? _getParametersString(analyzer.Element element) {
|
|
// TODO(scheglov) expose the corresponding feature from ExecutableElement
|
|
List<analyzer.ParameterElement> parameters;
|
|
if (element is analyzer.ExecutableElement) {
|
|
// valid getters don't have parameters
|
|
if (element.kind == analyzer.ElementKind.GETTER &&
|
|
element.parameters.isEmpty) {
|
|
return null;
|
|
}
|
|
parameters = element.parameters;
|
|
} else if (element is analyzer.TypeAliasElement) {
|
|
var aliasedElement = element.aliasedElement;
|
|
if (aliasedElement is analyzer.GenericFunctionTypeElement) {
|
|
parameters = aliasedElement.parameters;
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
var buffer = StringBuffer();
|
|
var closeOptionalString = '';
|
|
buffer.write('(');
|
|
for (var i = 0; i < parameters.length; i++) {
|
|
var parameter = parameters[i];
|
|
if (i > 0) {
|
|
buffer.write(', ');
|
|
}
|
|
if (closeOptionalString.isEmpty) {
|
|
if (parameter.isNamed) {
|
|
buffer.write('{');
|
|
closeOptionalString = '}';
|
|
} else if (parameter.isOptionalPositional) {
|
|
buffer.write('[');
|
|
closeOptionalString = ']';
|
|
}
|
|
}
|
|
parameter.appendToWithoutDelimiters(buffer, withNullability: false);
|
|
}
|
|
buffer.write(closeOptionalString);
|
|
buffer.write(')');
|
|
return buffer.toString();
|
|
}
|
|
|
|
/// Return a textual representation of the return type of the given [element],
|
|
/// or `null` if the element does not have a return type.
|
|
String? _getReturnTypeString(analyzer.Element element) {
|
|
if (element is analyzer.ExecutableElement) {
|
|
if (element.kind == analyzer.ElementKind.SETTER) {
|
|
return null;
|
|
}
|
|
return element.returnType.getDisplayString(withNullability: false);
|
|
} else if (element is analyzer.VariableElement) {
|
|
return element.type.getDisplayString(withNullability: false);
|
|
} else if (element is analyzer.TypeAliasElement) {
|
|
var aliasedType = element.aliasedType;
|
|
if (aliasedType is FunctionType) {
|
|
var returnType = aliasedType.returnType;
|
|
return returnType.getDisplayString(withNullability: false);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Return a textual representation of the type parameters of the given
|
|
/// [element], or `null` if the element does not have type parameters.
|
|
String? _getTypeParametersString(analyzer.Element element) {
|
|
if (element is analyzer.TypeParameterizedElement) {
|
|
var typeParameters = element.typeParameters;
|
|
if (typeParameters.isEmpty) {
|
|
return null;
|
|
}
|
|
return '<${typeParameters.join(', ')}>';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Return the compilation unit containing the given [element].
|
|
analyzer.CompilationUnitElement? _getUnitElement(analyzer.Element element) {
|
|
analyzer.Element? currentElement = element;
|
|
if (currentElement is analyzer.CompilationUnitElement) {
|
|
return currentElement;
|
|
}
|
|
if (currentElement.enclosingElement is analyzer.LibraryElement) {
|
|
currentElement = currentElement.enclosingElement;
|
|
}
|
|
if (currentElement is analyzer.LibraryElement) {
|
|
return currentElement.definingCompilationUnit;
|
|
}
|
|
for (;
|
|
currentElement != null;
|
|
currentElement = currentElement.enclosingElement) {
|
|
if (currentElement is analyzer.CompilationUnitElement) {
|
|
return currentElement;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bool _isAbstract(analyzer.Element element) {
|
|
// TODO(scheglov) add isAbstract to Element API
|
|
if (element is analyzer.ClassElement) {
|
|
return element.isAbstract;
|
|
} else if (element is analyzer.MethodElement) {
|
|
return element.isAbstract;
|
|
} else if (element is analyzer.PropertyAccessorElement) {
|
|
return element.isAbstract;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool _isConst(analyzer.Element element) {
|
|
// TODO(scheglov) add isConst to Element API
|
|
if (element is analyzer.ConstructorElement) {
|
|
return element.isConst;
|
|
} else if (element is analyzer.VariableElement) {
|
|
return element.isConst;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool _isFinal(analyzer.Element element) {
|
|
// TODO(scheglov) add isFinal to Element API
|
|
if (element is analyzer.VariableElement) {
|
|
return element.isFinal;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool _isStatic(analyzer.Element element) {
|
|
// TODO(scheglov) add isStatic to Element API
|
|
if (element is analyzer.ExecutableElement) {
|
|
return element.isStatic;
|
|
} else if (element is analyzer.PropertyInducingElement) {
|
|
return element.isStatic;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Create and return a location within the given [unitElement] at the given
|
|
/// [range].
|
|
plugin.Location? _locationForArgs(
|
|
analyzer.CompilationUnitElement? unitElement,
|
|
analyzer.SourceRange range) {
|
|
var startLine = 0;
|
|
var startColumn = 0;
|
|
var endLine = 0;
|
|
var endColumn = 0;
|
|
|
|
if (unitElement == null) {
|
|
return null;
|
|
}
|
|
try {
|
|
var lineInfo = unitElement.lineInfo;
|
|
if (lineInfo != null) {
|
|
var offsetLocation = lineInfo.getLocation(range.offset);
|
|
startLine = offsetLocation.lineNumber;
|
|
startColumn = offsetLocation.columnNumber;
|
|
var endLocation = lineInfo.getLocation(range.offset + range.length);
|
|
endLine = endLocation.lineNumber;
|
|
endColumn = endLocation.columnNumber;
|
|
}
|
|
} on analyzer.AnalysisException {
|
|
// Ignore exceptions
|
|
}
|
|
return plugin.Location(unitElement.source.fullName, range.offset,
|
|
range.length, startLine, startColumn, endLine, endColumn);
|
|
}
|
|
}
|