Always verify constants in LspEnum.

Change-Id: I4d62eacbf97c5f83e0eab378bc7d239ab934e6b8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505623
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Konstantin Shcheglov
2026-05-22 08:59:05 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent dfa4478902
commit d6c87cc934
8 changed files with 76 additions and 40 deletions
@@ -22,7 +22,7 @@ class CodegenTest {
LspEnum(
name: 'x',
typeOfValues: TypeReference.int,
members: [
constants: [
Constant(name: 'new', type: TypeReference.string, value: '1'),
],
),
@@ -29,13 +29,13 @@ ast.LspEnum _enum({
String name = 'x',
String type = 'int',
bool flags = false,
required List<ast.Member> members,
required List<ast.Constant> constants,
}) {
return ast.LspEnum(
name: name,
typeOfValues: ast.TypeReference(type),
flags: flags,
members: members,
constants: constants,
);
}
@@ -48,22 +48,35 @@ ast.UnionType _union(List<String> names) =>
class DartTest {
void test_flags_requiresPowerOfTwo_double() {
expect(
() => _enum(flags: true, members: [_constant(1.2)]),
throwsA(isA<AssertionError>()),
() => _enum(flags: true, constants: [_constant(1.2)]),
throwsArgumentError,
);
}
void test_flags_requiresPowerOfTwo_nonPowerOfTwo() {
expect(
() => _enum(flags: true, members: [_constant(3)]),
throwsA(isA<AssertionError>()),
() => _enum(flags: true, constants: [_constant(3)]),
throwsArgumentError,
);
}
void test_flags_requiresPowerOfTwo_string() {
expect(
() => _enum(flags: true, members: [_constant('test')]),
throwsA(isA<AssertionError>()),
() => _enum(flags: true, constants: [_constant('test')]),
throwsArgumentError,
);
}
void test_flags_requiresUniqueValues() {
expect(
() => _enum(
flags: true,
constants: [
_constant(1, name: 'a'),
_constant(1, name: 'b'),
],
),
throwsArgumentError,
);
}
@@ -385,13 +385,10 @@ Sometimes after a blank line we'll have a note.
expect(output[0], const TypeMatcher<LspEnum>());
var namespace = output[0] as LspEnum;
expect(namespace.members, hasLength(3));
for (var m in namespace.members) {
expect(m, const TypeMatcher<Constant>());
}
var create = namespace.members[0] as Constant,
delete = namespace.members[1] as Constant,
rename = namespace.members[2] as Constant;
expect(namespace.constants, hasLength(3));
var create = namespace.constants[0],
delete = namespace.constants[1],
rename = namespace.constants[2];
expect(create.name, equals('Create'));
expect(create.type, isSimpleType('ResourceOperationKind'));
expect(create.comment, equals('Supports creating new files and folders.'));
@@ -633,7 +633,7 @@ void _writeDocCommentsAndAnnotations(
void _writeEnumClass(IndentableStringBuffer buffer, LspEnum namespace) {
_writeDocCommentsAndAnnotations(buffer, namespace);
var consts = namespace.members.cast<Constant>().toList();
var consts = namespace.constants;
var namespaceName = namespace.name;
var typeOfValues = namespace.typeOfValues;
var allowsAnyValue = enumClassAllowsAnyValue(namespaceName);
@@ -674,10 +674,10 @@ void _writeEnumClass(IndentableStringBuffer buffer, LspEnum namespace) {
..outdent()
..writeIndentedln('}');
}
namespace.members.whereType<Constant>().forEach((cons) {
for (var cons in consts) {
// We don't use any deprecated enum values, so omit them entirely.
if (cons.isDeprecated) {
return;
continue;
}
_writeDocCommentsAndAnnotations(buffer, cons);
var memberName = cons.dartSafeName;
@@ -685,7 +685,7 @@ void _writeEnumClass(IndentableStringBuffer buffer, LspEnum namespace) {
buffer.writeIndentedln(
'static const $memberName = $namespaceName$constructorName($value);',
);
});
}
if (namespace.flags) {
buffer
..writeln()
@@ -137,7 +137,7 @@ final interactiveFormClasses = <LspEntity>[
name: 'FileExistence',
typeOfValues: TypeReference.int,
flags: true,
members: [
constants: [
// Values should be powers of 2 to allow New|Existing.
Constant(
name: 'New',
@@ -161,7 +161,7 @@ final interactiveFormClasses = <LspEntity>[
name: 'FileType',
typeOfValues: TypeReference.int,
flags: true,
members: [
constants: [
// Values should be powers of 2 to allow Regular|Directory.
Constant(
name: 'Regular',
@@ -185,23 +185,49 @@ abstract class LspEntity {
class LspEnum extends LspEntity {
final TypeBase typeOfValues;
final bool flags;
final List<Member> members;
final List<Constant> constants;
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());
required this.constants,
}) {
_validateFlagsEnum();
constants.sortBy((constant) => constant.name.toLowerCase());
}
void _validateFlagsEnum() {
if (!flags) {
return;
}
if (typeOfValues.dartTypeWithTypeArgs != 'int') {
throw ArgumentError(
'Flags enum $name has value type ${typeOfValues.dartTypeWithTypeArgs}; '
'flags enum values must be ints.',
);
}
var usedValues = <int>{};
for (var constant in constants) {
var value = int.tryParse(constant.value);
if (value == null || !_isPowerOfTwo(value)) {
throw ArgumentError(
'Flags enum $name constant ${constant.name} has value '
'${constant.value}; '
'flags enum values must be int powers of two.',
);
}
if (!usedValues.add(value)) {
throw ArgumentError(
'Flags enum $name has duplicate value $value; '
'flags enum values must be unique.',
);
}
}
}
}
@@ -191,9 +191,9 @@ class LspMetaModelCleaner {
isProposed: namespace.isProposed,
typeOfValues: namespace.typeOfValues,
flags: namespace.flags,
members: namespace.members
constants: namespace.constants
.where(_includeEntityInOutput)
.map((member) => _cleanMember(namespace.name, member))
.map(_cleanConst)
.toList(),
);
}
@@ -387,7 +387,7 @@ class LspMetaModelCleaner {
isProposed: dest.isProposed,
typeOfValues: dest.typeOfValues,
flags: dest.flags || source.flags,
members: [...dest.members, ...source.members],
constants: [...dest.constants, ...source.constants],
);
} else if (source is Interface && dest is Interface) {
return Interface(
@@ -533,7 +533,7 @@ class LspMetaModelCleaner {
isProposed: type.isProposed,
typeOfValues: type.typeOfValues,
flags: type.flags,
members: type.members,
constants: type.constants,
);
} else {
throw 'Renaming ${type.runtimeType} is not implemented';
@@ -60,7 +60,7 @@ class LspMetaModelReader {
return LspMetaModel(
types: types,
methods: methodsEnum?.members.cast<Constant>().toList() ?? [],
methods: methodsEnum?.constants.toList() ?? [],
);
}
@@ -106,7 +106,7 @@ class LspMetaModelReader {
name: 'Method',
comment: comment,
typeOfValues: TypeReference.string,
members: methodConstants,
constants: methodConstants,
);
}
@@ -299,7 +299,7 @@ class LspMetaModelReader {
comment: documentation,
isProposed: _isProposed(documentation),
typeOfValues: baseType,
members: [
constants: [
...?(model['values'] as List?)?.map((p) => _extractEnumValue(type, p)),
],
);