Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,11 @@ jobs:
matrix:
sdk: [ stable ]
analyzer:
- ^5.13.0 # This should match the lower bound
- ^6.0.0
- ^7.0.0
- ^8.0.0
- ^9.0.0
- ^10.0.0
- ^10.0.0 # This should match the lower bound
- ^11.0.0
- ^12.0.0
- ^13.0.0
- ^14.0.0

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -157,7 +156,9 @@ jobs:

- id: link
name: Override over_react dependency with local path
run: cd ../.. && dart pub get && dart tool/travis_link_plugin_deps.dart
# TODO reinstate this once we update analyzer_plugin's analyzer range to overlap with what over_react supports

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that analyzer_plugin isn't up to date on analyzer yet; that'll be a separate PR

# run: cd ../.. && dart pub get && dart tool/travis_link_plugin_deps.dart
run: echo "Skipping since analyzer plugin is pinned to older over_react with compatible analyzer"

- id: install
name: Install dependencies
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OverReact Changelog

## Unreleased
- Update analyzer dependency to `>=10.0.0 <15.0.0` (allow 11, 12, 13, and 14)
- Drop Dart 2 support; minimum SDK is now 3.0.0
- Fix Dart language level 3.0 errors and downstream compile errors by switching internal classes to `mixin` declarations
- `GeneratedClass` - switched to `mixin`
Expand Down
56 changes: 42 additions & 14 deletions lib/src/builder/parsing/ast_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,31 +68,59 @@ extension TypeAnnotationNameHelper on TypeAnnotation {
/// Extension built on [NameHelper] to allow for easy access to the `name`
/// field of [Identifier]s.
extension TypeNameHelper on NamedType {
// Backwards compatibility for various analyzer versions that remove name/name2.

@greglittlefield-wf greglittlefield-wf Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed these since they were only needed for older analyzer versions

dynamic get name => this.name2; // Use `this.` to point to real impl if it exists, not extension.
dynamic get name2 => this.name; // Use `this.` to point to real impl if it exists, not extension.
dynamic get _name => name;
String get nameLexeme {
final name = this._name;
if (name is Identifier) return name.name;
if (name is Token) return name.lexeme;
if (name is String) return name;
throw UnimplementedError('Unexpected type for name: ${name.runtimeType}');
}

/// The type name without any namespace prefixes.
String get nameWithoutPrefix => nameLexeme;
String get nameWithoutPrefix => name.lexeme;

/// The type name including the namespace prefix.
String get nameWithPrefix {
final prefix = importPrefix?.name.lexeme;
return [
if (prefix != null) prefix,
nameLexeme,
name.lexeme,
].join('.');
}
}

/// Provides `.members` on [ClassBody] for analyzer versions where it is only
/// declared on [BlockClassBody].
///
/// On analyzer 12+ the real instance member wins over this extension.
/// On analyzer 10/11 the cast always succeeds because [EmptyClassBody] does not exist.
extension ClassBodyMembersCompat on ClassBody {
NodeList<ClassMember> get members => (this as BlockClassBody).members;
}

/// Parts of a named argument, returned by [namedArgumentParts].
class NamedArgumentParts {
const NamedArgumentParts(this.name, this.value);
final String name;
final Expression value;
}

/// Returns the name label lexeme and value expression of a named argument node
/// ([NamedArgument] in analyzer 13+, [NamedExpression] in earlier versions),
/// or `null` if [node] is not a named argument.
///
/// TODO remove once we only support analyzer 13+
NamedArgumentParts? namedArgumentParts(AstNode node) {
// analyzer 13+: NamedArgument (name: Token, argumentExpression: Expression)
// analyzer 10–12: NamedExpression (name: Label (label: SimpleIdentifier), expression: Expression)
try {
final dynamicNode = node as dynamic;
final nameToken = dynamicNode.name;
if (nameToken is Token) {
// analyzer 13+: NamedArgument
return NamedArgumentParts(nameToken.lexeme, dynamicNode.argumentExpression as Expression);
} else {
// analyzer 10–12: NamedExpression — nameToken is a Label whose .label is SimpleIdentifier
final labelName = (nameToken as dynamic).label.name as String;
return NamedArgumentParts(labelName, dynamicNode.expression as Expression);
}
} catch (_) {
return null;
}
}

/// Utilities related to simplifying access to node identifier fields.
extension NameHelper on Identifier {
/// The identifier without any prefixes (e.g., namespaces, classes, etc).
Expand Down
26 changes: 19 additions & 7 deletions lib/src/builder/parsing/ast_util/classish.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

extension Classish on NamedCompilationUnitMember {
// ignore: unused_import — shadowed by real ClassBody.members on analyzer >=12; needed on 10/11
import '../ast_util.dart' show ClassBodyMembersCompat;

extension Classish on CompilationUnitMember {
ClassishDeclaration asClassish() => ClassishDeclaration(this);
}

/// Provides a common interface for [ClassDeclaration], [MixinDeclaration],
/// and [ClassTypeAlias].
abstract class ClassishDeclaration {
factory ClassishDeclaration(NamedCompilationUnitMember node) {
factory ClassishDeclaration(CompilationUnitMember node) {
if (node is ClassDeclaration) {
return _ClassishClass(node);
} else if (node is ClassTypeAlias) {
Expand All @@ -37,12 +40,12 @@ abstract class ClassishDeclaration {

ClassishDeclaration._();

NamedCompilationUnitMember get node;
CompilationUnitMember get node;

//
// Shared

Token get name => node.name;
Token get name;
NodeList<Annotation> get metadata => node.metadata;

TypeParameterList? get typeParameters;
Expand Down Expand Up @@ -78,6 +81,9 @@ class _ClassishClass extends ClassishDeclaration {

_ClassishClass(this.node) : super._();

@override
Token get name => node.namePart.typeName;

@override
Token? get abstractKeyword => node.abstractKeyword;

Expand All @@ -96,10 +102,10 @@ class _ClassishClass extends ClassishDeclaration {
Token get classOrMixinKeyword => node.classKeyword;

@override
List<ClassMember> get members => node.members;
List<ClassMember> get members => node.body.members;

@override
TypeParameterList? get typeParameters => node.typeParameters;
TypeParameterList? get typeParameters => node.namePart.typeParameters;
}

class _ClasssishMixin extends ClassishDeclaration {
Expand All @@ -108,6 +114,9 @@ class _ClasssishMixin extends ClassishDeclaration {

_ClasssishMixin(this.node) : super._();

@override
Token get name => node.name;

@override
Token? get abstractKeyword => null;

Expand All @@ -127,7 +136,7 @@ class _ClasssishMixin extends ClassishDeclaration {
WithClause? get withClause => null;

@override
List<ClassMember> get members => node.members;
List<ClassMember> get members => node.body.members;

@override
TypeParameterList? get typeParameters => node.typeParameters;
Expand All @@ -139,6 +148,9 @@ class _ClassishClassTypeAlias extends ClassishDeclaration {

_ClassishClassTypeAlias(this.node) : super._();

@override
Token get name => node.name;

@override
Token? get abstractKeyword => node.abstractKeyword;

Expand Down
2 changes: 1 addition & 1 deletion lib/src/builder/parsing/members.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ abstract class BoilerplateTypedMapMember extends BoilerplateMember {
BoilerplateTypedMapMember(VersionConfidences versionConfidence) : super(versionConfidence);

@override
NamedCompilationUnitMember get node;
CompilationUnitMember get node;

ClassishDeclaration get nodeHelper;

Expand Down
2 changes: 1 addition & 1 deletion lib/src/builder/parsing/members/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class BoilerplateComponent extends BoilerplateMember {

/// The [ClassDeclaration] backing the member
@override
final NamedCompilationUnitMember node;
final CompilationUnitMember node;

@override
Token get name => nodeHelper.name;
Expand Down
21 changes: 12 additions & 9 deletions lib/src/builder/parsing/members/props_and_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ abstract class BoilerplatePropsOrState extends BoilerplateTypedMapMember

/// The [ClassDeclaration] backing the member
@override
final NamedCompilationUnitMember node;
final CompilationUnitMember node;

/// A metadata class that lifts helpful fields out of [node] to a top level,
/// in addition to providing additional getters relevant member parsing.
Expand Down Expand Up @@ -74,13 +74,16 @@ abstract class BoilerplatePropsOrState extends BoilerplateTypedMapMember
if (nodeHelper.superclass?.nameWithoutPrefix != propsOrStateBaseClassString) {
errorCollector.addError(
'$propsOrStateClassString implementations must extend directly from $propsOrStateBaseClassString',
errorCollector.spanFor(nodeHelper.superclass ?? node.name));
errorCollector.spanFor(nodeHelper.superclass ?? nodeHelper.name));
}

if (node is ClassDeclaration && !node.members.every(isStaticMember)) {
if (node is ClassDeclaration && !node.body.members.every(isStaticMember)) {
final body = node.body;
errorCollector.addError(
'$propsOrStateClassString implementations must not declare any $propsOrStateFieldsName or other non-static members.',
errorCollector.span(node.leftBracket.offset, node.rightBracket.end));
errorCollector.span(
body is BlockClassBody ? body.leftBracket.offset : body.offset,
body is BlockClassBody ? body.rightBracket.end : body.end));
}

if (nodeHelper.hasAbstractKeyword) {
Expand All @@ -100,9 +103,9 @@ abstract class BoilerplatePropsOrState extends BoilerplateTypedMapMember
final companion = this.companion;
if (companion == null) {
// Don't emit this and the prefix error.
if (node.name.name.startsWith(privateSourcePrefix)) {
if (nodeHelper.name.lexeme.startsWith(privateSourcePrefix)) {
errorCollector.addError(
'Should have companion class', errorCollector.spanFor(node.name));
'Should have companion class', errorCollector.spanFor(nodeHelper.name));
}
} else {
validateMetaField(companion, propsOrStateMetaStructName, errorCollector);
Expand All @@ -124,11 +127,11 @@ abstract class BoilerplatePropsOrState extends BoilerplateTypedMapMember
}

// Check that class name starts with [privateSourcePrefix]
if (!node.name.name.startsWith(privateSourcePrefix)) {
if (!nodeHelper.name.lexeme.startsWith(privateSourcePrefix)) {
errorCollector.addError(
'The class `${node.name.name}` does not start with `$privateSourcePrefix`. All Props, State, '
'The class `${nodeHelper.name.lexeme}` does not start with `$privateSourcePrefix`. All Props, State, '
'AbstractProps, and AbstractState classes should begin with `$privateSourcePrefix` on Dart 2',
errorCollector.spanFor(node.name));
errorCollector.spanFor(nodeHelper.name));
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions lib/src/builder/parsing/members/props_and_state_mixins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ abstract class BoilerplatePropsOrStateMixin extends BoilerplateTypedMapMember

/// The node backing the member.
@override
NamedCompilationUnitMember get node => nodeHelper.node;
CompilationUnitMember get node => nodeHelper.node;

/// A metadata class that lifts helpful fields out of [node] to a top level,
/// in addition to providing additional getters relevant member parsing.
Expand Down Expand Up @@ -64,14 +64,14 @@ abstract class BoilerplatePropsOrStateMixin extends BoilerplateTypedMapMember
errorCollector.addError(
'Legacy boilerplate ${propsOrStateMixinString}s must be annotated with '
'`@$propsOrStateMixinAnnotationName()`',
errorCollector.spanFor(node.name));
errorCollector.spanFor(nodeHelper.name));
}

if (!nodeHelper.hasAbstractGetter('Map', propsOrStateString)) {
errorCollector.addError(
'$propsOrStateMixinString classes must declare an abstract $propsOrStateString getter `Map get $propsOrStateString;` '
'so that they can be statically analyzed properly.',
errorCollector.spanFor(node.name));
errorCollector.spanFor(nodeHelper.name));
}
}

Expand Down
5 changes: 3 additions & 2 deletions lib/src/builder/parsing/members/props_and_state_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ annotations.TypedMap getPropsOrStateAnnotation(bool isProps, AnnotatedNode node)
if (potentiallyIncompleteValue is annotations.Props) {
if (meta.unsupportedArguments.length == 1) {
final arg = meta.unsupportedArguments[0];
if (arg is NamedExpression && arg.name.label.name == 'disableRequiredPropValidation') {
final argParts = namedArgumentParts(arg);
if (argParts != null && argParts.name == 'disableRequiredPropValidation') {
// Attempt to parse the value, and fall through if something goes wrong,
// and let `meta?.value` below throw.
final expression = arg.expression;
final expression = argParts.value;
if (expression is SetOrMapLiteral) {
final simpleStringElements =
expression.elements.whereType<SimpleStringLiteral>().toList();
Expand Down
Loading
Loading