| // 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:_fe_analyzer_shared/src/flow_analysis/flow_analysis_operations.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]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo).isEmpty(); |
| check(f.nonPromotabilityReason).equals(null); |
| }); |
| |
| test('final public field is not promotable', () { |
| var f = Field('f', isFinal: true); |
| var c = Class(fields: [f]); |
| // Note that the map returned by `_TestFieldPromotability.run` is just the |
| // map of *private* field names that are unpromotable, so even though `f` |
| // is not promotable, the returned map is empty. |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo).isEmpty(); |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotPrivate); |
| }); |
| |
| test('non-final private field is not promotable', () { |
| var f = Field('_f'); |
| var c = Class(fields: [f]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check(nonPromotabilityInfo['_f']!.conflictingFields).unorderedEquals([f]); |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotFinal); |
| }); |
| |
| test('public dominates non-final', () { |
| // If a field is both public and non-final, the fact that it is public is |
| // used as the non-promotability reason. |
| var f = Field('f'); |
| var c = Class(fields: [f]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo.keys).isEmpty; |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotPrivate); |
| }); |
| |
| test('external private final field is not promotable', () { |
| var f = Field('_f', isFinal: true, isExternal: true); |
| var c = Class(fields: [f]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isExternal); |
| }); |
| |
| test('public dominates external', () { |
| // If a field is both public and external, the fact that it is public is |
| // used as the non-promotability reason. |
| var f = Field('f', isFinal: true, isExternal: true); |
| var c = Class(fields: [f]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo.keys).isEmpty; |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotPrivate); |
| }); |
| |
| test('external dominates non-final', () { |
| // If a field is both external and non-final, the fact that it is external |
| // is used as the non-promotability reason. |
| var f = Field('_f', isFinal: false, isExternal: true); |
| var c = Class(fields: [f]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isExternal); |
| }); |
| |
| group('concrete getter renders a private field non-promotable:', () { |
| test('in a concrete class', () { |
| var c = Class(fields: [Field('_f', isFinal: true)]); |
| var getter = Getter('_f'); |
| var d = Class(getters: [getter]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| nonPromotabilityInfo['_f']!.conflictingGetters, |
| ).unorderedEquals([getter]); |
| check( |
| getter.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotField); |
| }); |
| |
| test('in an abstract class', () { |
| var c = Class(fields: [Field('_f', isFinal: true)]); |
| var getter = Getter('_f'); |
| var d = Class(isAbstract: true, getters: [getter]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| nonPromotabilityInfo['_f']!.conflictingGetters, |
| ).unorderedEquals([getter]); |
| check( |
| getter.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotField); |
| }); |
| }); |
| |
| test('abstract getter does not render a private field non-promotable', () { |
| var f = Field('_f', isFinal: true); |
| var c = Class(fields: [f]); |
| var getter = Getter('_f', isAbstract: true); |
| var d = Class(isAbstract: true, getters: [getter]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d]); |
| check(nonPromotabilityInfo).isEmpty(); |
| check(f.nonPromotabilityReason).equals(null); |
| check(getter.nonPromotabilityReason).equals(null); |
| }); |
| |
| 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 getter = Getter('f'); |
| var d = Class(getters: [getter]); |
| // Therefore the map returned by `_TestFieldPromotability.run` is empty. |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d]); |
| check(nonPromotabilityInfo).isEmpty(); |
| check( |
| f.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotPrivate); |
| check( |
| getter.nonPromotabilityReason, |
| ).equals(PropertyNonPromotabilityReason.isNotPrivate); |
| }); |
| |
| group('unimplemented getter renders a field non-promotable:', () { |
| test('induced by getter', () { |
| var f = Field('_f', isFinal: true); |
| var c = Class(fields: [f]); |
| var getter = Getter('_f', isAbstract: true); |
| var d = Class(isAbstract: true, getters: [getter]); |
| var e = Class(implements: [d]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| nonPromotabilityInfo['_f']!.conflictingNsmClasses, |
| ).unorderedEquals([e]); |
| check(getter.nonPromotabilityReason).equals(null); |
| }); |
| |
| 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]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| nonPromotabilityInfo['_f']!.conflictingNsmClasses, |
| ).unorderedEquals([e]); |
| }); |
| }); |
| |
| test('unimplemented getter in an abstract class is ok', () { |
| var f = Field('_f', isFinal: true); |
| var c = Class(fields: [f]); |
| var getter = Getter('_f', isAbstract: true); |
| var d = Class(isAbstract: true, getters: [getter]); |
| var e = Class(isAbstract: true, implements: [d]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]); |
| check(nonPromotabilityInfo).isEmpty(); |
| check(f.nonPromotabilityReason).equals(null); |
| check(getter.nonPromotabilityReason).equals(null); |
| }); |
| |
| 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]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| nonPromotabilityInfo['_f']!.conflictingNsmClasses, |
| ).unorderedEquals([e]); |
| }); |
| |
| 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]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]); |
| check(nonPromotabilityInfo).isEmpty(); |
| check(f.nonPromotabilityReason).equals(null); |
| }); |
| |
| 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]); |
| var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]); |
| check(nonPromotabilityInfo.keys).unorderedEquals({'_f'}); |
| check( |
| nonPromotabilityInfo['_f']!.conflictingNsmClasses, |
| ).unorderedEquals([e]); |
| }); |
| |
| 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; |
| final bool isExternal; |
| late final PropertyNonPromotabilityReason? nonPromotabilityReason; |
| |
| Field( |
| this.name, { |
| this.isFinal = false, |
| this.isAbstract = false, |
| this.isExternal = false, |
| }); |
| } |
| |
| class Getter { |
| final String name; |
| final bool isAbstract; |
| late final PropertyNonPromotabilityReason? nonPromotabilityReason; |
| |
| Getter(this.name, {this.isAbstract = false}); |
| } |
| |
| class _TestFieldPromotability extends FieldPromotability<Class, Field, Getter> { |
| @override |
| Iterable<Class> getSuperclasses( |
| Class class_, { |
| required bool ignoreImplements, |
| }) { |
| if (ignoreImplements) { |
| return class_.extendsOrMixesIn; |
| } else { |
| return [...class_.extendsOrMixesIn, ...class_.implements]; |
| } |
| } |
| |
| Map<String, FieldNameNonPromotabilityInfo<Class, Field, Getter>> 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.nonPromotabilityReason = addField( |
| classInfo, |
| field, |
| field.name, |
| isFinal: field.isFinal, |
| isAbstract: field.isAbstract, |
| isExternal: field.isExternal, |
| ); |
| } |
| for (var getter in class_.getters) { |
| getter.nonPromotabilityReason = addGetter( |
| classInfo, |
| getter, |
| getter.name, |
| isAbstract: getter.isAbstract, |
| ); |
| } |
| } |
| |
| // Compute field non-promotability info. |
| return computeNonPromotabilityInfo(); |
| } |
| } |