blob: 4489693c4167ce6fb4630a32b90b2f14e581951d [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 'dart:io';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/task/inference_error.dart';
import 'package:test/test.dart';
import 'resolved_ast_printer.dart';
/// Set this path to automatically replace expectations in invocations of
/// [checkElementText] with the new actual texts.
const String? _testPath = null;
/// The list of replacements that update expectations.
final List<_Replacement> _replacements = [];
/// The cached content of the file with the [_testPath].
String? _testCode;
/// The cache line information for the [_testPath] file.
LineInfo? _testCodeLines;
void applyCheckElementTextReplacements() {
if (_testPath != null && _replacements.isNotEmpty) {
_replacements.sort((a, b) => b.offset - a.offset);
String newCode = _testCode!;
_replacements.forEach((r) {
newCode =
newCode.substring(0, r.offset) + r.text + newCode.substring(r.end);
});
File(_testPath!).writeAsStringSync(newCode);
}
}
/// Write the given [library] elements into the canonical text presentation
/// taking into account the specified 'withX' options. Then compare the
/// actual text with the given [expected] one.
void checkElementText(
LibraryElement library,
String expected, {
bool withCodeRanges = false,
bool withExportScope = false,
bool withNonSynthetic = false,
}) {
var writer = _ElementWriter(
selfUriStr: '${library.source.uri}',
withCodeRanges: withCodeRanges,
withExportScope: withExportScope,
withNonSynthetic: withNonSynthetic,
);
writer.writeLibraryElement(library);
String actualText = writer.buffer.toString();
actualText =
actualText.split('\n').map((line) => line.trimRight()).join('\n');
if (_testPath != null && actualText != expected) {
if (_testCode == null) {
_testCode = File(_testPath!).readAsStringSync();
_testCodeLines = LineInfo.fromContent(_testCode!);
}
try {
throw 42;
} catch (e, trace) {
String traceString = trace.toString();
// Assuming traceString contains "$_testPath:$invocationLine:$column",
// figure out the value of invocationLine.
int testFilePathOffset = traceString.indexOf(_testPath!);
expect(testFilePathOffset, isNonNegative);
// Sanity check: there must be ':' after the path.
expect(traceString[testFilePathOffset + _testPath!.length], ':');
int lineOffset = testFilePathOffset + _testPath!.length + ':'.length;
int invocationLine = int.parse(traceString.substring(
lineOffset, traceString.indexOf(':', lineOffset)));
int invocationOffset =
_testCodeLines!.getOffsetOfLine(invocationLine - 1);
const String rawStringPrefix = "r'''";
int expectationOffset =
_testCode!.indexOf(rawStringPrefix, invocationOffset);
// Sanity check: there must be no other strings or blocks.
expect(_testCode!.substring(invocationOffset, expectationOffset),
isNot(anyOf(contains("'"), contains('"'), contains('}'))));
expectationOffset += rawStringPrefix.length;
int expectationEnd = _testCode!.indexOf("'''", expectationOffset);
_replacements.add(
_Replacement(expectationOffset, expectationEnd, '\n' + actualText));
}
}
// Print the actual text to simplify copy/paste into the expectation.
// if (actualText != expected) {
// print('-------- Actual --------');
// print(actualText + '------------------------');
// }
expect(actualText, expected);
}
/// Writes the canonical text presentation of elements.
class _ElementWriter {
final String? selfUriStr;
final bool withCodeRanges;
final bool withExportScope;
final bool withNonSynthetic;
final StringBuffer buffer = StringBuffer();
String indent = '';
_ElementWriter({
this.selfUriStr,
required this.withCodeRanges,
required this.withExportScope,
required this.withNonSynthetic,
});
void writeLibraryElement(LibraryElement e) {
_writelnWithIndent('library');
_withIndent(() {
var name = e.name;
if (name != null && name.isNotEmpty) {
_writelnWithIndent('name: $name');
}
var nameOffset = e.nameOffset;
if (nameOffset != -1) {
_writelnWithIndent('nameOffset: $nameOffset');
}
_writeDocumentation(e);
_writeMetadata(e);
var imports = e.imports.where((import) => !import.isSynthetic).toList();
_writeElements('imports', imports, _writeImportElement);
_writeElements('exports', e.exports, _writeExportElement);
_writelnWithIndent('definingUnit');
_withIndent(() {
_writeUnitElement(e.definingCompilationUnit);
});
_writeElements('parts', e.parts, (CompilationUnitElement e) {
_writelnWithIndent(e.uri!);
_withIndent(() {
_writeMetadata(e);
_writeUnitElement(e);
});
});
if (withExportScope) {
_writelnWithIndent('exportScope');
_withIndent(() {
_writeExportScope(e);
});
}
});
}
void _assertNonSyntheticElementSelf(Element element) {
expect(element.isSynthetic, isFalse);
expect(element.nonSynthetic, same(element));
}
/// Assert that the [accessor] of the [property] is correctly linked to
/// the same enclosing element as the [property].
void _assertSyntheticAccessorEnclosing(
PropertyInducingElement property, PropertyAccessorElement accessor) {
if (accessor.isSynthetic) {
// Usually we have a non-synthetic property, and a synthetic accessor.
} else {
// But it is possible to have a non-synthetic setter.
// class A {
// final int foo;
// set foo(int newValue) {}
// }
expect(accessor.isSetter, isTrue);
}
expect(accessor.variable, same(property));
var propertyEnclosing = property.enclosingElement;
expect(accessor.enclosingElement, same(propertyEnclosing));
if (propertyEnclosing is CompilationUnitElement) {
expect(propertyEnclosing.accessors, contains(accessor));
} else if (propertyEnclosing is ClassElement) {
expect(propertyEnclosing.accessors, contains(accessor));
}
}
ResolvedAstPrinter _createAstPrinter() {
return ResolvedAstPrinter(
selfUriStr: selfUriStr,
sink: buffer,
indent: indent,
withNullability: true,
withOffsets: true,
);
}
String _getElementLocationString(Element? element) {
if (element == null || element is MultiplyDefinedElement) {
return 'null';
}
String onlyName(String uri) {
if (uri.startsWith('file:///')) {
return uri.substring(uri.lastIndexOf('/') + 1);
}
return uri;
}
var location = element.location!;
List<String> components = location.components.toList();
if (components.isNotEmpty) {
components[0] = onlyName(components[0]);
}
if (components.length >= 2) {
components[1] = onlyName(components[1]);
if (components[0] == components[1]) {
components.removeAt(0);
}
}
return components.join(';');
}
String? _typeStr(DartType? type) {
return type?.getDisplayString(
withNullability: true,
);
}
void _withIndent(void Function() f) {
var savedIndent = indent;
indent = '$savedIndent ';
f();
indent = savedIndent;
}
void _writeBodyModifiers(ExecutableElement e) {
if (e.isAsynchronous) {
expect(e.isSynchronous, isFalse);
buffer.write(' async');
}
if (e.isSynchronous && e.isGenerator) {
expect(e.isAsynchronous, isFalse);
buffer.write(' sync');
}
_writeIf(e.isGenerator, '*');
}
void _writeClassElement(ClassElement e) {
_writeIndentedLine(() {
_writeIf(e.isAbstract && !e.isMixin, 'abstract ');
_writeIf(!e.isSimplyBounded, 'notSimplyBounded ');
if (e.isEnum) {
buffer.write('enum ');
} else if (e.isMixin) {
buffer.write('mixin ');
} else {
buffer.write('class ');
}
_writeIf(e.isMixinApplication, 'alias ');
_writeName(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeParameterElements(e.typeParameters);
var supertype = e.supertype;
if (supertype != null &&
(supertype.element.name != 'Object' || e.mixins.isNotEmpty)) {
_writeType(supertype, name: 'supertype');
}
if (e.isMixin) {
var superclassConstraints = e.superclassConstraints;
if (superclassConstraints.isEmpty) {
throw StateError('At least Object is expected.');
}
_writeElements<DartType>(
'superclassConstraints',
superclassConstraints,
_writeType,
);
}
_writeElements<DartType>('mixins', e.mixins, _writeType);
_writeElements<DartType>('interfaces', e.interfaces, _writeType);
_writeElements('fields', e.fields, _writePropertyInducingElement);
var constructors = e.constructors;
if (e.isEnum) {
expect(constructors, isEmpty);
} else {
expect(constructors, isNotEmpty);
}
_writeElements('constructors', constructors, _writeConstructorElement);
_writeElements('accessors', e.accessors, _writePropertyAccessorElement);
_writeElements('methods', e.methods, _writeMethodElement);
});
_assertNonSyntheticElementSelf(e);
}
void _writeCodeRange(Element e) {
if (withCodeRanges && !e.isSynthetic) {
e as ElementImpl;
_writelnWithIndent('codeOffset: ${e.codeOffset}');
_writelnWithIndent('codeLength: ${e.codeLength}');
}
}
void _writeConstantInitializer(Element e) {
if (e is ConstVariableElement) {
var initializer = e.constantInitializer;
if (initializer != null) {
_writelnWithIndent('constantInitializer');
_withIndent(() {
_writeNode(initializer);
});
}
}
}
void _writeConstructorElement(ConstructorElement e) {
e as ConstructorElementImpl;
_writeIndentedLine(() {
_writeIf(e.isSynthetic, 'synthetic ');
_writeIf(e.isExternal, 'external ');
_writeIf(e.isConst, 'const ');
_writeIf(e.isFactory, 'factory ');
expect(e.isAbstract, isFalse);
_writeName(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
var periodOffset = e.periodOffset;
var nameEnd = e.nameEnd;
if (periodOffset != null && nameEnd != null) {
_writelnWithIndent('periodOffset: $periodOffset');
_writelnWithIndent('nameEnd: $nameEnd');
}
_writeParameterElements(e.parameters);
_writeElements(
'constantInitializers',
e.constantInitializers,
_writeNode,
);
var redirectedConstructor = e.redirectedConstructor;
if (redirectedConstructor != null) {
_writeElementReference('redirectedConstructor', redirectedConstructor);
}
_writeNonSyntheticElement(e);
});
expect(e.isAsynchronous, isFalse);
expect(e.isGenerator, isFalse);
if (e.isSynthetic) {
expect(e.nameOffset, -1);
expect(e.nonSynthetic, same(e.enclosingElement));
} else {
expect(e.nameOffset, isPositive);
}
}
void _writeDocumentation(Element element) {
var documentation = element.documentationComment;
if (documentation != null) {
var str = documentation;
str = str.replaceAll('\n', r'\n');
str = str.replaceAll('\r', r'\r');
_writelnWithIndent('documentationComment: $str');
}
}
void _writeElementReference(String name, Element element) {
var printer = _createAstPrinter();
printer.writeElement(name, element);
}
void _writeElements<T>(String name, List<T> elements, void Function(T) f) {
if (elements.isNotEmpty) {
_writelnWithIndent(name);
_withIndent(() {
for (var element in elements) {
f(element);
}
});
}
}
void _writeExportElement(ExportElement e) {
_writeIndentedLine(() {
_writeUri(e.exportedLibrary?.source);
});
_withIndent(() {
_writeMetadata(e);
_writeNamespaceCombinators(e.combinators);
});
_assertNonSyntheticElementSelf(e);
}
void _writeExportScope(LibraryElement e) {
var map = e.exportNamespace.definedNames;
var names = map.keys.toList()..sort();
for (var name in names) {
var element = map[name];
var elementLocationStr = _getElementLocationString(element);
_writelnWithIndent('$name: $elementLocationStr');
}
}
void _writeExtensionElement(ExtensionElement e) {
_writeIndentedLine(() {
_writeName(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeParameterElements(e.typeParameters);
_writeType(e.extendedType, name: 'extendedType');
});
_withIndent(() {
_writeElements('fields', e.fields, _writePropertyInducingElement);
_writeElements('accessors', e.accessors, _writePropertyAccessorElement);
_writeElements('methods', e.methods, _writeMethodElement);
});
_assertNonSyntheticElementSelf(e);
}
void _writeFunctionElement(FunctionElement e) {
_writeIndentedLine(() {
_writeIf(e.isExternal, 'external ');
_writeName(e);
_writeBodyModifiers(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeParameterElements(e.typeParameters);
_writeParameterElements(e.parameters);
_writeType(e.returnType, name: 'returnType');
});
_assertNonSyntheticElementSelf(e);
}
void _writeIf(bool flag, String str) {
if (flag) {
buffer.write(str);
}
}
void _writeImportElement(ImportElement e) {
_writeIndentedLine(() {
_writeUri(e.importedLibrary?.source);
_writeIf(e.isDeferred, ' deferred');
var prefix = e.prefix;
if (prefix != null) {
buffer.write(' as ');
_writeName(prefix);
}
});
_withIndent(() {
_writeMetadata(e);
_writeNamespaceCombinators(e.combinators);
});
_assertNonSyntheticElementSelf(e);
}
void _writeIndentedLine(void Function() f) {
buffer.write(indent);
f();
buffer.writeln();
}
void _writelnWithIndent(String line) {
buffer.write(indent);
buffer.writeln(line);
}
void _writeMetadata(Element element) {
var annotations = element.metadata;
if (annotations.isNotEmpty) {
_writelnWithIndent('metadata');
_withIndent(() {
for (var annotation in annotations) {
annotation as ElementAnnotationImpl;
_writeNode(annotation.annotationAst);
}
});
}
}
void _writeMethodElement(MethodElement e) {
_writeIndentedLine(() {
_writeIf(e.isSynthetic, 'synthetic ');
_writeIf(e.isStatic, 'static ');
_writeIf(e.isAbstract, 'abstract ');
_writeIf(e.isExternal, 'external ');
_writeName(e);
_writeBodyModifiers(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeInferenceError(e);
_writeTypeParameterElements(e.typeParameters);
_writeParameterElements(e.parameters);
_writeType(e.returnType, name: 'returnType');
_writeNonSyntheticElement(e);
});
if (e.isSynthetic && e.enclosingElement is EnumElementImpl) {
expect(e.name, 'toString');
expect(e.nonSynthetic, same(e.enclosingElement));
} else {
_assertNonSyntheticElementSelf(e);
}
}
void _writeName(Element e) {
var name = e.displayName;
buffer.write(name);
buffer.write(name.isNotEmpty ? ' @' : '@');
buffer.write(e.nameOffset);
}
void _writeNamespaceCombinator(NamespaceCombinator e) {
_writeIndentedLine(() {
if (e is ShowElementCombinator) {
buffer.write('show: ');
buffer.write(e.shownNames.join(', '));
} else if (e is HideElementCombinator) {
buffer.write('hide: ');
buffer.write(e.hiddenNames.join(', '));
}
});
}
void _writeNamespaceCombinators(List<NamespaceCombinator> elements) {
_writeElements('combinators', elements, _writeNamespaceCombinator);
}
void _writeNode(AstNode node) {
buffer.write(indent);
node.accept(
_createAstPrinter(),
);
}
void _writeNonSyntheticElement(Element e) {
if (withNonSynthetic) {
_writeElementReference('nonSynthetic', e.nonSynthetic);
}
}
void _writeParameterElement(ParameterElement e) {
_writeIndentedLine(() {
if (e.isRequiredPositional) {
buffer.write('requiredPositional ');
} else if (e.isOptionalPositional) {
buffer.write('optionalPositional ');
} else if (e.isRequiredNamed) {
buffer.write('requiredName ');
} else if (e.isOptionalNamed) {
buffer.write('optionalNamed ');
}
_writeIf(e.isConst, 'const ');
_writeIf(e.isCovariant, 'covariant ');
_writeIf(e.isFinal, 'final ');
if (e is FieldFormalParameterElement) {
buffer.write('this.');
}
_writeName(e);
});
_withIndent(() {
_writeType(e.type, name: 'type');
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeParameterElements(e.typeParameters);
_writeParameterElements(e.parameters);
_writeConstantInitializer(e);
_writeNonSyntheticElement(e);
});
}
void _writeParameterElements(List<ParameterElement> elements) {
_writeElements('parameters', elements, _writeParameterElement);
}
void _writePropertyAccessorElement(PropertyAccessorElement e) {
PropertyInducingElement variable = e.variable;
expect(variable, isNotNull);
var variableEnclosing = variable.enclosingElement;
if (variableEnclosing is CompilationUnitElement) {
expect(variableEnclosing.topLevelVariables, contains(variable));
} else if (variableEnclosing is ClassElement) {
expect(variableEnclosing.fields, contains(variable));
}
if (e.isGetter) {
expect(variable.getter, same(e));
if (variable.setter != null) {
expect(variable.setter!.variable, same(variable));
}
} else {
expect(variable.setter, same(e));
if (variable.getter != null) {
expect(variable.getter!.variable, same(variable));
}
}
if (e.isSynthetic) {
expect(e.nameOffset, -1);
} else {
expect(e.nameOffset, isPositive);
_assertNonSyntheticElementSelf(e);
}
_writeIndentedLine(() {
_writeIf(e.isSynthetic, 'synthetic ');
_writeIf(e.isStatic, 'static ');
_writeIf(e.isAbstract, 'abstract ');
_writeIf(e.isExternal, 'external ');
if (e.isGetter) {
buffer.write('get ');
} else {
buffer.write('set ');
}
_writeName(e);
_writeBodyModifiers(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
expect(e.typeParameters, isEmpty);
_writeParameterElements(e.parameters);
_writeType(e.returnType, name: 'returnType');
_writeNonSyntheticElement(e);
});
}
void _writePropertyInducingElement(PropertyInducingElement e) {
DartType type = e.type;
expect(type, isNotNull);
if (e.isSynthetic) {
expect(e.nameOffset, -1);
} else {
expect(e.getter, isNotNull);
_assertSyntheticAccessorEnclosing(e, e.getter!);
if (e.setter != null) {
_assertSyntheticAccessorEnclosing(e, e.setter!);
}
expect(e.nameOffset, isPositive);
_assertNonSyntheticElementSelf(e);
}
_writeIndentedLine(() {
_writeIf(e.isSynthetic, 'synthetic ');
_writeIf(e.isStatic, 'static ');
_writeIf(e is FieldElementImpl && e.isAbstract, 'abstract ');
_writeIf(e is FieldElementImpl && e.isCovariant, 'covariant ');
_writeIf(e is FieldElementImpl && e.isExternal, 'external ');
_writeIf(e.isLate, 'late ');
_writeIf(e.isFinal, 'final ');
_writeIf(e.isConst, 'const ');
_writeName(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeInferenceError(e);
_writeType(e.type, name: 'type');
_writeConstantInitializer(e);
_writeNonSyntheticElement(e);
});
}
void _writeType(DartType type, {String? name}) {
var typeStr = _typeStr(type);
if (name != null) {
_writelnWithIndent('$name: $typeStr');
} else {
_writelnWithIndent('$typeStr');
}
var aliasElement = type.aliasElement;
var aliasArguments = type.aliasArguments;
if (aliasElement is TypeAliasElementImpl && aliasArguments != null) {
_withIndent(() {
_createAstPrinter().writeElement('aliasElement', aliasElement);
_writeElements<DartType>(
'aliasArguments',
aliasArguments,
_writeType,
);
});
}
}
void _writeTypeAliasElement(TypeAliasElement e) {
e as TypeAliasElementImpl;
_writeIndentedLine(() {
_writeIf(e.isFunctionTypeAliasBased, 'functionTypeAliasBased ');
_writeIf(!e.isSimplyBounded, 'notSimplyBounded ');
_writeName(e);
});
_withIndent(() {
_writeDocumentation(e);
_writeMetadata(e);
_writeCodeRange(e);
_writeTypeParameterElements(e.typeParameters);
var aliasedType = e.aliasedType;
_writeType(aliasedType, name: 'aliasedType');
// TODO(scheglov) https://github.com/dart-lang/sdk/issues/44629
// TODO(scheglov) Remove it when we stop providing it everywhere.
if (aliasedType is FunctionType) {
// ignore: deprecated_member_use_from_same_package
expect(aliasedType.element, isNull);
}
var aliasedElement = e.aliasedElement;
if (aliasedElement is GenericFunctionTypeElement) {
final aliasedElement_ = aliasedElement as GenericFunctionTypeElement;
_writelnWithIndent('aliasedElement: GenericFunctionTypeElement');
_withIndent(() {
_writeTypeParameterElements(aliasedElement_.typeParameters);
_writeParameterElements(aliasedElement_.parameters);
_writeType(aliasedElement_.returnType, name: 'returnType');
});
}
});
_assertNonSyntheticElementSelf(e);
}
void _writeTypeInferenceError(Element e) {
TopLevelInferenceError? inferenceError;
if (e is MethodElementImpl) {
inferenceError = e.typeInferenceError;
} else if (e is PropertyInducingElementImpl) {
inferenceError = e.typeInferenceError;
}
if (inferenceError != null) {
String kindName = inferenceError.kind.toString();
if (kindName.startsWith('TopLevelInferenceErrorKind.')) {
kindName = kindName.substring('TopLevelInferenceErrorKind.'.length);
}
_writelnWithIndent('typeInferenceError: $kindName');
}
}
void _writeTypeParameterElement(TypeParameterElement e) {
e as TypeParameterElementImpl;
_writeIndentedLine(() {
buffer.write('${e.variance} ');
_writeName(e);
});
_withIndent(() {
_writeCodeRange(e);
var bound = e.bound;
if (bound != null) {
_writeType(bound, name: 'bound');
}
var defaultType = e.defaultType;
if (defaultType != null) {
_writeType(defaultType, name: 'defaultType');
}
_writeMetadata(e);
});
_assertNonSyntheticElementSelf(e);
}
void _writeTypeParameterElements(List<TypeParameterElement> elements) {
_writeElements('typeParameters', elements, _writeTypeParameterElement);
}
void _writeUnitElement(CompilationUnitElement e) {
_writeElements('classes', e.classes, _writeClassElement);
_writeElements('enums', e.enums, _writeClassElement);
_writeElements('extensions', e.extensions, _writeExtensionElement);
_writeElements('mixins', e.mixins, _writeClassElement);
_writeElements('typeAliases', e.typeAliases, _writeTypeAliasElement);
_writeElements(
'topLevelVariables',
e.topLevelVariables,
_writePropertyInducingElement,
);
_writeElements(
'accessors',
e.accessors,
_writePropertyAccessorElement,
);
_writeElements('functions', e.functions, _writeFunctionElement);
}
void _writeUri(Source? source) {
if (source != null) {
Uri uri = source.uri;
String uriStr = uri.toString();
if (uri.isScheme('file')) {
uriStr = uri.pathSegments.last;
}
buffer.write('$uriStr');
} else {
buffer.write('<unresolved>');
}
}
}
class _Replacement {
final int offset;
final int end;
final String text;
_Replacement(this.offset, this.end, this.text);
}