-
Notifications
You must be signed in to change notification settings - Fork 3.8k
[pigeon] add error on empty class #11933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -619,6 +619,25 @@ List<Error> _validateAst(Root root, String source) { | |||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (classDefinition.isSealed) { | ||||||||||||||||||||||||||||||
| if (classDefinition.fields.isNotEmpty) { | ||||||||||||||||||||||||||||||
| result.add( | ||||||||||||||||||||||||||||||
| Error(message: 'Sealed class: "${classDefinition.name}" must not contain fields.'), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (classDefinition.fields.isEmpty && !classDefinition.isSealed) { | ||||||||||||||||||||||||||||||
| result.add( | ||||||||||||||||||||||||||||||
| Error(message: 'Class: "${classDefinition.name}" must contain fields or be sealed.'), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (classDefinition.superClass != null) { | ||||||||||||||||||||||||||||||
| if (!classDefinition.superClass!.isSealed) { | ||||||||||||||||||||||||||||||
| result.add( | ||||||||||||||||||||||||||||||
| Error(message: 'Child class: "${classDefinition.name}" must extend a sealed class.'), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+634
to
+640
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a class extends an unknown or undefined class (e.g., due to a typo or because the parent class is missing from the file), To prevent this silent bypass, we should check if
Suggested change
|
||||||||||||||||||||||||||||||
| for (final NamedType field in getFieldsInSerializationOrder(classDefinition)) { | ||||||||||||||||||||||||||||||
| final String? matchingPrefix = _findMatchingPrefixOrNull( | ||||||||||||||||||||||||||||||
| field.name, | ||||||||||||||||||||||||||||||
|
|
@@ -644,26 +663,6 @@ List<Error> _validateAst(Root root, String source) { | |||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (classDefinition.isSealed) { | ||||||||||||||||||||||||||||||
| if (classDefinition.fields.isNotEmpty) { | ||||||||||||||||||||||||||||||
| result.add( | ||||||||||||||||||||||||||||||
| Error( | ||||||||||||||||||||||||||||||
| message: 'Sealed class: "${classDefinition.name}" must not contain fields.', | ||||||||||||||||||||||||||||||
| lineNumber: _calculateLineNumberNullable(source, field.offset), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (classDefinition.superClass != null) { | ||||||||||||||||||||||||||||||
| if (!classDefinition.superClass!.isSealed) { | ||||||||||||||||||||||||||||||
| result.add( | ||||||||||||||||||||||||||||||
| Error( | ||||||||||||||||||||||||||||||
| message: 'Child class: "${classDefinition.name}" must extend a sealed class.', | ||||||||||||||||||||||||||||||
| lineNumber: _calculateLineNumberNullable(source, field.offset), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -1805,22 +1804,21 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> { | |||||||||||||||||||||||||||||
| lineNumber: calculateLineNumber(source, node.offset), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| final dart_ast.TypeArgumentList? typeArguments = type.typeArguments; | ||||||||||||||||||||||||||||||
| final String name = node.fields.variables[0].name.lexeme; | ||||||||||||||||||||||||||||||
| final field = NamedType( | ||||||||||||||||||||||||||||||
| type: TypeDeclaration( | ||||||||||||||||||||||||||||||
| baseName: _getNamedTypeQualifiedName(type), | ||||||||||||||||||||||||||||||
| isNullable: type.question != null, | ||||||||||||||||||||||||||||||
| typeArguments: _typeAnnotationsToTypeArguments(typeArguments), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| name: name, | ||||||||||||||||||||||||||||||
| offset: node.offset, | ||||||||||||||||||||||||||||||
| defaultValue: _currentClassDefaultValues[name], | ||||||||||||||||||||||||||||||
| documentationComments: _documentationCommentsParser(node.documentationComment?.tokens), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| _currentClass!.fields.add(field); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| final dart_ast.TypeArgumentList? typeArguments = type.typeArguments; | ||||||||||||||||||||||||||||||
| final String name = node.fields.variables[0].name.lexeme; | ||||||||||||||||||||||||||||||
| final field = NamedType( | ||||||||||||||||||||||||||||||
| type: TypeDeclaration( | ||||||||||||||||||||||||||||||
| baseName: _getNamedTypeQualifiedName(type), | ||||||||||||||||||||||||||||||
| isNullable: type.question != null, | ||||||||||||||||||||||||||||||
| typeArguments: _typeAnnotationsToTypeArguments(typeArguments), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| name: name, | ||||||||||||||||||||||||||||||
| offset: node.offset, | ||||||||||||||||||||||||||||||
| defaultValue: _currentClassDefaultValues[name], | ||||||||||||||||||||||||||||||
| documentationComments: _documentationCommentsParser(node.documentationComment?.tokens), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| _currentClass!.fields.add(field); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| _errors.add( | ||||||||||||||||||||||||||||||
| Error( | ||||||||||||||||||||||||||||||
|
|
@@ -1969,23 +1967,22 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> { | |||||||||||||||||||||||||||||
| lineNumber: calculateLineNumber(source, node.offset), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| final dart_ast.TypeArgumentList? typeArguments = type.typeArguments; | ||||||||||||||||||||||||||||||
| (_currentApi as AstProxyApi?)!.fields.add( | ||||||||||||||||||||||||||||||
| ApiField( | ||||||||||||||||||||||||||||||
| type: TypeDeclaration( | ||||||||||||||||||||||||||||||
| baseName: _getNamedTypeQualifiedName(type), | ||||||||||||||||||||||||||||||
| isNullable: type.question != null, | ||||||||||||||||||||||||||||||
| typeArguments: _typeAnnotationsToTypeArguments(typeArguments), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| name: node.fields.variables[0].name.lexeme, | ||||||||||||||||||||||||||||||
| isAttached: _hasMetadata(node.metadata, 'attached') || isStatic, | ||||||||||||||||||||||||||||||
| isStatic: isStatic, | ||||||||||||||||||||||||||||||
| offset: node.offset, | ||||||||||||||||||||||||||||||
| documentationComments: _documentationCommentsParser(node.documentationComment?.tokens), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| final dart_ast.TypeArgumentList? typeArguments = type.typeArguments; | ||||||||||||||||||||||||||||||
| (_currentApi as AstProxyApi?)!.fields.add( | ||||||||||||||||||||||||||||||
| ApiField( | ||||||||||||||||||||||||||||||
| type: TypeDeclaration( | ||||||||||||||||||||||||||||||
| baseName: _getNamedTypeQualifiedName(type), | ||||||||||||||||||||||||||||||
| isNullable: type.question != null, | ||||||||||||||||||||||||||||||
| typeArguments: _typeAnnotationsToTypeArguments(typeArguments), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| name: node.fields.variables[0].name.lexeme, | ||||||||||||||||||||||||||||||
| isAttached: _hasMetadata(node.metadata, 'attached') || isStatic, | ||||||||||||||||||||||||||||||
| isStatic: isStatic, | ||||||||||||||||||||||||||||||
| offset: node.offset, | ||||||||||||||||||||||||||||||
| documentationComments: _documentationCommentsParser(node.documentationComment?.tokens), | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In sealed class hierarchies (union types), it is a very common and idiomatic pattern to have empty subclasses to represent states with no associated data (for example, a
LoadingorEmptystate in aResultorOptionunion).Currently, the empty class check flags any non-sealed class with no fields as an error. This will incorrectly reject valid empty subclasses of sealed classes.
To support idiomatic sealed class unions, we should allow empty classes if they are subclasses (i.e.,
superClassName != null).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Gemini correct here, or is
isSealedtrue for subclasses of a sealed class?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Either way, we should have a test of this scenario.)