[analysis_server] Add helper functions to support flags enums

In the new interactive forms, some of the enums can be combined (eg. `FileExistence.New | FileExisting.Existing`). This adds a flag that produces some helper functions for `hasFlag()` and `combine()`.

These helpers are not used outside of tests in this CL but will be used in a future CL (which I'm trying to avoid getting too big to simplify reviewing).

Change-Id: I1b5f06f05d96781c4bc6a246a0a9309a786c1987
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505320
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Keerti Parthasarathy <keertip@google.com>
This commit is contained in:
Danny Tuppeny
2026-05-21 13:06:43 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 0e3e174444
commit 026043d9ed
7 changed files with 108 additions and 1 deletions
@@ -15,6 +15,30 @@ void main() {
ast.ArrayType _array(String name) => ast.ArrayType(_simple(name));
/// Helper to create a constant with defaults.
ast.Constant _constant(Object value, {String name = 'x', String? type}) {
return ast.Constant(
name: name,
type: type != null ? ast.TypeReference(type) : ast.TypeReference.lspAny,
value: value.toString(),
);
}
/// Helper to create a enum with defaults.
ast.LspEnum _enum({
String name = 'x',
String type = 'int',
bool flags = false,
required List<ast.Member> members,
}) {
return ast.LspEnum(
name: name,
typeOfValues: ast.TypeReference(type),
flags: flags,
members: members,
);
}
ast.TypeReference _simple(String name) => ast.TypeReference(name);
ast.UnionType _union(List<String> names) =>
@@ -22,6 +46,27 @@ ast.UnionType _union(List<String> names) =>
@reflectiveTest
class DartTest {
void test_flags_requiresPowerOfTwo_double() {
expect(
() => _enum(flags: true, members: [_constant(1.2)]),
throwsA(isA<AssertionError>()),
);
}
void test_flags_requiresPowerOfTwo_nonPowerOfTwo() {
expect(
() => _enum(flags: true, members: [_constant(3)]),
throwsA(isA<AssertionError>()),
);
}
void test_flags_requiresPowerOfTwo_string() {
expect(
() => _enum(flags: true, members: [_constant('test')]),
throwsA(isA<AssertionError>()),
);
}
void test_mapping_arrays() {
expect(_array('string').dartTypeWithTypeArgs, equals('List<String>'));
}
@@ -119,6 +119,23 @@ class GeneratedClassesTest {
expect(a.hashCode, equals(b.hashCode));
}
void test_generatedClasses_flagsEnum_combined() {
var combined = FileExistence.combine([
FileExistence.New,
FileExistence.Existing,
]);
expect(combined.toJson(), 3);
expect(combined.hasFlag(FileExistence.New), isTrue);
expect(combined.hasFlag(FileExistence.Existing), isTrue);
expect(combined.hasFlag(FileExistence(64)), isFalse);
}
void test_generatedClasses_flagsEnum_notCombined() {
expect(FileExistence.New.hasFlag(FileExistence.New), isTrue);
expect(FileExistence.New.hasFlag(FileExistence.Existing), isFalse);
}
void test_interactiveForms_deserialize_formFieldsIntoSubclasses() {
var stringField = FormField.fromJson({
'type': {'kind': 'string'},
@@ -686,6 +686,22 @@ void _writeEnumClass(IndentableStringBuffer buffer, LspEnum namespace) {
'static const $memberName = $namespaceName$constructorName($value);',
);
});
if (namespace.flags) {
buffer
..writeln()
..writeIndentedln(
'static $namespaceName combine(List<$namespaceName> values) =>',
)
..indent()
..writeIndentedln(
'$namespaceName$constructorName(values.fold<$dartType>(0, (combinedValue, value) => combinedValue | value._value));',
)
..outdent()
..writeln()
..writeIndentedln(
'bool hasFlag($namespaceName value) => (_value & value._value) == value._value;',
);
}
buffer
..writeln()
..writeIndentedln('@override $dartType toJson() => _value;')
@@ -136,6 +136,7 @@ final interactiveFormClasses = <LspEntity>[
LspEnum(
name: 'FileExistence',
typeOfValues: TypeReference.int,
flags: true,
members: [
// Values should be powers of 2 to allow New|Existing.
Constant(
@@ -159,6 +160,7 @@ final interactiveFormClasses = <LspEntity>[
LspEnum(
name: 'FileType',
typeOfValues: TypeReference.int,
flags: true,
members: [
// Values should be powers of 2 to allow Regular|Directory.
Constant(
@@ -23,6 +23,10 @@ bool isNullType(TypeBase t) =>
bool isObjectType(TypeBase t) =>
resolveTypeAlias(t).dartTypeWithTypeArgs == 'Object';
bool _isPowerOfTwo(int x) {
return x > 0 && (x & (x - 1)) == 0;
}
class AbstractGetter extends Member {
final TypeBase type;
@@ -178,14 +182,23 @@ abstract class LspEntity {
/// An enum parsed from the LSP JSON model.
class LspEnum extends LspEntity {
final TypeBase typeOfValues;
final bool flags;
final List<Member> members;
LspEnum({
required super.name,
super.comment,
super.isProposed,
required this.typeOfValues,
this.flags = false,
required this.members,
}) {
}) : assert(
!flags ||
members
.whereType<Constant>()
.map((member) => int.tryParse(member.value))
.every((value) => value != null && _isPowerOfTwo(value)),
'flags enums require all enum values to be int powers of two.',
) {
members.sortBy((member) => member.name.toLowerCase());
}
}
@@ -190,6 +190,7 @@ class LspMetaModelCleaner {
comment: _cleanComment(namespace.comment),
isProposed: namespace.isProposed,
typeOfValues: namespace.typeOfValues,
flags: namespace.flags,
members: namespace.members
.where(_includeEntityInOutput)
.map((member) => _cleanMember(namespace.name, member))
@@ -385,6 +386,7 @@ class LspMetaModelCleaner {
comment: comment,
isProposed: dest.isProposed,
typeOfValues: dest.typeOfValues,
flags: dest.flags || source.flags,
members: [...dest.members, ...source.members],
);
} else if (source is Interface && dest is Interface) {
@@ -530,6 +532,7 @@ class LspMetaModelCleaner {
comment: type.comment,
isProposed: type.isProposed,
typeOfValues: type.typeOfValues,
flags: type.flags,
members: type.members,
);
} else {
@@ -2244,6 +2244,8 @@ class FileExistence implements ToJsonable {
bool operator ==(Object other) =>
other is FileExistence && other._value == _value;
bool hasFlag(FileExistence value) => (_value & value._value) == value._value;
@override
int toJson() => _value;
@@ -2251,6 +2253,10 @@ class FileExistence implements ToJsonable {
String toString() => _value.toString();
static bool canParse(Object? obj, LspJsonReporter reporter) => obj is int;
static FileExistence combine(List<FileExistence> values) =>
FileExistence(values.fold<int>(
0, (combinedValue, value) => combinedValue | value._value));
}
/// FileType represents the expected filesystem resource type.
@@ -2274,6 +2280,8 @@ class FileType implements ToJsonable {
@override
bool operator ==(Object other) => other is FileType && other._value == _value;
bool hasFlag(FileType value) => (_value & value._value) == value._value;
@override
int toJson() => _value;
@@ -2281,6 +2289,9 @@ class FileType implements ToJsonable {
String toString() => _value.toString();
static bool canParse(Object? obj, LspJsonReporter reporter) => obj is int;
static FileType combine(List<FileType> values) => FileType(values.fold<int>(
0, (combinedValue, value) => combinedValue | value._value));
}
class FlutterOutline implements ToJsonable {