blob: dd95ce3fa8435f913d5e77423807a52c2fe64523 [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:_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();
}
}