blob: 10640fc5b6a0d9fc141b6cb7cef2b65e2dacc451 [file] [log] [blame]
// 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 {
/// Optional provider for [analyzer.Element] location, used for example to
/// override the location of macro-generated elements.
final ElementLocationProvider? _locationProvider;
AnalyzerConverter({ElementLocationProvider? locationProvider})
: _locationProvider = locationProvider;
/// 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: endLine, endColumn: 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: endLine, endColumn: 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;
}
var result = _locationProvider?.forElement(element);
if (result != null) {
return result;
}
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: endLine, endColumn: endColumn);
}
}
abstract class ElementLocationProvider {
/// Return the location of [element] that this provider wants to override,
/// or `null` if the default location should be used.
plugin.Location? forElement(analyzer.Element element);
}