Field promotion: make the core promotability algorithm sharable; fix bugs

In the following code, it's not safe for the field `C._f` to undergo
type promotion, because a variable with static type `C` might have
type `D` at runtime, in which case `C._f` will get dispatched to
`noSuchMethod`, which is not guaranteed to return a stable result.

    class C {
      final int? _f;
    }
    class D implements C {
      noSuchMethod(_) => ...;
    }
    foo(C c) {
      if (c._f != null) {
        print(c._f + 1); // UNSAFE!
      }
    }

Therefore, in order to determine which fields are promotable, the
implementations need to analyze enough of the class hierarchy to
figure out which field accesses might get dispatched to
`noSuchMethod`.

Currently, the CFE does this by following its usual algorithm for
generating `noSuchMethod` forwarders before trying to determine which
fields are promotable. The analyzer, on the other hand, doesn't have
an algorithm for generating `noSuchMethod` forwarders (since it
doesn't implement execution semantics); so instead it has its own
logic to figure out when a `noSuchMethod` forwarder is needed for a
field, and disable promotion for that field.

But there's a chicken-and-egg problem in the CFE: the CFE needs to
determine which fields are promotable before doing top-level inference
(since the initializers of top-level fields might make use of field
promotion, affecting their inferred types--see #50522). But it doesn't
decide where `noSuchMethod` forwarders are needed until after
top-level inference (because the same phase that generates
`noSuchMethod` forwarders also generates forwarders that do runtime
covariant type-checking, and so it has to run after all top level
types have been inferred).

To fix the chicken-and-egg problem, I plan to rework the CFE so that
it uses the same algorithm as the analyzer to determine which fields
are promotable. This CL makes a first step towards that goal, by
reworking the analyzer's field promotability algorithm into a form
where it can be shared with the CFE, and moving it to
`package:_fe_analyzer_shared`.  Since this required a fairly
substantial rewrite, I went ahead and fixed #52938 in the process.

Fixes #52938.

Change-Id: I9e68f51b3ea9a967f55f15bdc445cc1c0efdabdd
Bug: https://github.com/dart-lang/sdk/issues/52938
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/313293
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
Paul Berry 2023-07-18 18:54:26 +00:00 committed by Commit Queue
parent 6b4337ab69
commit 130d6199c3
7 changed files with 834 additions and 267 deletions

View file

@ -0,0 +1,355 @@
// Copyright (c) 2022, 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:_fe_analyzer_shared/src/util/dependency_walker.dart';
/// Information about a [Class] that is necessary for computing the set of
/// private `noSuchMethod` getters the compiler will generate.
class ClassInfo<Class extends Object> {
/// The [_InterfaceNode] for the [Class].
final _InterfaceNode<Class> _interfaceNode;
/// The [_ImplementedNode] for the [Class].
final _ImplementedNode<Class> _implementedNode;
ClassInfo(this._interfaceNode, this._implementedNode);
}
/// This class examines all the [Class]es in a library and determines which
/// fields are promotable within that library.
///
/// Note: the term [Class] is used in a general sense here; it can represent any
/// class, mixin, or enum declared in the user's program.
///
/// A field is considered promotable if all of the following are true:
/// - It is final
/// - Its name is private
/// - The library doesn't contain any non-final fields, concrete getters, or
/// `noSuchMethod` getters with the same name.
///
/// These rules were chosen because they are sufficient to determine that all
/// reads of the field that complete normally will either throw an exception,
/// return a single stable object, or return objects with a single stable
/// runtime type; this ensures that type promotion is safe.
///
/// The reason the field must be private is because if it isn't, then at
/// runtime, a read of the field might resolve to a getter or a non-final field
/// in a subclass that's implemented in some other library, and hence the result
/// of the read won't be stable.
///
/// Note that as a result of the third bullet item, any private name that's
/// associated with a non-final field, concrete getter, or `noSuchMethod` getter
/// renders all fields with the same name unpromotable within the library. The
/// reason for this is that since most Dart classes can also be used as
/// interfaces (via an `implements` clause), a read that statically resolves to
/// a private final field might not actually resolve to that field at runtime;
/// it might resolve to some other field or getter with the same name. (In
/// principle it would be possible to narrow the set of possible resolution
/// targets through whole-program analysis, or by careful consideration of class
/// modifiers and public vs. private class names; however that would make the
/// analysis much more complex, and also make it difficult to explain to users
/// when to expect field promotion to work; so we've elected to simply assume
/// that all fields and getters with the same name might potentially alias to
/// one another).
///
/// Since all fields with the same name within the library have the same
/// promotability, this class doesn't attempt to assign a promotability boolean
/// to each field; instead it computes the set of private names for which field
/// promotion is not allowed.
///
/// A word about `noSuchMethod` getters: a `noSuchMethod` getter is a
/// `noSuchMethod` forwarder that implements a getter. The compiler generates
/// `noSuchMethod` forwarders when a concrete [Class] inherits a member into its
/// interface (via an `implements` clause), but it doesn't contain or inherit a
/// concrete implementation of that member. (Note that this is only legal if the
/// class contains or inherits an implementation of `noSuchMethod`). If the name
/// of the inherited member is accessible in the [Class]'s library (i.e. it's
/// either a public name or it's a private name that belongs to the [Class]'s
/// library), then the synthetic `noSuchMethod` forwarder calls `noSuchMethod`
/// and passes along the return value (casting it to the proper type). If the
/// name of the inherited member *isn't* accessible in the [Class]'s library,
/// then the synthetic `noSuchMethod` forwarder throws an exception.
///
/// `noSuchMethod` getters that forward to `noSuchMethod` defeat field promotion
/// for the same reason that ordinary getters do (they aren't guaranteed to
/// return a stable value). However, `noSuchMethod` getters that simply throw an
/// exception do not defeat field promotion, because the exception prevents any
/// code that relies on field promotion soundness from being reachable. Since
/// only private fields are eligible from promotion in the first place, this
/// means that it's only necessary to search the current library for
/// `noSuchMethod` getters that might defeat field promotion (because a
/// `noSuchMethod` getter in another library that's associated with a private
/// name in *this* library will always throw).
///
/// Note that it's possible that one class will have a final field with a given
/// private name, while another class will have a method with the same name (or
/// a `noSuchMethod` forwarder for such a method). If this happens, then an
/// attempt to read the field might at runtime resolve to a tear-off of the
/// corresponding method. This is ok (it's not necessary to suppress promotion
/// of the field), because even though the resulting tear-offs might not be
/// stable in the sense of always being identical, they will nonetheless always
/// have the same runtime type. Hence, we can completely ignore methods when
/// computing which fields in the library are promotable.
abstract class FieldPromotability<Class extends Object> {
/// The set of field names in the library that have been determined to be
/// unsafe to promote.
final Set<String> _unpromotableFieldNames = {};
/// Map from a [Class] object to the [_ImplementedNode] that records the names
/// of concrete fields and getters declared in or inherited by the [Class].
final Map<Class, _ImplementedNode<Class>> _implementedNodes =
new Map.identity();
/// Map from a [Class] object to the [_InterfaceNode] that records the names
/// of getters in the interface for a [Class].
final Map<Class, _InterfaceNode<Class>> _interfaceNodes = new Map.identity();
/// List of information about concrete [Class]es in the library.
final List<ClassInfo<Class>> _concreteInfoList = [];
/// Records the presence of [class_] in the library. The client should call
/// this method once for each class, mixin, and enum declared in the library.
///
/// Returns a [ClassInfo] object describing the class. After calling this
/// method, the client should call [addField] and [addGetter] to record all
/// the non-synthetic instance fields and getters in the class.
ClassInfo<Class> addClass(Class class_, {required bool isAbstract}) {
ClassInfo<Class> classInfo = new ClassInfo<Class>(
_getInterfaceNode(class_), _getImplementedNode(class_));
if (!isAbstract) {
_concreteInfoList.add(classInfo);
}
return classInfo;
}
/// Records that the [Class] described by [classInfo] contains a non-synthetic
/// instance field with the given [name].
///
/// [isFinal] indicates whether the field is a final field. [isAbstract]
/// indicates whether the field is abstract.
///
/// A return value of `true` indicates that this field *might* wind up being
/// promotable; a return value of `false` indicates that it *definitely* isn't
/// promotable.
bool addField(ClassInfo<Class> classInfo, String name,
{required bool isFinal, required bool isAbstract}) {
// Public fields are never promotable, so we may safely ignore fields with
// public names.
if (!name.startsWith('_')) {
return false;
}
// Record the field name for later use in computation of `noSuchMethod`
// getters.
classInfo._interfaceNode._directNames.add(name);
if (!isAbstract) {
classInfo._implementedNode._directNames.add(name);
}
if (!isFinal) {
// The field isn't final, so it isn't promotable, nor is any other field
// in the library with the same name.
_unpromotableFieldNames.add(name);
}
// If the field is final, it might wind up being promotable.
return isFinal;
}
/// Records that the [Class] described by [classInfo] contains a non-synthetic
/// instance getter with the given [name].
///
/// [isAbstract] indicates whether the getter is abstract.
void addGetter(ClassInfo<Class> classInfo, String name,
{required bool isAbstract}) {
// Public fields are never promotable, so we may safely ignore getters with
// public names.
if (!name.startsWith('_')) {
return;
}
// Record the getter name for later use in computation of `noSuchMethod`
// getters.
classInfo._interfaceNode._directNames.add(name);
if (!isAbstract) {
classInfo._implementedNode._directNames.add(name);
// The getter is concrete, so no fields with the same name are promotable.
_unpromotableFieldNames.add(name);
}
}
/// Computes the set of private field names which are not safe to promote in
/// the library.
///
/// The client should call this method once after all [Class]es, fields, and
/// getters have been recorded using [addClass], [addField], and [addGetter].
Set<String> computeUnpromotablePrivateFieldNames() {
// The names of private non-final fields and private getters have already
// been added to [_unpromotableFieldNames] by [addField] and [addGetter]. So
// all that remains to do is figure out which field names are unpromotable
// due to the presence of `noSuchMethod` getters. To do this, we'll need to
// walk the class hierarchy and gather (a) the names of private instance
// getters in each class's interface, and (b) the names of private instance
// fields and getters in each class's implementation.
_ClassHierarchyWalker<Class> interfaceWalker =
new _ClassHierarchyWalker<Class>();
_ClassHierarchyWalker<Class> implementedWalker =
new _ClassHierarchyWalker<Class>();
// For each concrete class in the library,
for (ClassInfo<Class> info in _concreteInfoList) {
// Compute names of getters in the interface.
_InterfaceNode<Class> interfaceNode = info._interfaceNode;
interfaceWalker.walk(interfaceNode);
Set<String> interfaceNames = interfaceNode._transitiveNames!;
// Compute names of actually implemented getters.
_ImplementedNode<Class> implementedNode = info._implementedNode;
implementedWalker.walk(implementedNode);
Set<String> implementedNames = implementedNode._transitiveNames!;
// `noSuchMethod` getters will be generated for getters that are in the
// interface, but not actually implemented; consequently, fields with
// these names are not safe to promote.
for (String name in interfaceNames) {
if (!implementedNames.contains(name)) {
_unpromotableFieldNames.add(name);
}
}
}
return _unpromotableFieldNames;
}
/// Returns an iterable of the direct superclasses of [class_]. If
/// [ignoreImplements] is `true`, then superclasses reached through an
/// `implements` clause are ignored.
///
/// This is used to gather the transitive closure of fields and getters
/// present in a class's interface and implementation. Therefore, it does not
/// matter whether the client uses a fully sugared model of mixins (in which
/// `class C extends B with M1, M2 {}` is represented as a single class with
/// direct superclasses `B`, `M1`, and `M2`) or a fully desugared model (in
/// which `class C extends B with M1, M2` represents a class `C` with
/// superclass `B&M1&M2`, which in turn has supertypes `B&M1` and `M2`, etc.)
Iterable<Class> getSuperclasses(Class class_,
{required bool ignoreImplements});
/// Gets or creates the [_ImplementedNode] for [class_].
_ImplementedNode<Class> _getImplementedNode(Class class_) =>
_implementedNodes[class_] ??= new _ImplementedNode<Class>(this, class_);
/// Gets or creates the [_InterfaceNode] for [class_].
_InterfaceNode<Class> _getInterfaceNode(Class class_) =>
_interfaceNodes[class_] ??= new _InterfaceNode<Class>(this, class_);
}
/// Dependency walker that traverses the graph of a class's type hierarchy,
/// gathering the transitive closure of field and getter names.
///
/// This is based on the [DependencyWalker] class, which implements Tarjan's
/// strongly connected component's algorithm in order to efficiently handle
/// cycles in the class hierarchy (which the analyzer tolerates).
class _ClassHierarchyWalker<Class extends Object>
extends DependencyWalker<_Node<Class>> {
@override
void evaluate(_Node<Class> v) => evaluateScc([v]);
@override
void evaluateScc(List<_Node<Class>> scc) {
// Gather the names directly declared in all the classes in this strongly
// connected component, plus all the names in the transitive closure of the
// strongly connected components this component depends on.
Set<String> transitiveNames = <String>{};
for (_Node<Class> node in scc) {
transitiveNames.addAll(node._directNames);
for (_Node<Class> dependency in Node.getDependencies(node)) {
Set<String>? namesFromDependency = dependency._transitiveNames;
if (namesFromDependency != null) {
transitiveNames.addAll(namesFromDependency);
}
}
}
// Store this list of names in all the nodes of this strongly connected
// component.
for (_Node<Class> node in scc) {
node._transitiveNames = transitiveNames;
}
}
}
/// Data structure tracking the set of private fields and getters a [Class]
/// concretely implements.
///
/// This data structure extends [_Node] so that we can efficiently walk the
/// superclass chain (without having to worry about circularities) in order to
/// include getters concretely implemented in superclasses and mixins.
class _ImplementedNode<Class extends Object> extends _Node<Class> {
_ImplementedNode(super.fieldPromotability, super.element);
@override
List<_Node<Class>> computeDependencies() {
// We need to gather field and getter names from the transitive closure of
// superclasses, following `with` and `extends` edges but not `implements`
// edges. So the set of dependencies of this node is the set of immediate
// superclasses, ignoring `implements`.
List<_Node<Class>> dependencies = [];
for (Class supertype in _fieldPromotability.getSuperclasses(_class,
ignoreImplements: true)) {
dependencies.add(_fieldPromotability._getImplementedNode(supertype));
}
return dependencies;
}
}
/// Data structure tracking the set of getters in a [Class]'s interface.
///
/// This data structure extends [_Node] so that we can efficiently walk the
/// class hierarchy (without having to worry about circularities) in order to
/// include getters defined in superclasses, mixins, and interfaces.
class _InterfaceNode<Class extends Object> extends _Node<Class> {
_InterfaceNode(super.fieldPromotability, super.element);
@override
List<_Node<Class>> computeDependencies() {
// We need to gather field and getter names from the transitive closure of
// superclasses, following `with`, `extends`, `implements`, and `on` edges.
// So the set of dependencies of this node is the set of immediate
// superclasses, including `implements`.
List<_Node<Class>> dependencies = [];
for (Class supertype in _fieldPromotability.getSuperclasses(_class,
ignoreImplements: false)) {
dependencies.add(_fieldPromotability._getInterfaceNode(supertype));
}
return dependencies;
}
}
/// A node in either the graph of a class's type hierarchy, recording the
/// information necessary for computing the set of private `noSuchMethod`
/// getters the compiler will generate.
abstract class _Node<Class extends Object> extends Node<_Node<Class>> {
/// A reference back to the [FieldPromotability] object.
final FieldPromotability<Class> _fieldPromotability;
/// The [Class] represented by this node.
final Class _class;
/// The names of getters declared by [_class] directly.
final Set<String> _directNames = {};
/// The names of getters declared by [_class] and its superinterfaces.
///
/// Populated when [_ClassHierarchyWalker] encounters a strongly connected
/// component.
Set<String>? _transitiveNames;
_Node(this._fieldPromotability, this._class);
@override
bool get isEvaluated => _transitiveNames != null;
}

View file

@ -16,6 +16,7 @@ dependencies:
# best practice for packages is to specify their compatible version ranges.
# See also https://dart.dev/tools/pub/dependencies.
dev_dependencies:
checks: any
test: any
dependency_overrides:

View file

@ -0,0 +1,197 @@
// 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:_fe_analyzer_shared/src/field_promotability.dart';
import 'package:checks/checks.dart';
import 'package:test/scaffolding.dart';
main() {
test('final private field is promotable', () {
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
check(_TestFieldPromotability().run([c])).unorderedEquals({});
check(f.isPossiblyPromotable).isTrue();
});
test('final public field is not promotable', () {
var f = Field('f', isFinal: true);
var c = Class(fields: [f]);
// Note that the set returned by `_TestFieldPromotability.run` is just the
// set of *private* field names that are unpromotable, so even though `f`
// is not promotable, the returned set is empty.
check(_TestFieldPromotability().run([c])).unorderedEquals({});
check(f.isPossiblyPromotable).isFalse();
});
test('non-final private field is not promotable', () {
var f = Field('_f');
var c = Class(fields: [f]);
check(_TestFieldPromotability().run([c])).unorderedEquals({'_f'});
check(f.isPossiblyPromotable).isFalse();
});
group('concrete getter renders a private field non-promotable:', () {
test('in a concrete class', () {
var c = Class(fields: [Field('_f', isFinal: true)]);
var d = Class(getters: [Getter('_f')]);
check(_TestFieldPromotability().run([c, d])).unorderedEquals({'_f'});
});
test('in an abstract class', () {
var c = Class(fields: [Field('_f', isFinal: true)]);
var d = Class(isAbstract: true, getters: [Getter('_f')]);
check(_TestFieldPromotability().run([c, d])).unorderedEquals({'_f'});
});
});
test('abstract getter does not render a private field non-promotable', () {
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(isAbstract: true, getters: [Getter('_f', isAbstract: true)]);
check(_TestFieldPromotability().run([c, d])).unorderedEquals({});
check(f.isPossiblyPromotable).isTrue();
});
test('public concrete getter is ignored', () {
// Since public fields are never promotable, there's no need for the
// algorithm to keep track of public concrete getters.
var f = Field('f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(getters: [Getter('f')]);
// Therefore the set returned by `_TestFieldPromotability.run` is empty.
check(_TestFieldPromotability().run([c, d])).unorderedEquals({});
check(f.isPossiblyPromotable).isFalse();
});
group('unimplemented getter renders a field non-promotable:', () {
test('induced by getter', () {
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d =
Class(isAbstract: true, getters: [Getter('_f', isAbstract: true)]);
var e = Class(implements: [d]);
check(_TestFieldPromotability().run([c, d, e])).unorderedEquals({'_f'});
});
test('induced by field', () {
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(isAbstract: true, fields: [Field('_f', isFinal: true)]);
var e = Class(implements: [d]);
check(_TestFieldPromotability().run([c, d, e])).unorderedEquals({'_f'});
});
});
test('unimplemented getter in an abstract class is ok', () {
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(isAbstract: true, getters: [Getter('_f', isAbstract: true)]);
var e = Class(isAbstract: true, implements: [d]);
check(_TestFieldPromotability().run([c, d, e])).unorderedEquals({});
check(f.isPossiblyPromotable).isTrue();
});
test('unimplemented abstract field renders a field non-promotable:', () {
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(
isAbstract: true,
fields: [Field('_f', isAbstract: true, isFinal: true)]);
var e = Class(extendsOrMixesIn: [d]);
check(_TestFieldPromotability().run([c, d, e])).unorderedEquals({'_f'});
});
test('implementations are inherited transitively', () {
// `e` inherits `f` from `c` via `d`, so no `noSuchMethod` forwarder is
// needed, and therefore promotion is allowed.
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(extendsOrMixesIn: [c]);
var e = Class(extendsOrMixesIn: [d], implements: [c]);
check(_TestFieldPromotability().run([c, d, e])).unorderedEquals({});
check(f.isPossiblyPromotable).isTrue();
});
test('interfaces are inherited transitively', () {
// `e` inherits the interface for `f` from `c` via `d`, so a `noSuchMethod`
// forwarder is needed, and therefore promotion is not allowed.
var f = Field('_f', isFinal: true);
var c = Class(fields: [f]);
var d = Class(isAbstract: true, implements: [c]);
var e = Class(implements: [d]);
check(_TestFieldPromotability().run([c, d, e])).unorderedEquals({'_f'});
});
test('class hierarchy circularities are handled', () {
// Since it's a compile error to have a circularity in the class hierarchy,
// all we need to check is that the algorithm terminates; we don't check the
// result.
var c = Class(extendsOrMixesIn: []);
var d = Class(extendsOrMixesIn: [c]);
c.extendsOrMixesIn.add(d);
var e = Class(extendsOrMixesIn: [d]);
_TestFieldPromotability().run([c, d, e]);
});
}
class Class {
final List<Class> extendsOrMixesIn;
final List<Class> implements;
final bool isAbstract;
final List<Field> fields;
final List<Getter> getters;
Class(
{this.extendsOrMixesIn = const [],
this.implements = const [],
this.isAbstract = false,
this.fields = const [],
this.getters = const []});
}
class Field {
final String name;
final bool isFinal;
final bool isAbstract;
late final bool isPossiblyPromotable;
Field(this.name, {this.isFinal = false, this.isAbstract = false});
}
class Getter {
final String name;
final bool isAbstract;
Getter(this.name, {this.isAbstract = false});
}
class _TestFieldPromotability extends FieldPromotability<Class> {
@override
Iterable<Class> getSuperclasses(Class class_,
{required bool ignoreImplements}) {
if (ignoreImplements) {
return class_.extendsOrMixesIn;
} else {
return [...class_.extendsOrMixesIn, ...class_.implements];
}
}
Set<String> run(Iterable<Class> classes) {
// Iterate through all the classes, enums, and mixins in the library,
// recording the non-synthetic instance fields and getters of each.
for (var class_ in classes) {
var classInfo = addClass(class_, isAbstract: class_.isAbstract);
for (var field in class_.fields) {
field.isPossiblyPromotable = addField(classInfo, field.name,
isFinal: field.isFinal, isAbstract: field.isAbstract);
}
for (var getter in class_.getters) {
addGetter(classInfo, getter.name, isAbstract: getter.isAbstract);
}
}
// Compute the set of field names that are not promotable.
return computeUnpromotablePrivateFieldNames();
}
}

View file

@ -1,265 +0,0 @@
// Copyright (c) 2022, 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:_fe_analyzer_shared/src/util/dependency_walker.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/summary2/library_builder.dart';
import 'package:collection/collection.dart';
/// This class computes which fields are promotable in a library.
class FieldPromotability {
final LibraryBuilder _libraryBuilder;
/// Fields that might be promotable, if not marked unpromotable later.
final List<FieldElementImpl> _potentiallyPromotableFields = [];
/// The set of field names that are not safe to promote.
/// 1. There is a non-final field with this name.
/// 2. There is a concrete getter with this name.
/// 3. Has a `noSuchMethod` forwarder.
final Set<String> _unpromotableFieldNames = {};
/// Getters actually implemented by the key element.
final Map<InterfaceElement, _ImplementedNode> _implementedNodes =
Map.identity();
/// Getters in the interface of the key element.
final Map<InterfaceElement, _InterfaceNode> _interfaceNodes = Map.identity();
/// Information about concrete [InterfaceElement]s.
final List<_InterfaceElementInfo> _concreteInfoList = [];
FieldPromotability(this._libraryBuilder);
void perform() {
for (var unitElement in _libraryBuilder.element.units) {
for (var class_ in unitElement.classes) {
_addInterfaceElement(class_, isAbstract: class_.isAbstract);
}
for (var enum_ in unitElement.enums) {
_addInterfaceElement(enum_, isAbstract: false);
}
for (var mixin_ in unitElement.mixins) {
_addInterfaceElement(mixin_, isAbstract: true);
}
}
var interfaceWalker = _Walker();
var implementedWalker = _Walker();
for (var info in _concreteInfoList) {
// Compute names of getters in the interface.
var interfaceNode = info._interfaceNode;
interfaceWalker.walk(interfaceNode);
var interfaceNames = interfaceNode._transitiveNames!;
// Compute names of actually implemented getters.
var implementedNode = info._implementedNode;
implementedWalker.walk(implementedNode);
var implementedNames = implementedNode._transitiveNames!;
// noSuchMethod forwarders will be generated for getters that are
// in the interface, but not actually implemented; consequently,
// fields with these names are not safe to promote.
for (var name in interfaceNames) {
if (!implementedNames.contains(name)) {
_unpromotableFieldNames.add(name);
}
}
}
for (var field in _potentiallyPromotableFields) {
if (!_unpromotableFieldNames.contains(field.name)) {
field.isPromotable = true;
}
}
}
void _addInterfaceElement(
InterfaceElement element, {
required bool isAbstract,
}) {
var interfaceInfo = _InterfaceElementInfo(this, element,
_getInterfaceNode(element), _getImplementedNode(element));
if (!isAbstract) {
_concreteInfoList.add(interfaceInfo);
}
}
/// Gets or creates the [_ImplementedNode] for [element].
_ImplementedNode _getImplementedNode(InterfaceElement element) =>
_implementedNodes[element] ??= _ImplementedNode(this, element);
/// If the [element] is not in the [_libraryBuilder], returns `null`.
/// Otherwise, invokes [_getImplementedNode].
_ImplementedNode? _getImplementedNodeOrNull(InterfaceElement element) {
if (element.library == _libraryBuilder.element) {
return _getImplementedNode(element);
}
return null;
}
/// Gets or creates the [_InterfaceNode] for [element].
_InterfaceNode _getInterfaceNode(InterfaceElement element) =>
_interfaceNodes[element] ??= _InterfaceNode(this, element);
/// If the [element] is not in the [_libraryBuilder], returns `null`.
/// Otherwise, invokes [_getInterfaceNode].
_InterfaceNode? _getInterfaceNodeOrNull(InterfaceElement element) {
if (element.library == _libraryBuilder.element) {
return _getInterfaceNode(element);
}
return null;
}
}
/// Data structure tracking the set of getters a class concretely implements.
///
/// This data structure extends [_Node] so that we can efficiently walk the
/// superclass chain (without having to worry about circularities) in order to
/// include getters concretely implemented in superclasses and mixins.
class _ImplementedNode extends _Node {
_ImplementedNode(super.fieldPromotability, super.element);
@override
List<_Node> computeDependencies() {
return [_element.supertype, ..._element.mixins]
.whereNotNull()
.map((type) => type.element)
.map(_fieldPromotability._getImplementedNodeOrNull)
.whereNotNull()
.toList();
}
}
/// Information about an [InterfaceElement].
class _InterfaceElementInfo {
final FieldPromotability _fieldPromotability;
final InterfaceElement _element;
final _InterfaceNode _interfaceNode;
final _ImplementedNode _implementedNode;
_InterfaceElementInfo(
this._fieldPromotability,
this._element,
this._interfaceNode,
this._implementedNode,
) {
for (var field in _element.fields) {
field as FieldElementImpl;
_addFieldElement(field);
}
for (var accessor in _element.accessors) {
_addPropertyAccessorElement(accessor);
}
}
void _addFieldElement(FieldElementImpl element) {
if (element.isStatic || element.isSynthetic) {
return;
}
var name = element.name;
if (!name.startsWith('_')) {
return;
}
_interfaceNode._directNames.add(name);
_implementedNode._directNames.add(name);
if (element.isFinal) {
_fieldPromotability._potentiallyPromotableFields.add(element);
} else {
_fieldPromotability._unpromotableFieldNames.add(name);
}
}
void _addPropertyAccessorElement(PropertyAccessorElement element) {
if (!element.isGetter || element.isStatic || element.isSynthetic) {
return;
}
var name = element.name;
if (!name.startsWith('_')) {
return;
}
_interfaceNode._directNames.add(name);
if (!element.isAbstract) {
_fieldPromotability._unpromotableFieldNames.add(name);
_implementedNode._directNames.add(name);
}
}
}
/// Data structure tracking the set of getters in a class's interface.
///
/// This data structure extends [_Node] so that we can efficiently walk the
/// class hierarchy (without having to worry about circularities) in order to
/// include getters defined in superclasses, mixins, and interfaces.
class _InterfaceNode extends _Node {
_InterfaceNode(super.fieldPromotability, super.element);
@override
List<_Node> computeDependencies() {
var directInterfaces = [
_element.supertype,
..._element.mixins,
..._element.interfaces
];
return directInterfaces
.whereNotNull()
.map((type) => type.element)
.map(_fieldPromotability._getInterfaceNodeOrNull)
.whereNotNull()
.toList();
}
}
/// Dependency walker node allowing us to efficiently walk the class hierarchy
/// and accumulate getter names.
abstract class _Node extends Node<_Node> {
/// A reference back to the [FieldPromotability] object.
final FieldPromotability _fieldPromotability;
/// The element represented by this node.
final InterfaceElement _element;
/// The names of getters declared by [_element] directly.
final Set<String> _directNames = {};
/// The names of getters declared by [_element] and its superinterfaces.
Set<String>? _transitiveNames;
_Node(this._fieldPromotability, this._element);
@override
bool get isEvaluated => _transitiveNames != null;
}
class _Walker extends DependencyWalker<_Node> {
@override
void evaluate(_Node v) => evaluateScc([v]);
@override
void evaluateScc(List<_Node> scc) {
var transitiveNames = <String>{};
for (var node in scc) {
transitiveNames.addAll(node._directNames);
for (var dependency in Node.getDependencies(node)) {
var namesFromDependency = dependency._transitiveNames;
if (namesFromDependency != null) {
transitiveNames.addAll(namesFromDependency);
}
}
}
for (var node in scc) {
node._transitiveNames = transitiveNames;
}
}
}

View file

@ -2,6 +2,7 @@
// 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:_fe_analyzer_shared/src/field_promotability.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/element/element.dart';
@ -19,7 +20,6 @@ import 'package:analyzer/src/summary2/constructor_initializer_resolver.dart';
import 'package:analyzer/src/summary2/default_value_resolver.dart';
import 'package:analyzer/src/summary2/element_builder.dart';
import 'package:analyzer/src/summary2/export.dart';
import 'package:analyzer/src/summary2/field_promotability.dart';
import 'package:analyzer/src/summary2/link.dart';
import 'package:analyzer/src/summary2/macro_application.dart';
import 'package:analyzer/src/summary2/metadata_resolver.dart';
@ -279,7 +279,7 @@ class LibraryBuilder {
return;
}
FieldPromotability(this).perform();
_FieldPromotability(this).perform();
}
void declare(String name, Reference reference) {
@ -998,6 +998,95 @@ class LinkingUnit {
});
}
/// This class examines all the [InterfaceElement]s in a library and determines
/// which fields are promotable within that library.
class _FieldPromotability extends FieldPromotability<InterfaceElement> {
/// The [_libraryBuilder] for the library being analyzed.
final LibraryBuilder _libraryBuilder;
/// Fields that might be promotable, if not marked unpromotable later.
final List<FieldElementImpl> _potentiallyPromotableFields = [];
_FieldPromotability(this._libraryBuilder);
@override
Iterable<InterfaceElement> getSuperclasses(InterfaceElement class_,
{required bool ignoreImplements}) {
List<InterfaceElement> result = [];
var supertype = class_.supertype;
if (supertype != null) {
result.add(supertype.element);
}
for (var m in class_.mixins) {
result.add(m.element);
}
if (!ignoreImplements) {
for (var interface in class_.interfaces) {
result.add(interface.element);
}
if (class_ is MixinElement) {
for (var constraint in class_.superclassConstraints) {
result.add(constraint.element);
}
}
}
return result;
}
/// Computes which fields are promotable and updates their `isPromotable`
/// properties accordingly.
void perform() {
// Iterate through all the classes, enums, and mixins in the library,
// recording the non-synthetic instance fields and getters of each.
for (var unitElement in _libraryBuilder.element.units) {
for (var class_ in unitElement.classes) {
_handleMembers(addClass(class_, isAbstract: class_.isAbstract), class_);
}
for (var enum_ in unitElement.enums) {
_handleMembers(addClass(enum_, isAbstract: false), enum_);
}
for (var mixin_ in unitElement.mixins) {
_handleMembers(addClass(mixin_, isAbstract: true), mixin_);
}
}
// Compute the set of field names that are not promotable.
var unpromotableFieldNames = computeUnpromotablePrivateFieldNames();
// Set the `isPromotable` bit for each field element that *is* promotable.
for (var field in _potentiallyPromotableFields) {
if (!unpromotableFieldNames.contains(field.name)) {
field.isPromotable = true;
}
}
}
/// Records all the non-synthetic instance fields and getters of [class_] into
/// [classInfo].
void _handleMembers(
ClassInfo<InterfaceElement> classInfo, InterfaceElementImpl class_) {
for (var field in class_.fields) {
if (field.isStatic || field.isSynthetic) {
continue;
}
var isPotentiallyPromotable = addField(classInfo, field.name,
isFinal: field.isFinal, isAbstract: field.isAbstract);
if (isPotentiallyPromotable) {
_potentiallyPromotableFields.add(field);
}
}
for (var accessor in class_.accessors) {
if (!accessor.isGetter || accessor.isStatic || accessor.isSynthetic) {
continue;
}
addGetter(classInfo, accessor.name, isAbstract: accessor.isAbstract);
}
}
}
class _FlushElementOffsets extends GeneralizingElementVisitor<void> {
@override
void visitElement(covariant ElementImpl element) {

View file

@ -115,6 +115,49 @@ PropertyAccess
''');
}
test_class_field_abstract() async {
// Even though an abstract non-final field is just syntactic sugar for an
// abstract getter/setter pair (and thus in principle shouldn't prevent
// promotion), there's no way to implement it without introducing either a
// getter or a non-final field (either of which would prevent promotion). So
// the implementation goes ahead and prevents promotion even if there's no
// implementation yet, to reduce churn for the user.
await assertNoErrorsInCode('''
abstract class B {
abstract int? _foo;
}
// Suppress "unused field" warning on `B._foo`.
int? f(B b) => b._foo;
class C {
final int? _foo;
C(this._foo);
}
void g(C c) {
if (c._foo != null) {
c._foo;
}
}
''');
var node = findNode.prefixed('c._foo;');
assertResolvedNodeText(node, r'''
PrefixedIdentifier
prefix: SimpleIdentifier
token: c
staticElement: self::@function::g::@parameter::c
staticType: C
period: .
identifier: SimpleIdentifier
token: _foo
staticElement: self::@class::C::@getter::_foo
staticType: int?
staticElement: self::@class::C::@getter::_foo
staticType: int?
''');
}
test_class_field_invocation_prefixedIdentifier_nullability() async {
await assertNoErrorsInCode('''
class C {
@ -669,6 +712,105 @@ PrefixedIdentifier
''');
}
test_implemented_via_other_library() async {
// When determining the set of fields/getters in a class's implementation,
// it's necessary to traverse the whole class hierarchy, including classes
// outside the current library, because a class outside the current library
// may extend a class inside the current library. In the example below,
// `c._foo` is promotable because class E doesn't contain any `noSuchMethod`
// getters, due to the fact that it inherits an implementation of `_foo`
// from `C` via `D`.
newFile('$testPackageLibPath/other.dart', '''
import 'test.dart';
class D extends C {
D(super.foo);
}
''');
await assertNoErrorsInCode('''
import 'other.dart';
class C {
final int? _foo;
C(this._foo);
}
class E extends D implements C {
E(super.foo);
noSuchMethod(_) => 12345;
}
void f(C c) {
if (c._foo != null) {
c._foo;
}
}
''');
var node = findNode.prefixed('c._foo;');
assertResolvedNodeText(node, r'''
PrefixedIdentifier
prefix: SimpleIdentifier
token: c
staticElement: self::@function::f::@parameter::c
staticType: C
period: .
identifier: SimpleIdentifier
token: _foo
staticElement: self::@class::C::@getter::_foo
staticType: int
staticElement: self::@class::C::@getter::_foo
staticType: int
''');
}
test_interface_via_other_library() async {
// When determining the set of fields/getters in a class's interface, it's
// necessary to traverse the whole class hierarchy, including classes
// outside the current library, because a class outside the current library
// may extend or implement a class inside the current library. In the
// example below, `c._foo` is not promotable because class E contains a
// `noSuchMethod` getter for `_foo`, due to the fact that its interface
// inherits `_foo` from `C` via `D`.
newFile('$testPackageLibPath/other.dart', '''
import 'test.dart';
class D extends C {
D(super.foo);
}
''');
await assertNoErrorsInCode('''
import 'other.dart';
class C {
final int? _foo;
C(this._foo);
}
class E implements D {
noSuchMethod(_) => 12345;
}
void f(C c) {
if (c._foo != null) {
c._foo;
}
}
''');
var node = findNode.prefixed('c._foo;');
assertResolvedNodeText(node, r'''
PrefixedIdentifier
prefix: SimpleIdentifier
token: c
staticElement: self::@function::f::@parameter::c
staticType: C
period: .
identifier: SimpleIdentifier
token: _foo
staticElement: self::@class::C::@getter::_foo
staticType: int?
staticElement: self::@class::C::@getter::_foo
staticType: int?
''');
}
test_language219() async {
await assertNoErrorsInCode('''
// @dart = 2.19
@ -703,6 +845,45 @@ PropertyAccess
''');
}
test_mixin_on_clause() async {
// The type mentioned in a a mixin's "on" clause contributes to its
// interface. This needs to be accounted for when determining whether a
// `noSuchMethod` getter will be synthesized. In the example below,
// `c._foo` is not promotable because class D contains a `noSuchMethod`
// getter for `_foo`.
await assertNoErrorsInCode('''
mixin M on C {}
class C {
final int? _foo;
C(this._foo);
}
class D implements M {
noSuchMethod(_) => 12345;
}
void f(C c) {
if (c._foo != null) {
c._foo;
}
}
''');
var node = findNode.prefixed('c._foo;');
assertResolvedNodeText(node, r'''
PrefixedIdentifier
prefix: SimpleIdentifier
token: c
staticElement: self::@function::f::@parameter::c
staticType: C
period: .
identifier: SimpleIdentifier
token: _foo
staticElement: self::@class::C::@getter::_foo
staticType: int?
staticElement: self::@class::C::@getter::_foo
staticType: int?
''');
}
test_super_get() async {
await assertNoErrorsInCode('''
class B {

View file

@ -173,6 +173,7 @@ buffers
buggy
builder`s
bulk
bullet
bump
bypassing
c
@ -366,6 +367,7 @@ deepest
deeply
def
defaulting
defeat
degrades
degree
del
@ -469,6 +471,7 @@ efficient
efficiently
eight
eighth
elected
elem
eliminating
elt
@ -514,6 +517,7 @@ estimation
et
eval
evar
examines
exchanging
execute
executor
@ -984,6 +988,7 @@ nnbd
node's
nomenclature
nominality
nonetheless
nonexistent
nonimplementation
nonzero
@ -1288,6 +1293,7 @@ removal
remover
renames
render
renders
reordered
reparse
repeating
@ -1528,6 +1534,7 @@ subtraction
subtracts
suffixed
suffixing
sugared
suggests
suite
sum
@ -1615,6 +1622,7 @@ tokenized
tokenizer
tolerate
tolerated
tolerates
toplevel
topological
tops
@ -1629,6 +1637,7 @@ transforming
translation
traversal
traverse
traverses
traversing
trees
tricky