blob: 28235f450d9f1eb31150c81580f398d45973b87d [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/source/error_processor.dart' as analyzer;
import 'package:analyzer/source/line_info.dart' as analyzer;
import 'package:analyzer/source/source_range.dart' as analyzer;
import 'package:analyzer/src/generated/engine.dart' as analyzer;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:path/path.dart' as path;
/// 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 {
/// Converts the analysis [diagnostic] 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.Diagnostic diagnostic, {
analyzer.LineInfo? lineInfo,
analyzer.DiagnosticSeverity? severity,
}) {
var diagnosticCode = diagnostic.diagnosticCode;
severity ??= diagnosticCode.severity;
var offset = diagnostic.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 + diagnostic.length);
endLine = endLocation.lineNumber;
endColumn = endLocation.columnNumber;
}
List<plugin.DiagnosticMessage>? contextMessages;
if (diagnostic.contextMessages.isNotEmpty) {
contextMessages = diagnostic.contextMessages
.map(
(message) => convertDiagnosticMessage(message, lineInfo: lineInfo),
)
.toList();
}
return plugin.AnalysisError(
convertErrorSeverity(severity),
convertErrorType(diagnosticCode.type),
plugin.Location(
diagnostic.source.fullName,
offset,
diagnostic.length,
startLine,
startColumn,
endLine: endLine,
endColumn: endColumn,
),
diagnostic.message,
diagnosticCode.name.toLowerCase(),
contextMessages: contextMessages,
correction: diagnostic.correctionMessage,
hasFix: true,
);
}
/// Converts the list of analysis [diagnostics] from the 'analyzer' package to
/// a list of analysis errors defined by the plugin API.
///
/// The severities of the errors are altered based on [options].
List<plugin.AnalysisError> convertAnalysisErrors(
List<analyzer.Diagnostic> diagnostics, {
// TODO(srawlins): Make `lineInfo` required and non-nullable, in a breaking
// change release.
analyzer.LineInfo? lineInfo,
// TODO(srawlins): Make `options` required and non-nullable, in a breaking
// change release.
analyzer.AnalysisOptions? options,
}) {
var serverErrors = <plugin.AnalysisError>[];
for (var diagnostic in diagnostics) {
var processor = analyzer.ErrorProcessor.getProcessor(options, diagnostic);
if (processor != null) {
var severity = processor.severity;
// Errors with null severity are filtered out.
if (severity != null) {
// Specified severities override.
serverErrors.add(
convertAnalysisError(
diagnostic,
lineInfo: lineInfo,
severity: severity,
),
);
}
} else {
serverErrors.add(convertAnalysisError(diagnostic, 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,
),
);
}
Element convertElement(analyzer.Element element) {
var kind = convertElementToElementKind(element);
var name = getElementDisplayName(element);
var elementTypeParameters = _getTypeParametersString(element);
var aliasedType = _getAliasedTypeString(element);
var elementParameters = _getParametersString(element);
var elementReturnType = _getReturnTypeString(element);
return Element(
kind,
name,
Element.makeFlags(
isPrivate: element.isPrivate,
isDeprecated: element.metadata.hasDeprecated,
isAbstract: _isAbstract(element),
isConst: _isConst(element),
isFinal: _isFinal(element),
isStatic: _isStatic(element),
),
location: newLocation_fromElement(element),
typeParameters: elementTypeParameters,
aliasedType: aliasedType,
parameters: elementParameters,
returnType: elementReturnType,
);
}
/// 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.
// TODO(srawlins): Deprecate this.
plugin.ElementKind convertElementKind(analyzer.ElementKind kind) =>
kind.toPluginElementKind;
/// Return an [ElementKind] corresponding to the given [analyzer.Element].
ElementKind convertElementToElementKind(analyzer.Element element) {
if (element is analyzer.EnumElement) {
return ElementKind.ENUM;
} else if (element is analyzer.MixinElement) {
return ElementKind.MIXIN;
}
if (element is analyzer.FieldElement && element.isEnumConstant) {
return ElementKind.ENUM_CONSTANT;
}
return convertElementKind(element.kind);
}
/// Convert the error [severity] from the 'analyzer' package to an analysis
/// error severity defined by the plugin API.
plugin.AnalysisErrorSeverity convertErrorSeverity(
analyzer.DiagnosticSeverity severity,
) => plugin.AnalysisErrorSeverity.values.byName(severity.name);
/// Convert the error [type] from the 'analyzer' package to an analysis error
/// type defined by the plugin API.
plugin.AnalysisErrorType convertErrorType(analyzer.DiagnosticType type) =>
plugin.AnalysisErrorType.values.byName(type.name);
String getElementDisplayName(analyzer.Element element) {
if (element is analyzer.LibraryFragment) {
return path.basename(
(element as analyzer.LibraryFragment).source.fullName,
);
} else {
return element.displayName;
}
}
/// Create a Location based on an [analyzer.Element].
Location? newLocation_fromElement(analyzer.Element? element) {
if (element == null) {
return null;
}
if (element is analyzer.FormalParameterElement &&
element.enclosingElement == null) {
return null;
}
var fragment = element.firstFragment;
var offset = fragment.nameOffset ?? -1;
var length = fragment.name?.length ?? 0;
var range = analyzer.SourceRange(offset, length);
return _locationForArgs(fragment, range);
}
String? _getAliasedTypeString(analyzer.Element element) {
if (element is analyzer.TypeAliasElement) {
var aliasedType = element.aliasedType;
return aliasedType.getDisplayString();
}
return null;
}
String? _getParametersString(analyzer.Element element) {
// TODO(scheglov): expose the corresponding feature from ExecutableElement
List<analyzer.FormalParameterElement> parameters;
if (element is analyzer.ExecutableElement) {
// valid getters don't have parameters
if (element.kind == analyzer.ElementKind.GETTER &&
element.formalParameters.isEmpty) {
return null;
}
parameters = element.formalParameters.toList();
} else if (element is analyzer.TypeAliasElement) {
var aliasedType = element.aliasedType;
if (aliasedType is FunctionType) {
parameters = aliasedType.formalParameters.toList();
} else {
return null;
}
} else {
return null;
}
parameters.sort(_preferRequiredParams);
var sb = StringBuffer();
var closeOptionalString = '';
for (var parameter in parameters) {
if (sb.isNotEmpty) {
sb.write(', ');
}
if (closeOptionalString.isEmpty) {
if (parameter.isNamed) {
sb.write('{');
closeOptionalString = '}';
} else if (parameter.isOptionalPositional) {
sb.write('[');
closeOptionalString = ']';
}
}
if (parameter.isRequiredNamed) {
sb.write('required ');
} else if (parameter.metadata.hasDeprecated) {
sb.write('@required ');
}
parameter.appendToWithoutDelimiters(sb);
}
sb.write(closeOptionalString);
return '($sb)';
}
String? _getReturnTypeString(analyzer.Element element) {
if (element is analyzer.ExecutableElement) {
if (element.kind == analyzer.ElementKind.SETTER) {
return null;
} else {
return element.returnType.getDisplayString();
}
} else if (element is analyzer.VariableElement) {
var type = element.type;
return type.getDisplayString();
} else if (element is analyzer.TypeAliasElement) {
var aliasedType = element.aliasedType;
if (aliasedType is FunctionType) {
var returnType = aliasedType.returnType;
return returnType.getDisplayString();
}
}
return null;
}
String? _getTypeParametersString(analyzer.Element element) {
List<analyzer.TypeParameterElement>? typeParameters;
if (element is analyzer.InterfaceElement) {
typeParameters = element.typeParameters;
} else if (element is analyzer.TypeAliasElement) {
typeParameters = element.typeParameters;
}
if (typeParameters == null || typeParameters.isEmpty) {
return null;
}
return '<${typeParameters.join(', ')}>';
}
bool _isAbstract(analyzer.Element element) {
if (element is analyzer.ClassElement) {
return element.isAbstract;
}
if (element is analyzer.MethodElement) {
return element.isAbstract;
}
if (element is analyzer.MixinElement) {
return true;
}
return false;
}
bool _isConst(analyzer.Element element) {
if (element is analyzer.ConstructorElement) {
return element.isConst;
}
if (element is analyzer.VariableElement) {
return element.isConst;
}
return false;
}
bool _isFinal(analyzer.Element element) {
if (element is analyzer.VariableElement) {
return element.isFinal;
}
return false;
}
bool _isStatic(analyzer.Element element) {
if (element is analyzer.ExecutableElement) {
return element.isStatic;
}
if (element is analyzer.PropertyInducingElement) {
return element.isStatic;
}
return false;
}
/// Creates a new [Location].
Location? _locationForArgs(
analyzer.Fragment fragment,
analyzer.SourceRange range,
) {
var libraryFragment = fragment.libraryFragment;
if (libraryFragment == null) {
return null;
}
var lineInfo = libraryFragment.lineInfo;
var startLocation = lineInfo.getLocation(range.offset);
var endLocation = lineInfo.getLocation(range.end);
var startLine = startLocation.lineNumber;
var startColumn = startLocation.columnNumber;
var endLine = endLocation.lineNumber;
var endColumn = endLocation.columnNumber;
return Location(
fragment.libraryFragment!.source.fullName,
range.offset,
range.length,
startLine,
startColumn,
endLine: endLine,
endColumn: endColumn,
);
}
/// Sort required named parameters before optional ones.
int _preferRequiredParams(
analyzer.FormalParameterElement e1,
analyzer.FormalParameterElement e2,
) {
var rank1 = (e1.isRequiredNamed || e1.metadata.hasRequired)
? 0
: !e1.isNamed
? -1
: 1;
var rank2 = (e2.isRequiredNamed || e2.metadata.hasRequired)
? 0
: !e2.isNamed
? -1
: 1;
return rank1 - rank2;
}
}
// TODO(srawlins): Move this to a better location.
extension ElementKindExtensions on analyzer.ElementKind {
static const _kindMap = {
analyzer.ElementKind.CLASS: plugin.ElementKind.CLASS,
analyzer.ElementKind.COMPILATION_UNIT: plugin.ElementKind.COMPILATION_UNIT,
analyzer.ElementKind.CONSTRUCTOR: plugin.ElementKind.CONSTRUCTOR,
analyzer.ElementKind.FIELD: plugin.ElementKind.FIELD,
analyzer.ElementKind.FUNCTION: plugin.ElementKind.FUNCTION,
analyzer.ElementKind.FUNCTION_TYPE_ALIAS:
plugin.ElementKind.FUNCTION_TYPE_ALIAS,
analyzer.ElementKind.GENERIC_FUNCTION_TYPE:
plugin.ElementKind.FUNCTION_TYPE_ALIAS,
analyzer.ElementKind.GETTER: plugin.ElementKind.GETTER,
analyzer.ElementKind.LABEL: plugin.ElementKind.LABEL,
analyzer.ElementKind.LIBRARY: plugin.ElementKind.LIBRARY,
analyzer.ElementKind.LOCAL_VARIABLE: plugin.ElementKind.LOCAL_VARIABLE,
analyzer.ElementKind.METHOD: plugin.ElementKind.METHOD,
analyzer.ElementKind.PARAMETER: plugin.ElementKind.PARAMETER,
analyzer.ElementKind.PREFIX: plugin.ElementKind.PREFIX,
analyzer.ElementKind.SETTER: plugin.ElementKind.SETTER,
analyzer.ElementKind.TOP_LEVEL_VARIABLE:
plugin.ElementKind.TOP_LEVEL_VARIABLE,
analyzer.ElementKind.TYPE_ALIAS: plugin.ElementKind.TYPE_ALIAS,
analyzer.ElementKind.TYPE_PARAMETER: plugin.ElementKind.TYPE_PARAMETER,
};
/// 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 get toPluginElementKind =>
_kindMap[this] ?? plugin.ElementKind.UNKNOWN;
}