blob: b1229a432262222411d45db537f63c0c852190e0 [file] [log] [blame]
// Copyright (c) 2023, 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:kernel/ast.dart';
/// Dart scope
///
/// Provides information about symbols available inside a dart scope.
class DartScope {
final Library library;
final Class? cls;
final Member? member;
final bool isStatic;
final Map<String, DartType> definitions;
final List<TypeParameter> typeParameters;
DartScope(this.library, this.cls, this.member, this.definitions,
this.typeParameters)
: isStatic = member is Procedure ? member.isStatic : false;
@override
String toString() {
return '''DartScope {
Library: ${library.importUri},
Class: ${cls?.name},
Procedure: $member,
isStatic: $isStatic,
Scope: $definitions,
typeParameters: $typeParameters
}
''';
}
}
/// Dart scope
///
/// Provides information about symbols available inside a dart scope.
class DartScope2 {
final TreeNode node;
final Library library;
final Class? cls;
final Member? member;
final bool isStatic;
final Map<String, VariableDeclaration> definitions;
final List<TypeParameter> typeParameters;
DartScope2(this.node, this.library, this.cls, this.member, this.definitions,
this.typeParameters)
: isStatic = member is Procedure ? member.isStatic : false;
@override
String toString() {
return '''DartScope {
Library: ${library.importUri},
Class: ${cls?.name},
Procedure: $member,
isStatic: $isStatic,
Scope: $definitions,
typeParameters: $typeParameters
}
''';
}
}
/// DartScopeBuilder finds dart scope information for a location.
///
/// Find all definitions in scope at a given 1-based [line] and [column]:
///
/// - library
/// - class
/// - locals
/// - formals
/// - captured variables (for closures)
class DartScopeBuilder extends VisitorDefault<void> with VisitorVoidMixin {
final Component _component;
final int _line;
final int _column;
Library? _library;
Class? _cls;
Member? _member;
int _offset = -1;
final List<FunctionNode> _functions = [];
final Map<String, DartType> _definitions = {};
final List<TypeParameter> _typeParameters = [];
DartScopeBuilder._(this._component, this._line, this._column);
static DartScope? findScope(
Component component, Library library, int line, int column) {
DartScopeBuilder builder = DartScopeBuilder._(component, line, column);
library.accept(builder);
return builder.build();
}
DartScope? build() {
if (_offset < 0 || _library == null) return null;
return DartScope(_library!, _cls, _member, _definitions, _typeParameters);
}
@override
void defaultTreeNode(Node node) {
node.visitChildren(this);
}
@override
void visitLibrary(Library library) {
_library = library;
_offset = 0;
if (_line > 0) {
_offset = _component.getOffset(_library!.fileUri, _line, _column);
}
// Exit early if the evaluation offset is not found.
// Note: the complete scope is not found in this case,
// so the expression compiler will report an error.
if (_offset >= 0) super.visitLibrary(library);
}
@override
void visitClass(Class cls) {
if (_scopeContainsOffset(cls.fileOffset, cls.fileEndOffset, _offset)) {
_cls = cls;
_typeParameters.addAll(cls.typeParameters);
super.visitClass(cls);
}
}
@override
void defaultMember(Member m) {
if (_scopeContainsOffset(m.fileOffset, m.fileEndOffset, _offset)) {
_member = m;
super.defaultMember(m);
}
}
@override
void visitFunctionNode(FunctionNode fun) {
if (_scopeContainsOffset(fun.fileOffset, fun.fileEndOffset, _offset)) {
_functions.add(fun);
_typeParameters.addAll(fun.typeParameters);
super.visitFunctionNode(fun);
}
}
@override
void visitVariableDeclaration(VariableDeclaration decl) {
String? name = decl.name;
// Collect locals and formals appearing before current breakpoint.
// Note that we include variables with no offset because the offset
// is not set in many cases in generated code, so omitting them would
// make expression evaluation fail in too many cases.
// Issue: https://github.com/dart-lang/sdk/issues/43966
//
// A null name signals that the variable was synthetically introduced by the
// compiler so they are skipped.
if ((decl.fileOffset < 0 || decl.fileOffset < _offset) && name != null) {
_definitions[name] = decl.type;
}
super.visitVariableDeclaration(decl);
}
@override
void visitBlock(Block block) {
int fileEndOffset = FileEndOffsetCalculator.calculateEndOffset(block);
if (_scopeContainsOffset(block.fileOffset, fileEndOffset, _offset)) {
super.visitBlock(block);
}
}
bool _scopeContainsOffset(int startOffset, int endOffset, int offset) {
if (offset < 0 || startOffset < 0 || endOffset < 0) {
return false;
}
return startOffset <= offset && offset <= endOffset;
}
}
/// File end offset calculator.
///
/// Helps calculate file end offsets for nodes with internal scope
/// that do not have .fileEndOffset field.
///
/// For example - [Block]
class FileEndOffsetCalculator extends VisitorDefault<int?>
with VisitorNullMixin<int> {
static const int noOffset = -1;
final int _startOffset;
final TreeNode _root;
final TreeNode _original;
int _endOffset = noOffset;
/// Create calculator for a scoping node with no .fileEndOffset.
///
/// [_root] is the parent of the scoping node.
/// [_startOffset] is the start offset of the scoping node.
FileEndOffsetCalculator._(this._root, this._original)
: _startOffset = _original.fileOffset;
/// Calculate file end offset for a scoping node.
///
/// This calculator finds the first node in the ancestor chain that
/// can give such information for a given [node], i.e. satisfies one
/// of the following conditions:
///
/// - a node with a greater start offset that is a child of the
/// closest ancestor. The start offset of this child is used as a
/// file end offset of the [node].
///
/// - the closest ancestor with .fileEndOffset information. The file
/// end offset of the ancestor is used as the file end offset of
/// the [node.]
///
/// If none found, return [noOffset].
static int calculateEndOffset(TreeNode node) {
for (TreeNode? n = node.parent; n != null; n = n.parent) {
FileEndOffsetCalculator calculator = FileEndOffsetCalculator._(n, node);
int? offset = n.accept(calculator);
if (offset != noOffset) return offset!;
}
return noOffset;
}
@override
int defaultTreeNode(TreeNode node) {
if (node == _original) return _endOffset;
if (node == _root) {
node.visitChildren(this);
if (_endOffset != noOffset) return _endOffset;
return _endOffsetForNode(node);
}
// Skip synthesized variables as they could have offsets
// from later code (in case they are hoisted, for example).
if ((node is! VariableDeclaration || !node.isSynthesized) &&
_endOffset == noOffset &&
node.fileOffset > _startOffset) {
_endOffset = node.fileOffset;
}
return _endOffset;
}
static int _endOffsetForNode(TreeNode node) {
if (node is Class) return node.fileEndOffset;
if (node is Constructor) return node.fileEndOffset;
if (node is Procedure) return node.fileEndOffset;
if (node is Field) return node.fileEndOffset;
if (node is FunctionNode) return node.fileEndOffset;
return noOffset;
}
}
class DartScopeBuilder2 extends VisitorDefault<void> with VisitorVoidMixin {
final Library _library;
final Uri _scriptUri;
final int _offset;
final List<DartScope2> findScopes = [];
final Set<int> foundOffsets = {};
final Set<VariableDeclaration> hoistedUnwritten = {};
final List<List<VariableDeclaration>> scopes = [];
final List<List<TypeParameter>> typeParameterScopes = [];
Class? _currentCls = null;
Member? _currentMember = null;
bool checkClasses = true;
Uri _currentUri;
DartScopeBuilder2._(this._library, this._scriptUri, this._offset)
: _currentUri = _library.fileUri;
void clearScope() {
scopes.clear();
hoistedUnwritten.clear();
}
void addFound(TreeNode node) {
Map<String, VariableDeclaration> definitions = {};
for (List<VariableDeclaration> scope in scopes) {
for (VariableDeclaration decl in scope) {
String? name = decl.name;
if (name != null &&
!decl.isSynthesized &&
!hoistedUnwritten.contains(decl)) {
definitions[name] = decl;
}
}
}
// TODO(jensj): If the current member is static and we're in a class we have
// to skip the typeParameters from the class.
List<TypeParameter> typeParameters = [];
for (List<TypeParameter> typeParameterScope in typeParameterScopes) {
typeParameters.addAll(typeParameterScope);
}
DartScope2 findScope = new DartScope2(node, _library, _currentCls,
_currentMember, definitions, typeParameters);
findScopes.add(findScope);
}
@override
void defaultDartType(DartType node) {
return;
}
@override
void defaultTreeNode(TreeNode node) {
Uri prevUri = _currentUri;
if (node is FileUriNode) {
_currentUri = node.fileUri;
}
_checkOffset(node);
node.visitChildren(this);
_currentUri = prevUri;
}
@override
void visitAssertBlock(AssertBlock node) {
scopes.add([]);
super.visitAssertBlock(node);
scopes.removeLast();
}
@override
void visitBlock(Block node) {
scopes.add([]);
_checkOffset(node);
bool shouldSkip;
// See also the test [DillBlockChecker] which checks that we can do this
// pruning!
if (_currentUri != _scriptUri) {
shouldSkip = true;
} else if (node.parent is ForInStatement) {
// E.g. dart2js implicit cast in for-in loop
shouldSkip = false;
} else if (node.parent?.parent is ForStatement) {
// A vm transformation turns
// `for (var foo in bar) {}`
// into
// `for(;iterator.moveNext; ) { var foo = iterator.current; {} }`
// where the block directly containing `foo` has the original blocks
// offset, i.e. after the variable declaration, but it still contain
// it. So we pretend it has no offsets.
shouldSkip = false;
} else if (node.fileOffset >= 0 &&
node.fileEndOffset >= 0 &&
node.fileOffset != node.fileEndOffset) {
if (_offset < node.fileOffset || _offset > node.fileEndOffset) {
// Not contained in the block.
shouldSkip = true;
} else {
// Contained in the block.
shouldSkip = false;
}
} else {
// The block doesn't have valid offsets.
shouldSkip = false;
}
if (!shouldSkip) {
node.visitChildren(this);
}
scopes.removeLast();
}
@override
void visitBlockExpression(BlockExpression node) {
scopes.add([]);
super.visitBlockExpression(node);
scopes.removeLast();
}
@override
void visitCatch(Catch node) {
scopes.add([]);
super.visitCatch(node);
scopes.removeLast();
}
@override
void visitClass(Class node) {
if (!checkClasses) {
return;
}
_currentCls = node;
typeParameterScopes.add([...node.typeParameters]);
scopes.add([]);
super.visitClass(node);
clearScope();
typeParameterScopes.removeLast();
assert(typeParameterScopes.isEmpty);
_currentCls = null;
}
@override
void visitConstructor(Constructor node) {
Uri prevUri = _currentUri;
_currentUri = node.fileUri;
_currentMember = node;
clearScope();
scopes.add([]);
_checkOffset(node);
// The constructor is special in that the parameters from the contained
// function node is in scope in the initializers.
node.function.accept(this);
for (VariableDeclaration param in node.function.positionalParameters) {
scopes.last.add(param);
}
for (VariableDeclaration param in node.function.namedParameters) {
scopes.last.add(param);
}
for (Initializer initializer in node.initializers) {
initializer.accept(this);
}
clearScope();
_currentMember = null;
_currentUri = prevUri;
}
@override
void visitExtension(Extension node) {
typeParameterScopes.add([...node.typeParameters]);
super.visitExtension(node);
typeParameterScopes.removeLast();
}
@override
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
typeParameterScopes.add([...node.typeParameters]);
super.visitExtensionTypeDeclaration(node);
typeParameterScopes.removeLast();
}
@override
void visitField(Field node) {
_currentMember = node;
clearScope();
scopes.add([]);
super.visitField(node);
clearScope();
_currentMember = null;
}
@override
void visitForInStatement(ForInStatement node) {
scopes.add([]);
super.visitForInStatement(node);
scopes.removeLast();
}
@override
void visitForStatement(ForStatement node) {
scopes.add([]);
super.visitForStatement(node);
scopes.removeLast();
}
@override
void visitFunctionNode(FunctionNode node) {
typeParameterScopes.add([...node.typeParameters]);
scopes.add([]);
super.visitFunctionNode(node);
scopes.removeLast();
typeParameterScopes.removeLast();
}
@override
void visitLet(Let node) {
scopes.add([]);
super.visitLet(node);
scopes.removeLast();
}
@override
void visitLibrary(Library node) {
scopes.add([]);
super.visitLibrary(node);
clearScope();
}
@override
void visitProcedure(Procedure node) {
_currentMember = node;
clearScope();
scopes.add([]);
super.visitProcedure(node);
clearScope();
_currentMember = null;
}
@override
void visitTypedef(Typedef node) {
clearScope();
scopes.add([]);
typeParameterScopes.add([...node.typeParameters]);
super.visitTypedef(node);
typeParameterScopes.removeLast();
clearScope();
}
@override
void visitVariableSet(VariableSet node) {
super.visitVariableSet(node);
if (node.variable.isHoisted) {
hoistedUnwritten.remove(node.variable);
}
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
if (node.isHoisted) hoistedUnwritten.add(node);
super.visitVariableDeclaration(node);
// Declare it after.
scopes.last.add(node);
}
void _checkOffset(TreeNode node) {
if (_currentUri == _scriptUri) {
foundOffsets.add(node.fileOffset);
if (node.fileOffset == _offset) {
addFound(node);
} else {
List<int>? allOffsets = node.fileOffsetsIfMultiple;
if (allOffsets != null) {
for (final int offset in allOffsets) {
foundOffsets.add(offset);
if (offset == _offset) {
addFound(node);
break;
}
}
}
}
}
}
static DartScope findScopeFromOffsetAndClass(
Library library, Uri scriptUri, Class? cls, int offset) {
DartScopeBuilder2 data = _raw(library, scriptUri, cls, offset);
if (data.findScopes.isEmpty) {
int? closestMatchingOrSmallerOffset =
_findClosestMatchingOrSmallerOffset(data, offset);
if (closestMatchingOrSmallerOffset != null) {
offset = closestMatchingOrSmallerOffset;
data = _raw(library, scriptUri, cls, offset);
}
}
return _findScopePick(data.findScopes, library, cls, offset);
}
static int? _findClosestMatchingOrSmallerOffset(
DartScopeBuilder2 data, int offset) {
List<int> foundOffsets = data.foundOffsets.toList()..sort();
if (foundOffsets.isEmpty) return null;
int low = 0;
int high = foundOffsets.length - 1;
while (low < high) {
int mid = high - ((high - low) >> 1); // Get middle, rounding up.
int pivot = foundOffsets[mid];
if (pivot <= offset) {
low = mid;
} else {
high = mid - 1;
}
}
int result = foundOffsets[low];
if (result < 0) return null;
return result;
}
static DartScope _findScopePick(
List<DartScope2> scopes, Library library, Class? cls, int offset) {
DartScope2 scope;
if (scopes.length == 0) {
// This shouldn't happen.
return new DartScope(library, cls, null, {}, []);
} else if (scopes.length == 1) {
scope = scopes.single;
} else {
List<DartScope2> filteredScopes = _filterAll(scopes, library, offset);
if (filteredScopes.length == 0) {
// This shouldn't happen.
filteredScopes = scopes;
}
if (filteredScopes.length == 1) {
scope = filteredScopes.single;
} else {
// TODO(jensj): This shouldn't happen, but inevitably will.
// When it does, what should we do?
scope = scopes.last;
}
}
Map<String, DartType> definitions = {};
for (MapEntry<String, VariableDeclaration> entry
in scope.definitions.entries) {
definitions[entry.key] = entry.value.type;
}
return new DartScope(scope.library, scope.cls, scope.member, definitions,
scope.typeParameters);
}
static DartScope findScopeFromOffset(
Library library, Uri scriptUri, int offset) {
DartScopeBuilder2 data = _rawNoClass(library, scriptUri, offset);
if (data.findScopes.isEmpty) {
int? closestMatchingOrSmallerOffset =
_findClosestMatchingOrSmallerOffset(data, offset);
if (closestMatchingOrSmallerOffset != null) {
offset = closestMatchingOrSmallerOffset;
data = _rawNoClass(library, scriptUri, offset);
}
}
return _findScopePick(data.findScopes, library, null, offset);
}
static List<DartScope2> _filterAll(
List<DartScope2> rawScopes, Library library, int offset) {
List<DartScope2> firstFilteredScopes =
_filterScopesWithArtificialNodes(rawScopes, library);
if (firstFilteredScopes.isEmpty) {
return rawScopes;
}
if (_allHaveTheSameDefinitions(firstFilteredScopes)) {
return [firstFilteredScopes.first];
}
List<DartScope2> filteredScopes =
_filter(firstFilteredScopes, library, offset);
if (_allHaveTheSameDefinitions(filteredScopes)) {
return [filteredScopes.first];
}
return filteredScopes;
}
static List<DartScope2> filterAllForTesting(
List<DartScope2> rawScopes, Library library, int offset) {
return _filterAll(rawScopes, library, offset);
}
static List<DartScope2> _filterScopesWithArtificialNodes(
List<DartScope2> unfilteredScopes, Library library) {
Set<Member> skipMembers = {};
for (Extension node in library.extensions) {
for (ExtensionMemberDescriptor memberDescriptor
in node.memberDescriptors) {
// The tear off procedures have two enclosing function nodes with the
// same offsets, but with (possibly) different type parameters.
// Skip them.
Member? skip = memberDescriptor.tearOffReference?.asMember;
if (skip != null) skipMembers.add(skip);
}
}
List<DartScope2> filtered = [];
for (DartScope2 node in unfilteredScopes) {
Member? member = node.member;
if (skipMembers.contains(member)) {
// Skip these.
continue;
}
if (member is Field) {
if (member.isInternalImplementation) {
// E.g. synthesized fields added by late lowering. Skip those.
continue;
}
} else if (member is Procedure) {
if (member.isSynthetic) {
// Skip synthetic procedures.
continue;
}
}
filtered.add(node);
}
return filtered;
}
static bool _allHaveTheSameDefinitions(List<DartScope2> scopes) {
if (scopes.isEmpty) return false;
Map<String, VariableDeclaration> definitions = scopes.first.definitions;
for (int i = 1; i < scopes.length; i++) {
DartScope2 scope = scopes[i];
if (scope.definitions.length != definitions.length) return false;
for (MapEntry<String, VariableDeclaration> entry
in scope.definitions.entries) {
VariableDeclaration? existing = definitions[entry.key];
if (existing == null) {
return false;
} else {
if (existing != entry.value) {
return false;
}
}
}
}
return true;
}
static List<DartScope2> _filter(
List<DartScope2> unfilteredScopes, Library library, int offset) {
List<DartScope2> filtered = unfilteredScopes.toList();
List<DartScope2> withoutEndOffset = [];
for (DartScope2 scope in unfilteredScopes) {
TreeNode? node = scope.node;
// Possibly filter out nodes that was only included because their end
// offset matched the offset we we're looking for.
if (node is Member) {
if (offset != node.fileEndOffset) {
withoutEndOffset.add(scope);
}
} else if (node is FunctionNode) {
if (offset != node.fileEndOffset) {
withoutEndOffset.add(scope);
}
} else if (node is Block) {
if (offset != node.fileEndOffset) {
withoutEndOffset.add(scope);
}
} else {
withoutEndOffset.add(scope);
}
}
if (withoutEndOffset.length == 1) {
return withoutEndOffset;
} else if (withoutEndOffset.isEmpty) {
// They're all on the end offset. Just return the last one.
// E.g.
// ```
// static makeErrorHandler(_EventSink controller) =>
// (Object e, StackTrace s) {
// controller._addError(e, s);
// controller._close();
// };
// ```
// have both the Procedure, the first FunctionNode and the second
// FunctionNode with the same end offset.
return [filtered.last];
} else {
filtered = withoutEndOffset;
}
if ((filtered.length == 2 &&
filtered[0].node is ForInStatement &&
filtered[1].node is Block) ||
(filtered.length == 3 &&
filtered[0].node is ForInStatement &&
filtered[1].node is LabeledStatement &&
filtered[2].node is Block)) {
// In the below code the for in statement was included on the brace
// because of the body offset, but as we have the block that's what we
// should use, so we remove the ForInStatement.
// ```
// String? name = node.name;
// // [...]
// for (String name in combinator.names) { // <- this brace
// // whatnot
// }
// ```
return [filtered.last];
}
if (filtered.length == 2 &&
filtered[0].node is ForInStatement &&
filtered[1].node is YieldStatement) {
// E.g.
// ```
// for (var key in keys) yield MapEntry<K, V>(key, this[key] as dynamic);
// ```
// where the ForIn and the yield has the same (body) offset.
return [filtered[1]];
}
if (filtered.length > 2 &&
filtered.length < 5 &&
filtered[0].node is ForInStatement &&
filtered[1].node is ExpressionStatement) {
// E.g.
// ```
// for (var entry in entries) this[entry.key] = entry.value;
// ```
return [filtered.last];
}
if (filtered.length == 2 &&
filtered[0].node is FunctionNode &&
filtered[1].node is Block) {
// Remove the function node one(s) --- assume we're on the block
// to remove some uncertainty in for instance these situations:
// ```
// void foo(String? s) {
// if (s == null) return;
// bar(String s) {
// // whatever
// } // <- if the position is at this brace.
// bar(s);
// }
// ```
// and
// ```
// void bla() {
// void foo(String foo) {
// // Here "foo" is a string.
// } // <- if the position is at this brace.
// }
// ```
// In both situations we have two variables with the same name but
// different types, depending on if the positionals from the function node
// is "on" or not.
return [filtered[1]];
}
if (filtered.length == 2 &&
filtered[0].node is VariableDeclaration &&
filtered[1].node is FieldInitializer) {
// Pick the FieldInitializer (i.e. have the variable in scope).
return [filtered[1]];
}
if (filtered.length == 2 &&
filtered[0].node is VariableDeclaration &&
filtered[1].node is VariableGet &&
filtered[1].node.parent?.parent is SuperInitializer) {
// E.g. `Foo(super.bar)`.
// Pick the VariableGet (with all variables in scope --- this is not
// great if we can actually stop before, but can we?).
return [filtered[1]];
}
if (filtered.length == 2 &&
filtered[0].node is Constructor &&
filtered[1].node is SuperInitializer) {
// Pick the SuperInitializer (i.e. have any variables in scope).
return [filtered[1]];
}
if (filtered.length == 2 &&
filtered[0].node is VariableDeclaration &&
filtered[1].node is VariableGet &&
filtered[0].node.parent?.parent is Procedure &&
(filtered[0].node.parent?.parent as Procedure).isRedirectingFactory) {
return [filtered[1]];
}
if (filtered.length == 2 &&
filtered[0].node is FunctionNode &&
filtered[1].node is InvocationExpression &&
filtered[0].node.parent is Procedure &&
(filtered[0].node.parent as Procedure).isRedirectingFactory) {
return [filtered[1]];
}
if (filtered.length > 1 &&
filtered[0].node is Class &&
(filtered[0].node as Class).isEnum) {
return [filtered[0]];
}
if (filtered.length == 2 &&
filtered[0].node is VariableDeclaration &&
filtered[1].node is VariableGet &&
filtered[0].node.parent?.parent is Constructor &&
filtered[1].node.parent?.parent is Arguments) {
// E.g. `super.localId`.
return [filtered[1]];
}
if (filtered.length == 2 &&
filtered[0].node is IfStatement &&
filtered[1].node is Block) {
return [filtered[0]];
}
if (filtered.length > 1 &&
filtered[0].node is InstanceGet &&
(filtered[0].node as InstanceGet).receiver is VariableGet &&
((filtered[0].node as InstanceGet).receiver as VariableGet)
.variable
.isSynthesized) {
// E.g. `await for (var fileEntity in stream) { [...] }` that has become
// FileSystemEntity fileEntity = :for-iterator.{_StreamIterator.current};
// {
// [...]
// }
// where the getting of the iterator and the block has the same offset,
// pointing to the start brace after `fileEntity` was defined in the
// source. We remove the InstanceGet.
filtered.removeAt(0);
}
if (filtered.length == 1) return filtered;
DartScope2? lookup;
// TODO(jensj): Look into rewriting the matching to use patterns.
// See also
// https://dart-review.googlesource.com/c/sdk/+/338840/8/pkg/kernel/lib/dart_scope_calculator.dart#924
lookup = _looksLikeVmForInTransformation(filtered);
if (lookup != null) return [lookup];
lookup = _looksLikeVmFfiTransformation(filtered);
if (lookup != null) return [lookup];
lookup = _looksLikeLateLoweredField(filtered);
if (lookup != null) return [lookup];
lookup = _looksLikePatternMatching(filtered);
if (lookup != null) return [lookup];
lookup = _looksLikeLateLoweredLocal(filtered);
if (lookup != null) return [lookup];
lookup = _looksLikeThrowPatternMatchingError(filtered);
if (lookup != null) return [lookup];
return filtered;
}
static DartScope2? _looksLikeVmForInTransformation(
List<DartScope2> filtered) {
// The VM has a transformation where
// ```
// void foo(String x) {
// for(dynamic x in bar()) {
// if (x == null) continue;
// print(x);
// }
// }
// List bar() => [1, 2, 3];
// ```
// is transformed into something like
// ```
// {
// synthesized core::Iterator<dynamic> :sync-for-iterator =
// self::bar().{core::Iterable::iterator}{core::Iterator<dynamic>};
// for (;
// :sync-for-iterator.{core::Iterator::moveNext}(){() → core::bool}; )
// { // <- this block
// dynamic x = :sync-for-iterator.
// {core::Iterator::current}{dynamic}; // <- the initializer
// #L1: // <- this label
// { // <- this block
// if(x == null)
// break #L1;
// core::print(x);
// }
// }
// }
// ```
// where I've marked the 4 places where the offset is the same.
// In the first 2 cases the "x" will be String, in the other 2 the "x" will
// be dynamic.
// Try to filter this so that we say we're where "x" is dynamic.
if (filtered.length >= 3 &&
filtered.first.node is Block &&
filtered[1].node is InstanceGet) {
Block firstBlock = filtered.first.node as Block;
InstanceGet instanceGet = filtered[1].node as InstanceGet;
Expression receiver = instanceGet.receiver;
if (firstBlock.parent is ForStatement &&
receiver is VariableGet &&
receiver.variable.isSynthesized &&
receiver.variable.name == ":sync-for-iterator") {
// Matches the case. Return the last block.
return filtered.last;
}
}
return null;
}
static DartScope2? _looksLikeVmFfiTransformation(List<DartScope2> filtered) {
if (filtered.length > 4 &&
filtered[0].node is Procedure &&
filtered[2].node is Field &&
(filtered[2].node as Field).name.text.endsWith("\$FfiNative\$Ptr")) {
return filtered[0];
} else if (filtered.length > 4 &&
filtered[0].node is Procedure &&
filtered[2].node is Procedure &&
(filtered[2].node as Procedure).name.text.endsWith("\$FfiNative")) {
return filtered[0];
}
return null;
}
// TODO(jensj): Possibly move the file to package:front_end and use
// lowering_predicates.dart.
static DartScope2? _looksLikeLateLoweredField(List<DartScope2> filtered) {
if (filtered.length > 10 &&
filtered[0].node is Procedure &&
filtered[1].node is FunctionNode &&
filtered[2].node is ReturnStatement &&
filtered[3].node is Let &&
filtered[4].node is VariableDeclaration) {
// The 10 is not special. This has just been observed to contain lots of
// scopes.
return filtered[0];
} else if (filtered.length > 10 &&
filtered[0].node is Procedure &&
filtered[1].node is FunctionNode &&
filtered[2].node is ReturnStatement) {
// The 10 is not special. This has just been observed to contain lots of
// scopes.
for (int i = 3; i < filtered.length - 1; i++) {
if (filtered[i].node is Let &&
filtered[i + 1].node is VariableDeclaration) {
return filtered[0];
}
}
} else if (filtered.length > 15 &&
filtered[0].node is Procedure &&
filtered[1].node is FunctionNode) {
// The 15 is not special. This has just been observed to contain lots of
// scopes.
for (int i = 3; i < filtered.length - 1; i++) {
if (filtered[i].node is Procedure &&
filtered[i + 1].node is FunctionNode &&
filtered[i + 2].node is ReturnStatement) {
for (int j = i + 1; j < filtered.length - 1; j++) {
if (filtered[j].node is Let &&
filtered[j + 1].node is VariableDeclaration) {
return filtered[0];
}
}
}
}
}
// late final field.
if (filtered.length > 10 &&
filtered[0].node is Procedure &&
filtered[1].node is FunctionNode &&
filtered[2].node is ReturnStatement &&
filtered[3].node is ConditionalExpression &&
filtered[4].node is InstanceGet &&
filtered[5].node is ThisExpression &&
filtered[6].node is InstanceGet &&
filtered[7].node is ThisExpression &&
filtered[8].node is Throw) {
// The 10 is not special. This has just been observed to contain lots of
// scopes.
return filtered[0];
}
return null;
}
static DartScope2? _looksLikePatternMatching(List<DartScope2> filtered) {
if (filtered.length > 5 &&
filtered[0].node is VariableDeclaration &&
(filtered[0].node as VariableDeclaration).isHoisted) {
// The 5 is not special. This has just been observed to contain lots of
// scopes.
// Pattern matching looks something like this:
// ```
// hoisted variable1; // offset x
// hoisted variable2; // offset y
// // some initialization stuff for variable1. // offset x
// // some initialization stuff for variable2. // offset y
// if (stuff with variable1 ending in the setting of it /* offset x */ &&
// stuff with variable2 ending in the setting of it /* offset y */) {
// // body
// }
// ```
// Meaning that even if only seeing a hoisted variable as in scope after
// it has been written for position y in the above example we'll have
// variable1 in scope for ~half the scopes.
// We'll assume we've hit this case if we can find a LogicalExpression
// followed later by a VariableSet.
// We'll then pick the VariableSet as the scope.
int foundLogicalExpressionAt = -1;
for (int i = 1; i < filtered.length; i++) {
if (filtered[i].node is LogicalExpression) {
foundLogicalExpressionAt = i;
}
}
if (foundLogicalExpressionAt >= 0) {
for (int i = foundLogicalExpressionAt + 1; i < filtered.length; i++) {
if (filtered[i].node is VariableSet) {
return filtered[i];
}
}
}
}
if (filtered.length > 5 &&
filtered[0].node is VariableDeclaration &&
filtered[1].node is VariableDeclaration &&
filtered.last.node is LocalFunctionInvocation) {
// It's beginning to look a lot like a late lowered nullable pattern
// matching case.
VariableDeclaration variable1 = filtered[0].node as VariableDeclaration;
VariableDeclaration variable2 = filtered[1].node as VariableDeclaration;
if (variable1.isSynthesized &&
variable1.name?.startsWith("#") == true &&
variable2.isSynthesized &&
variable2.isLowered &&
variable2.name?.startsWith("#") == true) {
// Assume so. We'll pick the last one where we have previous variables
//that already matched in scope.
return filtered.last;
}
}
if (filtered.length <= 4 &&
filtered[0].node is VariableDeclaration &&
(filtered[0].node as VariableDeclaration).isHoisted &&
filtered.last.node is VariableSet &&
(filtered.last.node as VariableSet).variable == filtered[0].node) {
return filtered.last;
}
return null;
}
static DartScope2? _looksLikeLateLoweredLocal(List<DartScope2> filtered) {
VariableDeclaration? variable1;
VariableDeclaration? variable2;
if (filtered.length > 5 &&
filtered[0].node is VariableDeclaration &&
filtered[1].node is VariableDeclaration) {
// A nullable one, e.g. `late Foo? foo` becomes something like
// ```
// Foo? #foo;
// bool #foo#isSet = false;
// ```
variable1 = filtered[0].node as VariableDeclaration;
variable2 = filtered[1].node as VariableDeclaration;
} else if (filtered.length > 5 &&
filtered[0].node is VariableDeclaration &&
filtered[2].node is VariableDeclaration) {
// A non-nullable one, e.g. `late Foo foo` becomes something like
// ```
// Foo? #foo;
// Foo #foo#get() => bla bla
// ```
variable1 = filtered[0].node as VariableDeclaration;
variable2 = filtered[2].node as VariableDeclaration;
}
if (variable1 != null && variable2 != null) {
// isLateLoweredLocalName/isLateLoweredLocalSetter/etc is in the CFE so we
// can't call it from here.
if (variable1.isLowered &&
variable1.name?.startsWith("#") == true &&
variable2.isLowered &&
variable2.name?.startsWith("#") == true) {
// Assume it's a late lowering thing with an exuberant amount of nodes
// with the same offset. Just pick the first one.
return filtered[0];
}
}
return null;
}
static DartScope2? _looksLikeThrowPatternMatchingError(
List<DartScope2> filtered) {
if (filtered.length == 5 &&
filtered[0].node is IfStatement &&
filtered[1].node is ExpressionStatement &&
filtered[2].node is Throw &&
filtered[3].node is ConstructorInvocation &&
filtered[4].node is StringLiteral) {
// Something like
// `var (Foo? a, Foo? b) = someCall();`
// becomes something like
// ```
//if (!(checks for Foo? and lets with assigns etc))
// throw new StateError("Pattern matching error");
// ```
// Pick the if.
return filtered[0];
} else if (filtered.length == 6 &&
filtered[0].node is Block &&
filtered[1].node is IfStatement &&
filtered[2].node is ExpressionStatement &&
filtered[3].node is Throw &&
filtered[4].node is ConstructorInvocation &&
filtered[5].node is StringLiteral) {
// As above, but inside a block, e.g. if used in a for-in loop like
// ```
// for (var Foo(:Whatnot? bar, :Whatnot? baz ) in entries) { [...] }
// ```
// Pick the if.
return filtered[1];
}
return null;
}
static DartScopeBuilder2 _raw(
Library library, Uri scriptUri, Class? cls, int offset) {
DartScopeBuilder2 builder = DartScopeBuilder2._(library, scriptUri, offset);
if (cls != null) {
builder.visitClass(cls);
} else {
builder.checkClasses = false;
builder.visitLibrary(library);
}
return builder;
}
static DartScopeBuilder2 _rawNoClass(
Library library, Uri scriptUri, int offset) {
DartScopeBuilder2 builder = DartScopeBuilder2._(library, scriptUri, offset);
builder.visitLibrary(library);
return builder;
}
static List<DartScope2> findScopeFromOffsetAndClassRawForTesting(
Library library, Uri scriptUri, Class? cls, int offset) =>
_raw(library, scriptUri, cls, offset).findScopes;
}