From a297840308e8abc108a66a2c0b2bb1edf5230d17 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 09:57:13 -0700 Subject: [PATCH 01/17] Remove dart:mirrors to fix analyzer plugin not AOT- compiling in Dart 3 --- lib/src/builder/parsing/meta.dart | 144 +++++++++++--- .../src/analyzer_helpers.dart | 110 ++--------- .../transformer_utils/src/node_with_meta.dart | 68 ------- .../transformer_utils/transformer_utils.dart | 4 +- .../unit/analyzer_helpers_test.dart | 180 ------------------ .../unit/node_with_meta_test.dart | 77 -------- 6 files changed, 133 insertions(+), 450 deletions(-) delete mode 100644 lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart delete mode 100644 test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart diff --git a/lib/src/builder/parsing/meta.dart b/lib/src/builder/parsing/meta.dart index 9f9b44345..d1826ecca 100644 --- a/lib/src/builder/parsing/meta.dart +++ b/lib/src/builder/parsing/meta.dart @@ -12,38 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:mirrors' as mirrors; - import 'package:analyzer/dart/ast/ast.dart'; import 'package:build/build.dart' show log; import 'package:collection/collection.dart' show IterableExtension; -import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; +import 'package:meta/meta.dart'; -import 'ast_util.dart'; +import 'package:over_react/src/component_declaration/annotations.dart' as a; -/// Uses reflection to instantiate and return the first annotation on [member] of type -/// [T], or null if no matching annotations are found. -/// -/// > See [instantiateAnnotation] for more information. -T? instantiateAnnotationTyped(AnnotatedNode member, - {dynamic Function(Expression argument)? onUnsupportedArgument}) { - return instantiateAnnotation(member, T, onUnsupportedArgument: onUnsupportedArgument) as T?; -} +import '../vendor/transformer_utils/src/analyzer_helpers.dart'; -/// Returns the first annotation AST node on [member] of type [annotationType], +/// Returns the first annotation AST node on [node] of type [T], /// or null if no matching annotations are found. -Annotation? _getMatchingAnnotation(AnnotatedNode member, Type annotationType) { - // Be sure to use `originalDeclaration` so that generic parameters work. - final classMirror = mirrors.reflectClass(annotationType).originalDeclaration; - final className = mirrors.MirrorSystem.getName(classMirror.simpleName); - return member.getAnnotationWithName(className); +Annotation? _getMatchingAnnotation(AnnotatedNode node) { + final annotationClass = getAnnotationClassFromGeneric(); + if (annotationClass == null) return null; + + return node.metadata.firstWhereOrNull((m) => m.name.name == annotationClass.className); } /// Utility class that allows partial instantiation of annotations, to support reading /// annotation data in a context without a resolved AST. See [isIncomplete] for more info. -/// -/// Based off of [NodeWithMeta]. -class InstantiatedMeta { +class InstantiatedMeta { /// The node of the [TMeta] annotation, if it exists. final Annotation metaNode; @@ -62,8 +51,8 @@ class InstantiatedMeta { /// The original node will be available via [node]. /// /// The instantiated annotation will be available via [value]. - static InstantiatedMeta? fromNode(AnnotatedNode node) { - final metaNode = _getMatchingAnnotation(node, T); + static InstantiatedMeta? fromNode(AnnotatedNode node) { + final metaNode = _getMatchingAnnotation(node); if (metaNode == null) return null; final unsupportedArguments = []; @@ -99,7 +88,7 @@ class InstantiatedMeta { /// Utility that allows partial instantiation of a `Component`/`Component2` annotation. /// /// See superclass for more information. -class InstantiatedComponentMeta extends InstantiatedMeta { +class InstantiatedComponentMeta extends InstantiatedMeta { static const String _subtypeOfParamName = 'subtypeOf'; final Identifier? subtypeOfValue; @@ -108,7 +97,7 @@ class InstantiatedComponentMeta extends InstantiatedMeta { Annotation metaNode, TMeta meta, List unsupportedArguments, this.subtypeOfValue) : super._(metaNode, meta, unsupportedArguments); - static InstantiatedComponentMeta? fromNode(AnnotatedNode node) { + static InstantiatedComponentMeta? fromNode(AnnotatedNode node) { try { final instantiated = InstantiatedMeta.fromNode(node); if (instantiated == null) return null; @@ -140,3 +129,108 @@ class InstantiatedComponentMeta extends InstantiatedMeta { } } } + +T? instantiateAnnotationTyped( + AnnotatedNode node, { + dynamic Function(Expression argument)? onUnsupportedArgument, +}) { + // TODO DRY up + final annotationClass = getAnnotationClassFromGeneric(); + if (annotationClass == null) return null; + + final annotation = + node.metadata.firstWhereOrNull((m) => m.name.name == annotationClass.className); + if (annotation == null) return null; + + final args = getAnnotationArgs(annotation, onUnsupportedArgument: onUnsupportedArgument); + + switch (annotationClass) { + case _AnnotationClass.props: + return a.Props( + keyNamespace: args.named['keyNamespace'] as String?, + disableRequiredPropValidation: args.named['disableRequiredPropValidation'] as Set?, + disableValidationForClassDefaultProps: + args.named['disableValidationForClassDefaultProps'] as bool? ?? true, + ) as T; + case _AnnotationClass.abstractProps: + return a.AbstractProps( + keyNamespace: args.named['keyNamespace'] as String?, + ) as T; + case _AnnotationClass.propsMixin: + // ignore: deprecated_member_use_from_same_package + return a.PropsMixin( + keyNamespace: args.named['keyNamespace'] as String?, + ) as T; + case _AnnotationClass.state: + return a.State( + keyNamespace: args.named['keyNamespace'] as String?, + ) as T; + case _AnnotationClass.abstractState: + return a.AbstractState( + keyNamespace: args.named['keyNamespace'] as String?, + ) as T; + case _AnnotationClass.stateMixin: + // ignore: deprecated_member_use_from_same_package + return a.StateMixin( + keyNamespace: args.named['keyNamespace'] as String?, + ) as T; + case _AnnotationClass.component2: + return a.Component2( + isWrapper: args.named['isWrapper'] as bool? ?? false, + subtypeOf: args.named['subtypeOf'] as Type?, + isErrorBoundary: args.named['isErrorBoundary'] as bool? ?? false, + ) as T; + case _AnnotationClass.component: + // ignore: deprecated_member_use_from_same_package + return a.Component( + isWrapper: args.named['isWrapper'] as bool? ?? false, + subtypeOf: args.named['subtypeOf'] as Type?, + ) as T; + case _AnnotationClass.accessor: + return a.Accessor( + key: args.named['key'] as String?, + keyNamespace: args.named['keyNamespace'] as String?, + isRequired: (args.named['isRequired'] as bool?) ?? false, + isNullable: (args.named['isNullable'] as bool?) ?? false, + requiredErrorMessage: args.named['requiredErrorMessage'] as String?, + doNotGenerate: (args.named['doNotGenerate'] as bool?) ?? false, + ) as T; + } +} + +enum _AnnotationClass { + props('Props'), + abstractProps('AbstractProps'), + propsMixin('PropsMixin'), + state('State'), + abstractState('AbstractState'), + stateMixin('StateMixin'), + component2('Component2'), + component('Component'), + accessor('Accessor'); + + final String className; + + const _AnnotationClass(this.className); +} + +@visibleForTesting +_AnnotationClass? getAnnotationClassFromGeneric() { + if (_isSubtypeOf()) return _AnnotationClass.props; + if (_isSubtypeOf()) return _AnnotationClass.abstractProps; + // ignore: deprecated_member_use_from_same_package + if (_isSubtypeOf()) return _AnnotationClass.propsMixin; + if (_isSubtypeOf()) return _AnnotationClass.state; + if (_isSubtypeOf()) return _AnnotationClass.abstractState; + // ignore: deprecated_member_use_from_same_package + if (_isSubtypeOf()) return _AnnotationClass.stateMixin; + if (_isSubtypeOf()) return _AnnotationClass.component2; + // ignore: deprecated_member_use_from_same_package + if (_isSubtypeOf()) return _AnnotationClass.component; + if (_isSubtypeOf()) return _AnnotationClass.accessor; + return null; +} + +bool _isSubtypeOf() => _SubtypeOfHelper() is _SubtypeOfHelper; + +class _SubtypeOfHelper {} diff --git a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart index 788c1ed99..8cd352038 100644 --- a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart +++ b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart @@ -16,10 +16,7 @@ library; -import 'dart:mirrors' as mirrors; - import 'package:analyzer/dart/ast/ast.dart'; -import 'package:collection/collection.dart' show IterableExtension; /// Returns a copy of a class [member] declaration with [body] as a new /// implementation. @@ -38,19 +35,6 @@ String copyClassMember(ClassMember? member, String body) { 'Only FieldDeclaration and MethodDeclaration are supported.'); } -/// Finds and returns all declarations within a compilation [unit] that are -/// annotated with the given [annotation] class. -/// -/// If this is being leveraged within a transformer, you can associate the -/// returned [DeclarationWithMeta] instance with the asset in which it is -/// located by passing in an [assetId]. -Iterable getDeclarationsAnnotatedBy(CompilationUnit unit, Type annotation) { - var annotationName = _getReflectedName(annotation); - return unit.declarations.where((member) { - return member.metadata.any((meta) => meta.name.name == annotationName); - }); -} - /// Returns the value of the specified [expression] AST node if it represents a literal. /// /// For non-literal nodes, the return value will be the result of calling [onUnsupportedExpression] with [expression]. @@ -85,47 +69,19 @@ dynamic getValue(Expression expression, 'Must be a uninterpolated string, boolean, integer, or null literal.'); } -/// Returns the first annotation AST node on [member] of type [annotationType], -/// or null if no matching annotations are found. -Annotation? getMatchingAnnotation(AnnotatedNode member, Type annotationType) { - // Be sure to use `originalDeclaration` so that generic parameters work. - final classMirror = - mirrors.reflectClass(annotationType).originalDeclaration as mirrors.ClassMirror; - String className = mirrors.MirrorSystem.getName(classMirror.simpleName); - - // Find the annotation that matches [type]'s name. - return member.metadata.firstWhereOrNull((annotation) { - return _getClassName(annotation) == className; - }); +class AnnotationArgs { + final Map named; + final List positional; + + AnnotationArgs({required this.named, required this.positional}); } -/// Uses reflection to instantiate and returns the first annotation on [member] of type -/// [annotationType], or null if no matching annotations are found. -/// -/// Annotation constructors are currently limited to the values supported by [getValue]. -/// -/// Naively assumes that the name of the [annotationType] class is canonical. -dynamic instantiateAnnotation(AnnotatedNode member, Type annotationType, +AnnotationArgs getAnnotationArgs(Annotation annotation, {dynamic Function(Expression argument)? onUnsupportedArgument}) { - final matchingAnnotation = getMatchingAnnotation(member, annotationType); - - // If no annotation is found, return null. - if (matchingAnnotation == null) { - return null; - } - - final matchingAnnotationArgs = matchingAnnotation.arguments; - if (matchingAnnotationArgs == null) { - throw Exception('Annotation not invocation of constructor: `$matchingAnnotation`. ' - 'This is likely due to invalid usage of the annotation class, but could' - 'also be a name conflict with the specified type `$annotationType`'); - } - - // Get the parameters from the annotation's AST. - Map namedParameters = {}; + Map namedParameters = {}; List positionalParameters = []; - matchingAnnotationArgs.arguments.forEach((argument) { + annotation.arguments?.arguments.forEach((argument) { var onUnsupportedExpression = onUnsupportedArgument == null ? null : (_) => onUnsupportedArgument(argument); @@ -133,7 +89,7 @@ dynamic instantiateAnnotation(AnnotatedNode member, Type annotationType, var name = argument.name.label.name; var value = getValue(argument.expression, onUnsupportedExpression: onUnsupportedExpression); - namedParameters[Symbol(name)] = value; + namedParameters[name] = value; } else { var value = getValue(argument, onUnsupportedExpression: onUnsupportedExpression); @@ -141,22 +97,10 @@ dynamic instantiateAnnotation(AnnotatedNode member, Type annotationType, } }); - // Instantiate and return an instance of the annotation using reflection. - String constructorName = _getConstructorName(matchingAnnotation) ?? ''; - - // Be sure to use `originalDeclaration` so that generic parameters work. - final classMirror = - mirrors.reflectClass(annotationType).originalDeclaration as mirrors.ClassMirror; - - try { - var instanceMirror = - classMirror.newInstance(Symbol(constructorName), positionalParameters, namedParameters); - return instanceMirror.reflectee; - } catch (e, stacktrace) { - throw Exception('Unable to instantiate annotation: $matchingAnnotation. This is ' - 'likely due to improper usage, or a naming conflict with ' - 'annotationType $annotationType. Original error: $e. Stacktrace: $stacktrace'); - } + return AnnotationArgs( + named: namedParameters, + positional: positionalParameters, + ); } String _copyFieldDeclaration(FieldDeclaration decl, String initializer) { @@ -221,31 +165,3 @@ String _copyMethodDeclaration(MethodDeclaration decl, String body) { result = '$result {\n$body\n }'; return result; } - -/// Returns the name of the class being instantiated for [annotation], or null -/// if the annotation is not the invocation of a constructor. -/// -/// Workaround for a Dart analyzer issue where the constructor name is included -/// in [annotation.name]. -String _getClassName(Annotation annotation) => annotation.name.name.split('.').first; - -/// Returns the name of the constructor being instantiated for [annotation], or -/// null if the annotation is not the invocation of a named constructor. -/// -/// Workaround for a Dart analyzer issue where the constructor name is included -/// in [annotation.name]. -String? _getConstructorName(Annotation annotation) { - var constructorName = annotation.constructorName?.name; - if (constructorName == null) { - var periodIndex = annotation.name.name.indexOf('.'); - if (periodIndex != -1) { - constructorName = annotation.name.name.substring(periodIndex + 1); - } - } - - return constructorName; -} - -/// Get the name of a [type] via reflection. -String _getReflectedName(Type type) => - mirrors.MirrorSystem.getName(mirrors.reflectType(type).simpleName); diff --git a/lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart b/lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart deleted file mode 100644 index 54c806645..000000000 --- a/lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2015 Workiva Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ - -library; - -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:build/build.dart'; - -import './analyzer_helpers.dart'; - -/// Utility class that allows for easy access to an annotated node's -/// instantiated annotation. -class NodeWithMeta { - /// The optionally-annotated node. - final TNode node; - - /// The optional asset ID associated with this node. - final AssetId? assetId; - - /// The node of the [TMeta] annotation, if it exists. - final Annotation? metaNode; - - /// A reflectively-instantiated version of [metaNode], if it exists. - TMeta? _meta; - - /// The arguments passed to the metadata that are not supported by [getValue], - /// (or by special handling in subclasses) and therefore not represented in the instantiation of [meta]. - final List unsupportedArguments = []; - - /// Construct a [NodeWithMeta] instance from an [AnnotatedNode]. - /// The original node will be available via [node]. - /// The instantiated annotation of type `TMeta` will be available via [meta]. - NodeWithMeta(this.node, {this.assetId}) : this.metaNode = getMatchingAnnotation(node, TMeta) { - this._meta = instantiateAnnotation(node, TMeta, onUnsupportedArgument: unsupportedArguments.add) - as TMeta?; - } - - /// Whether this node's metadata has arguments that could not be initialized using [getValue] - /// (or by special handling in subclasses), and therefore cannot represented in the instantiation of [meta]. - bool get isIncomplete => unsupportedArguments.isNotEmpty; - - /// A reflectively-instantiated version of [metaNode], if it exists. - /// - /// Throws a [StateError] if this node's metadata is incomplete. - TMeta? get meta { - if (isIncomplete) { - throw StateError('Metadata is incomplete; unsupported arguments $unsupportedArguments. ' - 'Use `potentiallyIncompleteMeta` instead.'); - } - return _meta; - } - - /// A reflectively-instantiated version of [metaNode], if it exists. - TMeta? get potentiallyIncompleteMeta => _meta; -} diff --git a/lib/src/builder/vendor/transformer_utils/transformer_utils.dart b/lib/src/builder/vendor/transformer_utils/transformer_utils.dart index 3cac85dd7..ae13acef7 100644 --- a/lib/src/builder/vendor/transformer_utils/transformer_utils.dart +++ b/lib/src/builder/vendor/transformer_utils/transformer_utils.dart @@ -16,9 +16,7 @@ library; -export './src/analyzer_helpers.dart' - show copyClassMember, getDeclarationsAnnotatedBy, instantiateAnnotation; +export './src/analyzer_helpers.dart' show copyClassMember; export './src/barback_utils.dart' show assetIdToPackageUri, getSpanForNode; -export './src/node_with_meta.dart' show NodeWithMeta; export './src/text_util.dart' show stringLiteral; export './src/transformed_source_file.dart' show TransformedSourceFile, getSpan; diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart index 25c35e875..9b13f9adc 100644 --- a/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart +++ b/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart @@ -17,7 +17,6 @@ @TestOn('vm') library; -import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:over_react/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart'; import 'package:test/test.dart'; @@ -204,183 +203,4 @@ main() { }); }); }); - - group('getDeclarationsAnnotatedBy()', () { - test('no matching declarations', () { - var result = parseString( - content: - ['var a;', '@OtherAnnotation()', 'var b;', 'var c;'].join('\n'), - throwIfDiagnostics: false); - var matches = getDeclarationsAnnotatedBy(result.unit, TestAnnotation); - expect(matches, isEmpty); - }); - - test('one matching declaration', () { - var result = parseString( - content: [ - '@TestAnnotation("test")', - 'var a;', - '@OtherAnnotation()', - 'var b;', - 'var c;' - ].join('\n'), - throwIfDiagnostics: false); - var matches = getDeclarationsAnnotatedBy(result.unit, TestAnnotation); - expect(matches.length, 1); - }); - - test('multiple matching declarations', () { - var result = parseString( - content: [ - '@TestAnnotation("test")', - 'var a;', - '@OtherAnnotation()', - 'var b;', - '@TestAnnotation("test")', - 'var c;' - ].join('\n'), - throwIfDiagnostics: false); - var matches = getDeclarationsAnnotatedBy(result.unit, TestAnnotation); - expect(matches.length, 2); - }); - }); - - group('instantiateAnnotation()', () { - group('instantiates an annotation with a parameter value specified as', () { - test('a string literal', () { - var node = parseAndGetSingleMember('@TestAnnotation("hello")\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.positional, 'hello'); - }); - - test('a concatenated string literal', () { - var node = parseAndGetSingleMember('@TestAnnotation("he" "y")\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.positional, 'hey'); - }); - - test('a boolean literal', () { - var node = parseAndGetSingleMember('@TestAnnotation(true)\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.positional, true); - }); - - test('an integer literal', () { - var node = parseAndGetSingleMember('@TestAnnotation(1)\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.positional, 1); - }); - - test('a null literal', () { - var node = parseAndGetSingleMember('@TestAnnotation(null)\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.positional, null); - }); - }); - - group('throws when an annotation parameter value is unsupported:', () { - test('a constant expression', () { - var node = parseAndGetSingleMember('@TestAnnotation(const [])\nvar a;'); - expect(() => instantiateAnnotation(node, TestAnnotation), - throwsUnsupportedError); - }); - - test('an interpolated String', () { - var node = parseAndGetSingleMember('@TestAnnotation("\$v")\nvar a;'); - expect(() => instantiateAnnotation(node, TestAnnotation), - throwsUnsupportedError); - }); - - test('an identifier', () { - var node = - parseAndGetSingleMember('@TestAnnotation(identifier)\nvar a;'); - expect(() => instantiateAnnotation(node, TestAnnotation), - throwsUnsupportedError); - }); - - group('(except when `onUnsupportedArgument` is specified)', () { - test('positional parameter', () { - Expression? unsupportedArgument; - - final instance = instantiateAnnotation( - parseAndGetSingleMember('@TestAnnotation(const [])\nvar a;'), - TestAnnotation, onUnsupportedArgument: (Expression expression) { - unsupportedArgument = expression; - return 'value to be passed to constructor instead'; - }) as TestAnnotation; - - expect(unsupportedArgument, isA()); - expect(instance.positional, - equals('value to be passed to constructor instead'), - reason: - 'should have passed the return value of `onUnsupportedArgument` to the constructor'); - }); - - test('named parameter', () { - Expression? unsupportedArgument; - - final instance = instantiateAnnotation( - parseAndGetSingleMember( - '@TestAnnotation.namedConstructor(namedConstructorOnly: const [])\nvar a;'), - TestAnnotation, onUnsupportedArgument: (Expression expression) { - unsupportedArgument = expression; - return 'value to be passed to constructor instead'; - }) as TestAnnotation; - - expect(unsupportedArgument, isA()); - expect((unsupportedArgument! as NamedExpression).name.label.name, - equals('namedConstructorOnly')); - expect(instance.namedConstructorOnly, - equals('value to be passed to constructor instead'), - reason: - 'should have passed the return value of `onUnsupportedArgument` to the constructor'); - }); - }); - }); - - test('annotation with both named and positional parameters', () { - var node = - parseAndGetSingleMember('@TestAnnotation(1, named: 2)\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.positional, 1); - expect(instance.named, 2); - }); - - test('instantiates an annotation using a named constructor', () { - var node = parseAndGetSingleMember( - '@TestAnnotation.namedConstructor(namedConstructorOnly: true)\nvar a;'); - final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; - expect(instance.namedConstructorOnly, true); - }); - - test('throws if the annotation cannot be constructed', () { - var node = parseAndGetSingleMember( - '@TestAnnotation(1, 2, 3, 4, "way more parameters than were declared")\nvar a;'); - expect(() { - instantiateAnnotation(node, TestAnnotation); - }, throwsA(hasToStringValue(contains('Unable to instantiate annotation')))); - }); - - test('throws if the annotation is not used as a constructor', () { - var node = parseAndGetSingleMember('@TestAnnotation\nvar a;'); - expect(() { - instantiateAnnotation(node, TestAnnotation); - }, throwsA(hasToStringValue(contains('Annotation not invocation of constructor')))); - }); - - test('returns null when the member is not annotated', () { - var node = parseAndGetSingleMember('var a;'); - expect(instantiateAnnotation(node, TestAnnotation), isNull); - }); - - test('returns null when the member has only non-matching annotations', () { - var node = parseAndGetSingleMember('@NonexistantAnnotation\nvar a;'); - expect(instantiateAnnotation(node, TestAnnotation), isNull); - }); - - test('returns null when the member has no annotations', () { - var node = parseAndGetSingleMember('var a;'); - expect(instantiateAnnotation(node, TestAnnotation), isNull); - }); - }); } diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart deleted file mode 100644 index 6d40676af..000000000 --- a/test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2015 Workiva Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ - -@TestOn('vm') -library; - -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:test/test.dart'; - -import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; - -import '../test_utils.dart'; - -void main() { - group('NodeWithMeta', () { - test('instantiates and provides access to an annotation and node', () { - var member = parseAndGetSingleMember('@TestAnnotation("hello")\nvar a;'); - var nodeWithMeta = - NodeWithMeta( - member as TopLevelVariableDeclaration); - - expect(nodeWithMeta.node, same(member)); - expect(nodeWithMeta.metaNode, isNotNull); - expect(nodeWithMeta.metaNode!.name.name, 'TestAnnotation'); - expect(nodeWithMeta.meta, isNotNull); - expect(nodeWithMeta.meta!.positional, 'hello'); - }); - - test('partially instantiates an "incomplete" annotation', () { - var member = parseAndGetSingleMember( - '@TestAnnotation(someIdentifier, named: "hello")\nvar a;'); - var nodeWithMeta = - NodeWithMeta( - member as TopLevelVariableDeclaration); - - expect(nodeWithMeta.node, same(member)); - expect(nodeWithMeta.metaNode, isNotNull); - expect(nodeWithMeta.metaNode!.name.name, 'TestAnnotation'); - - expect(nodeWithMeta.isIncomplete, isTrue); - expect(nodeWithMeta.unsupportedArguments, hasLength(1)); - expect(() => nodeWithMeta.meta, throwsStateError); - - expect(nodeWithMeta.potentiallyIncompleteMeta, isNotNull, - reason: - 'should still have attempted to instantiate the incomplete annotation'); - expect(nodeWithMeta.potentiallyIncompleteMeta!.named, equals('hello'), - reason: 'should still have passed the supported argument'); - expect(nodeWithMeta.potentiallyIncompleteMeta!.positional, isNull, - reason: 'should have used null for unsupported argument'); - }); - - test('gracefully handles a node without an annotation', () { - var member = parseAndGetSingleMember('var a;'); - var nodeWithMeta = - NodeWithMeta( - member as TopLevelVariableDeclaration); - - expect(nodeWithMeta.node, same(member)); - expect(nodeWithMeta.metaNode, isNull); - expect(nodeWithMeta.meta, isNull); - }); - }); -} From 23f3426311201db2ed134757524623a28caff4d6 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 10:37:27 -0700 Subject: [PATCH 02/17] Move doc metadata parsing to remove dart:mirrors from plugin lib --- .../src/doc_utils/docs_meta_annotation.dart | 2 +- .../documented_contributor_meta.dart | 59 ++----------------- tools/analyzer_plugin/tool/doc.dart | 5 +- .../annotations_from_ast.dart | 55 +++++++++++++++++ .../constant_instantiation.dart | 0 5 files changed, 63 insertions(+), 58 deletions(-) create mode 100644 tools/analyzer_plugin/tool/doc_generation_utils/annotations_from_ast.dart rename tools/analyzer_plugin/{lib/src/util => tool/doc_generation_utils}/constant_instantiation.dart (100%) diff --git a/tools/analyzer_plugin/lib/src/doc_utils/docs_meta_annotation.dart b/tools/analyzer_plugin/lib/src/doc_utils/docs_meta_annotation.dart index 234a8e6c1..db2bc10eb 100644 --- a/tools/analyzer_plugin/lib/src/doc_utils/docs_meta_annotation.dart +++ b/tools/analyzer_plugin/lib/src/doc_utils/docs_meta_annotation.dart @@ -6,7 +6,7 @@ import 'package:over_react_analyzer_plugin/src/doc_utils/maturity.dart'; /// * `AssistKind` static field within an assist class instance /// * `DiagnosticCode` static field within a `DiagnosticContributor` instance /// -/// > See: [DocumentedAssistContributorMeta.fromAnnotatedFieldAst] and [DocumentedDiagnosticContributorMeta..fromAnnotatedField] +/// > See: `getAssistDocsMetaFromNode` and `getDiagnosticDocsMetaFromNode` class DocsMeta implements IContributorMetaBase { @override final String description; diff --git a/tools/analyzer_plugin/lib/src/doc_utils/documented_contributor_meta.dart b/tools/analyzer_plugin/lib/src/doc_utils/documented_contributor_meta.dart index 4eeb2aa9d..c484b2866 100644 --- a/tools/analyzer_plugin/lib/src/doc_utils/documented_contributor_meta.dart +++ b/tools/analyzer_plugin/lib/src/doc_utils/documented_contributor_meta.dart @@ -27,10 +27,8 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import 'package:analyzer/dart/element/element.dart'; import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; import 'package:over_react_analyzer_plugin/src/doc_utils/maturity.dart'; -import 'package:over_react_analyzer_plugin/src/util/constant_instantiation.dart'; /// An interface implemented by registered metadata models and the annotation(s) /// that enable contribution to those models. @@ -68,25 +66,12 @@ abstract class DocumentedContributorMetaBase implements IContributorMetaBase { /// /// > __Required.__ String get name; - - /// Returns a `DocsMeta` instance constructed by parsing the AST of the provided annotated [element]. - static DocsMeta getDocsMetaFromAnnotation(FieldElement element) { - final annotation = - element.metadata.singleWhere((a) => a.element!.thisOrAncestorOfType()!.name == 'DocsMeta'); - final annotationObj = annotation.computeConstantValue()!; - final description = annotationObj.getField('description')!.toStringValue()!; - final details = annotationObj.getField('details')!.toStringValue(); - final maturity = getMatchingConst(annotationObj.getField('maturity')!, Maturity.VALUES); - final since = annotationObj.getField('since')!.toStringValue() ?? IContributorMetaBase.defaultSince; - - return DocsMeta(description, details: details, since: since, maturity: maturity); - } } /// Documentation metadata for an "assist" contributor. /// /// Must be constructed by parsing the AST of an element annotated with a [DocsMeta] annotation -/// via the [DocumentedAssistContributorMeta.fromAnnotatedFieldAst] factory constructor. +/// via the [`getAssistDocsMetaFromNode`] constructor. class DocumentedAssistContributorMeta extends DocumentedContributorMetaBase implements Comparable { @override @@ -100,27 +85,11 @@ class DocumentedAssistContributorMeta extends DocumentedContributorMetaBase @override final Maturity maturity; - DocumentedAssistContributorMeta._(this.name, this.description, + DocumentedAssistContributorMeta(this.name, this.description, {this.details, this.since = IContributorMetaBase.defaultSince, this.maturity = IContributorMetaBase.defaultMaturity}); - /// Creates a new instance from the field [element] annotated with a [DocsMeta] annotation. - factory DocumentedAssistContributorMeta.fromAnnotatedFieldAst(FieldElement element) { - final metaFromAnnotation = DocumentedContributorMetaBase.getDocsMetaFromAnnotation(element); - - final assistKindObj = element.computeConstantValue()!; - final name = assistKindObj.getField('id')!.toStringValue()!; - - return DocumentedAssistContributorMeta._( - name, - metaFromAnnotation.description, - details: metaFromAnnotation.details, - since: metaFromAnnotation.since, - maturity: metaFromAnnotation.maturity, - ); - } - @override int compareTo(DocumentedAssistContributorMeta other) { return name.compareTo(other.name); @@ -130,7 +99,7 @@ class DocumentedAssistContributorMeta extends DocumentedContributorMetaBase /// Documentation metadata for a "diagnostic" contributor _(e.g. lint / "rule")_. /// /// Must be constructed by parsing the AST of an element annotated with a [DocsMeta] annotation -/// via the [DocumentedDiagnosticContributorMeta..fromAnnotatedField] factory constructor. +/// via the `getDiagnosticDocsMetaFromNode` factory constructor. class DocumentedDiagnosticContributorMeta extends DocumentedContributorMetaBase implements Comparable { @override @@ -154,7 +123,7 @@ class DocumentedDiagnosticContributorMeta extends DocumentedContributorMetaBase /// > Should always match the [DiagnosticCode.type] value. final AnalysisErrorType type; - DocumentedDiagnosticContributorMeta._( + DocumentedDiagnosticContributorMeta( this.name, this.description, { required this.severity, @@ -164,26 +133,6 @@ class DocumentedDiagnosticContributorMeta extends DocumentedContributorMetaBase this.details, }); - /// Creates a new instance from the field [element] annotated with a [DocsMeta] annotation. - factory DocumentedDiagnosticContributorMeta.fromAnnotatedField(FieldElement element) { - final metaFromAnnotation = DocumentedContributorMetaBase.getDocsMetaFromAnnotation(element); - - final diagnosticCodeObj = element.computeConstantValue()!; - final name = diagnosticCodeObj.getField('name')!.toStringValue()!; - final severity = diagnosticCodeObj.getField('errorSeverity')!; - final type = diagnosticCodeObj.getField('type')!; - - return DocumentedDiagnosticContributorMeta._( - name, - metaFromAnnotation.description, - details: metaFromAnnotation.details, - since: metaFromAnnotation.since, - maturity: metaFromAnnotation.maturity, - severity: getMatchingConst(severity, AnalysisErrorSeverity.VALUES), - type: getMatchingConst(type, AnalysisErrorType.VALUES), - ); - } - @override int compareTo(DocumentedDiagnosticContributorMeta other) { var g = severity.name.compareTo(other.severity.name); diff --git a/tools/analyzer_plugin/tool/doc.dart b/tools/analyzer_plugin/tool/doc.dart index 08819c23e..f226c2d52 100644 --- a/tools/analyzer_plugin/tool/doc.dart +++ b/tools/analyzer_plugin/tool/doc.dart @@ -35,6 +35,7 @@ import 'package:over_react_analyzer_plugin/src/doc_utils/contributor_meta_regist import 'package:over_react_analyzer_plugin/src/doc_utils/documented_contributor_meta.dart'; import 'package:over_react_analyzer_plugin/src/util/constants.dart'; +import 'doc_generation_utils/annotations_from_ast.dart'; import 'doc_generation_utils/config.dart'; import 'doc_generation_utils/generate_assist_docs.dart'; import 'doc_generation_utils/generate_rule_docs.dart'; @@ -50,7 +51,7 @@ final configs = [ getSortedContributorMetas: () => RulesIndexer.rules, typeNameOfContributorClass: 'DiagnosticContributor', typeNameOfAnnotatedField: 'DiagnosticCode', - getMeta: DocumentedDiagnosticContributorMeta.fromAnnotatedField, + getMeta: getDiagnosticDocsMetaFromNode, getPageGenerator: (meta) => RuleDocGenerator(meta as DocumentedDiagnosticContributorMeta), getIndexGenerator: RulesIndexer.new, generateAdditionalDocs: (outDir) => OptionsSample(RulesIndexer.rules).generate(outDir), @@ -63,7 +64,7 @@ final configs = [ typeNameOfContributorClass: 'AssistContributorBase', typeNameOfAnnotatedField: 'AssistKind', packageNameContainingAnnotatedFieldType: 'analyzer_plugin', - getMeta: DocumentedAssistContributorMeta.fromAnnotatedFieldAst, + getMeta: getAssistDocsMetaFromNode, getPageGenerator: (meta) => AssistDocGenerator(meta as DocumentedAssistContributorMeta), getIndexGenerator: AssistsIndexer.new, ), diff --git a/tools/analyzer_plugin/tool/doc_generation_utils/annotations_from_ast.dart b/tools/analyzer_plugin/tool/doc_generation_utils/annotations_from_ast.dart new file mode 100644 index 000000000..ca5e25642 --- /dev/null +++ b/tools/analyzer_plugin/tool/doc_generation_utils/annotations_from_ast.dart @@ -0,0 +1,55 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; +import 'package:over_react_analyzer_plugin/src/doc_utils/documented_contributor_meta.dart'; +import 'package:over_react_analyzer_plugin/src/doc_utils/maturity.dart'; + +import 'constant_instantiation.dart'; + +/// Returns a `DocsMeta` instance constructed by parsing the AST of the provided annotated [element]. +DocsMeta _getDocsMetaFromAnnotation(FieldElement element) { + final annotation = + element.metadata.singleWhere((a) => a.element!.thisOrAncestorOfType()!.name == 'DocsMeta'); + final annotationObj = annotation.computeConstantValue()!; + final description = annotationObj.getField('description')!.toStringValue()!; + final details = annotationObj.getField('details')!.toStringValue(); + final maturity = getMatchingConst(annotationObj.getField('maturity')!, Maturity.VALUES); + final since = annotationObj.getField('since')!.toStringValue() ?? IContributorMetaBase.defaultSince; + + return DocsMeta(description, details: details, since: since, maturity: maturity); +} + +/// Creates a new instance from the field [element] annotated with a [DocsMeta] annotation. +DocumentedDiagnosticContributorMeta getDiagnosticDocsMetaFromNode(FieldElement element) { + final metaFromAnnotation = _getDocsMetaFromAnnotation(element); + + final diagnosticCodeObj = element.computeConstantValue()!; + final name = diagnosticCodeObj.getField('name')!.toStringValue()!; + final severity = diagnosticCodeObj.getField('errorSeverity')!; + final type = diagnosticCodeObj.getField('type')!; + + return DocumentedDiagnosticContributorMeta( + name, + metaFromAnnotation.description, + details: metaFromAnnotation.details, + since: metaFromAnnotation.since, + maturity: metaFromAnnotation.maturity, + severity: getMatchingConst(severity, AnalysisErrorSeverity.VALUES), + type: getMatchingConst(type, AnalysisErrorType.VALUES), + ); +} + +/// Creates a new instance from the field [element] annotated with a [DocsMeta] annotation. +DocumentedAssistContributorMeta getAssistDocsMetaFromNode(FieldElement element) { + final metaFromAnnotation = _getDocsMetaFromAnnotation(element); + + final assistKindObj = element.computeConstantValue()!; + final name = assistKindObj.getField('id')!.toStringValue()!; + + return DocumentedAssistContributorMeta( + name, + metaFromAnnotation.description, + details: metaFromAnnotation.details, + since: metaFromAnnotation.since, + maturity: metaFromAnnotation.maturity, + ); +} diff --git a/tools/analyzer_plugin/lib/src/util/constant_instantiation.dart b/tools/analyzer_plugin/tool/doc_generation_utils/constant_instantiation.dart similarity index 100% rename from tools/analyzer_plugin/lib/src/util/constant_instantiation.dart rename to tools/analyzer_plugin/tool/doc_generation_utils/constant_instantiation.dart From e5d98bee37bf0357c877815cd04955fcd6ff1383 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 13:17:06 -0700 Subject: [PATCH 03/17] Remove more dart:mirrors --- lib/src/builder/util.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/builder/util.dart b/lib/src/builder/util.dart index 9bc2ac49e..49474c922 100644 --- a/lib/src/builder/util.dart +++ b/lib/src/builder/util.dart @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:mirrors'; - import 'package:analyzer/dart/ast/ast.dart'; import 'package:build/build.dart' show AssetId; import 'package:over_react/src/builder/parsing.dart'; @@ -26,10 +24,6 @@ const privateSourcePrefix = r'_$'; const privatePrefix = r'_'; const publicGeneratedPrefix = r'$'; -String getName(Type type) { - return MirrorSystem.getName(reflectType(type).simpleName); -} - /// Converts [id] to a "package:" URI. /// /// This will return a schemeless URI if [id] doesn't represent a library in From 94d3d72f050912339bd0f858da35163a0c07532e Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:00:03 -0700 Subject: [PATCH 04/17] Improve name --- lib/src/builder/parsing/meta.dart | 2 +- .../builder/vendor/transformer_utils/src/analyzer_helpers.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/builder/parsing/meta.dart b/lib/src/builder/parsing/meta.dart index d1826ecca..d61b78a40 100644 --- a/lib/src/builder/parsing/meta.dart +++ b/lib/src/builder/parsing/meta.dart @@ -142,7 +142,7 @@ T? instantiateAnnotationTyped( node.metadata.firstWhereOrNull((m) => m.name.name == annotationClass.className); if (annotation == null) return null; - final args = getAnnotationArgs(annotation, onUnsupportedArgument: onUnsupportedArgument); + final args = parseAnnotationArgs(annotation, onUnsupportedArgument: onUnsupportedArgument); switch (annotationClass) { case _AnnotationClass.props: diff --git a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart index 8cd352038..649d080e1 100644 --- a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart +++ b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart @@ -76,7 +76,7 @@ class AnnotationArgs { AnnotationArgs({required this.named, required this.positional}); } -AnnotationArgs getAnnotationArgs(Annotation annotation, +AnnotationArgs parseAnnotationArgs(Annotation annotation, {dynamic Function(Expression argument)? onUnsupportedArgument}) { Map namedParameters = {}; List positionalParameters = []; From 12e648fb92c37282411beb7fba1d1e783033dc1c Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:16:59 -0700 Subject: [PATCH 05/17] Support analyzer 6.x in analyzer plugin --- .../lib/src/diagnostic/exhaustive_deps.dart | 21 +++++++++++++++++-- .../non_static_reference_visitor.dart | 2 +- tools/analyzer_plugin/pubspec.yaml | 2 +- .../test/integration/stubs.dart | 1 + 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart index 81c75407a..c22d2b437 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart @@ -680,7 +680,7 @@ class ExhaustiveDeps extends DiagnosticContributor { // NamedType case. This is for references to generic type parameters // (references to other types will get filtered out by the isDeclaredInPureScope check above). (reference) { - dependency = reference.name.name; + dependency = reference.nameLexeme; // These aren't possible for type annotations. isStable = false; isUsedAsCascadeTarget = false; @@ -1980,7 +1980,7 @@ _Recommendations collectRecommendations({ String? getConstructionExpressionType(Expression node) { if (node is InstanceCreationExpression) { if (node.isConst) return null; - return node.constructorName.type.name.name; + return node.constructorName.type.nameLexeme; } else if (node is ListLiteral) { return _DepType.list; } else if (node is SetOrMapLiteral) { @@ -2365,3 +2365,20 @@ extension> on Iterable { return true; } } + +extension TypeNameHelper on NamedType { + // Backwards compatibility for various analyzer versions that remove name/name2. + // ignore: unnecessary_this + dynamic get name => this.name2; // Use `this.` to point to real impl if it exists, not extension. + // ignore: unnecessary_this + 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 = _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}'); + } +} diff --git a/tools/analyzer_plugin/lib/src/diagnostic/visitors/non_static_reference_visitor.dart b/tools/analyzer_plugin/lib/src/diagnostic/visitors/non_static_reference_visitor.dart index 46596437d..083383090 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/visitors/non_static_reference_visitor.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/visitors/non_static_reference_visitor.dart @@ -70,7 +70,7 @@ bool referencesImplicitThis(SimpleIdentifier identifier) { } // not a class member final Element? enclosingElement = element.enclosingElement; - if (enclosingElement is! InterfaceOrAugmentationElement) { + if (enclosingElement is! InterfaceElement) { return false; } // comment diff --git a/tools/analyzer_plugin/pubspec.yaml b/tools/analyzer_plugin/pubspec.yaml index 09bf75bbc..2bee645c1 100644 --- a/tools/analyzer_plugin/pubspec.yaml +++ b/tools/analyzer_plugin/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/Workiva/over_react/tree/master/tools/analyzer_plu environment: sdk: '>=2.19.0 <3.0.0' dependencies: - analyzer: ^5.11.0 + analyzer: '>=5.11.0 <7.0.0' analyzer_plugin: ^0.11.0 collection: ^1.15.0-nullsafety.4 meta: ^1.16.0 diff --git a/tools/analyzer_plugin/test/integration/stubs.dart b/tools/analyzer_plugin/test/integration/stubs.dart index 6af6f0050..b66650633 100644 --- a/tools/analyzer_plugin/test/integration/stubs.dart +++ b/tools/analyzer_plugin/test/integration/stubs.dart @@ -101,6 +101,7 @@ class StubServerPlugin implements ServerPlugin { handleEditGetRefactoring(parameters) => throw UnimplementedError(); @override + // ignore: override_on_non_overriding_member handleKytheGetKytheEntries(parameters) => throw UnimplementedError(); @override From 0a03173f8daa9e9b8a67c084e5e37c1488d44b80 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:24:31 -0700 Subject: [PATCH 06/17] Fix lint in Dart 2.19.6 --- tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart index c22d2b437..ed1b10d53 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart @@ -2375,7 +2375,7 @@ extension TypeNameHelper on NamedType { dynamic get _name => name; String get nameLexeme { - final name = _name; + final dynamic name = _name; if (name is Identifier) return name.name; if (name is Token) return name.lexeme; if (name is String) return name; From c2c38fa041e2e77089c5bd0551250c393222311e Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:26:19 -0700 Subject: [PATCH 07/17] Use nameLexeme from over_react --- .../lib/src/diagnostic/exhaustive_deps.dart | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart index ed1b10d53..8f8f5efb2 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart @@ -34,6 +34,7 @@ import 'package:analyzer_plugin/protocol/protocol_common.dart' show Location; import 'package:over_react_analyzer_plugin/src/diagnostic/analyzer_debug_helper.dart'; import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; import 'package:over_react_analyzer_plugin/src/indent_util.dart'; +import 'package:over_react_analyzer_plugin/src/over_react_builder_parsing.dart' show TypeNameHelper; import 'package:over_react_analyzer_plugin/src/util/ast_util.dart'; import 'package:over_react_analyzer_plugin/src/util/function_components.dart'; import 'package:over_react_analyzer_plugin/src/util/pretty_print.dart'; @@ -2365,20 +2366,3 @@ extension> on Iterable { return true; } } - -extension TypeNameHelper on NamedType { - // Backwards compatibility for various analyzer versions that remove name/name2. - // ignore: unnecessary_this - dynamic get name => this.name2; // Use `this.` to point to real impl if it exists, not extension. - // ignore: unnecessary_this - 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 dynamic name = _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}'); - } -} From 956897f3f7798ec47b9ad831df124f4191a4d2cc Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:26:59 -0700 Subject: [PATCH 08/17] Fix deprecated name broken in new analyzer --- tools/analyzer_plugin/lib/src/assist/toggle_stateful.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/analyzer_plugin/lib/src/assist/toggle_stateful.dart b/tools/analyzer_plugin/lib/src/assist/toggle_stateful.dart index 4adf0fbff..ab9cd952e 100644 --- a/tools/analyzer_plugin/lib/src/assist/toggle_stateful.dart +++ b/tools/analyzer_plugin/lib/src/assist/toggle_stateful.dart @@ -47,7 +47,7 @@ class ToggleComponentStatefulness extends AssistContributorBase with ComponentDe await super.computeAssists(request, collector); if (!setupCompute() || !initializeAssistApi(request.result.content)) return; - newComponentBaseClass = _getNewBase(componentSupertypeNode.name.name); + newComponentBaseClass = _getNewBase(componentSupertypeNode.nameLexeme); // If there is no known corresponding base class, short circuit. if (newComponentBaseClass == null) return; From 09181aa13f48285a3ccaca79e2f95a2dc6db7a45 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:37:25 -0700 Subject: [PATCH 09/17] Ignore mustCallSuper errors in generated code in tests --- tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart b/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart index fd19f2242..e481dd115 100644 --- a/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart +++ b/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart @@ -42,6 +42,8 @@ Future resolveFileAndGeneratedPart(SharedAnalysisContext sha .where((e) => e.severity != Severity.info && !e.errorCode.name.toLowerCase().startsWith('unused_')) // FIXME(FED-2015) remove once these are properly ignored in generated code .where((e) => e.errorCode.name.toLowerCase() != 'invalid_use_of_visible_for_overriding_member') + // For some reason this is triggering for '$mustCallSuper'; ignore for now. FIXME look into what's going on here + .where((e) => e.errorCode.name.toLowerCase() != 'invalid_annotation_target') .toList(); final libraryResult = await getResolvedUnit(libraryFullPath); From dacf413eed329893766699a0e9016aa5bc1a8198 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Mon, 2 Mar 2026 14:52:25 -0700 Subject: [PATCH 10/17] Downgrade FIXME --- tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart b/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart index e481dd115..3730ba48b 100644 --- a/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart +++ b/tools/analyzer_plugin/test/unit/util/prop_declaration/util.dart @@ -42,7 +42,7 @@ Future resolveFileAndGeneratedPart(SharedAnalysisContext sha .where((e) => e.severity != Severity.info && !e.errorCode.name.toLowerCase().startsWith('unused_')) // FIXME(FED-2015) remove once these are properly ignored in generated code .where((e) => e.errorCode.name.toLowerCase() != 'invalid_use_of_visible_for_overriding_member') - // For some reason this is triggering for '$mustCallSuper'; ignore for now. FIXME look into what's going on here + // For some reason this is triggering for '$mustCallSuper'; ignore for now. TODO look into what's going on here .where((e) => e.errorCode.name.toLowerCase() != 'invalid_annotation_target') .toList(); From 1171d501ddff7171a3289210092c5f363920addb Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 09:27:45 -0700 Subject: [PATCH 11/17] Clean up casts with utility --- lib/src/builder/parsing/meta.dart | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/src/builder/parsing/meta.dart b/lib/src/builder/parsing/meta.dart index d61b78a40..f47ab5bec 100644 --- a/lib/src/builder/parsing/meta.dart +++ b/lib/src/builder/parsing/meta.dart @@ -143,57 +143,58 @@ T? instantiateAnnotationTyped( if (annotation == null) return null; final args = parseAnnotationArgs(annotation, onUnsupportedArgument: onUnsupportedArgument); + S namedArg(String name) => args.named[name] as S; switch (annotationClass) { case _AnnotationClass.props: return a.Props( - keyNamespace: args.named['keyNamespace'] as String?, - disableRequiredPropValidation: args.named['disableRequiredPropValidation'] as Set?, + keyNamespace: namedArg('keyNamespace'), + disableRequiredPropValidation: namedArg('disableRequiredPropValidation'), disableValidationForClassDefaultProps: - args.named['disableValidationForClassDefaultProps'] as bool? ?? true, + namedArg('disableValidationForClassDefaultProps') ?? true, ) as T; case _AnnotationClass.abstractProps: return a.AbstractProps( - keyNamespace: args.named['keyNamespace'] as String?, + keyNamespace: namedArg('keyNamespace'), ) as T; case _AnnotationClass.propsMixin: // ignore: deprecated_member_use_from_same_package return a.PropsMixin( - keyNamespace: args.named['keyNamespace'] as String?, + keyNamespace: namedArg('keyNamespace'), ) as T; case _AnnotationClass.state: return a.State( - keyNamespace: args.named['keyNamespace'] as String?, + keyNamespace: namedArg('keyNamespace'), ) as T; case _AnnotationClass.abstractState: return a.AbstractState( - keyNamespace: args.named['keyNamespace'] as String?, + keyNamespace: namedArg('keyNamespace'), ) as T; case _AnnotationClass.stateMixin: // ignore: deprecated_member_use_from_same_package return a.StateMixin( - keyNamespace: args.named['keyNamespace'] as String?, + keyNamespace: namedArg('keyNamespace'), ) as T; case _AnnotationClass.component2: return a.Component2( - isWrapper: args.named['isWrapper'] as bool? ?? false, - subtypeOf: args.named['subtypeOf'] as Type?, - isErrorBoundary: args.named['isErrorBoundary'] as bool? ?? false, + isWrapper: namedArg('isWrapper') ?? false, + subtypeOf: namedArg('subtypeOf'), + isErrorBoundary: namedArg('isErrorBoundary') ?? false, ) as T; case _AnnotationClass.component: // ignore: deprecated_member_use_from_same_package return a.Component( - isWrapper: args.named['isWrapper'] as bool? ?? false, - subtypeOf: args.named['subtypeOf'] as Type?, + isWrapper: namedArg('isWrapper') ?? false, + subtypeOf: namedArg('subtypeOf'), ) as T; case _AnnotationClass.accessor: return a.Accessor( - key: args.named['key'] as String?, - keyNamespace: args.named['keyNamespace'] as String?, - isRequired: (args.named['isRequired'] as bool?) ?? false, - isNullable: (args.named['isNullable'] as bool?) ?? false, - requiredErrorMessage: args.named['requiredErrorMessage'] as String?, - doNotGenerate: (args.named['doNotGenerate'] as bool?) ?? false, + key: namedArg('key'), + keyNamespace: namedArg('keyNamespace'), + isRequired: namedArg('isRequired') ?? false, + isNullable: namedArg('isNullable') ?? false, + requiredErrorMessage: namedArg('requiredErrorMessage'), + doNotGenerate: namedArg('doNotGenerate') ?? false, ) as T; } } From e32b7e26e0b0f3b1e8c160dd8381b7c5036912d8 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 10:40:19 -0700 Subject: [PATCH 12/17] DRY up --- lib/src/builder/parsing/meta.dart | 52 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/lib/src/builder/parsing/meta.dart b/lib/src/builder/parsing/meta.dart index f47ab5bec..d30ffe13c 100644 --- a/lib/src/builder/parsing/meta.dart +++ b/lib/src/builder/parsing/meta.dart @@ -23,12 +23,13 @@ import '../vendor/transformer_utils/src/analyzer_helpers.dart'; /// Returns the first annotation AST node on [node] of type [T], /// or null if no matching annotations are found. -Annotation? _getMatchingAnnotation(AnnotatedNode node) { - final annotationClass = getAnnotationClassFromGeneric(); - if (annotationClass == null) return null; +Annotation? _getMatchingAnnotationFromGeneric(AnnotatedNode node) => + _getMatchingAnnotation(_AnnotationClass.fromGeneric(), node); - return node.metadata.firstWhereOrNull((m) => m.name.name == annotationClass.className); -} +/// Returns the first annotation AST node on [node] of type [annotationClass], +/// or null if no matching annotations are found. +Annotation? _getMatchingAnnotation(_AnnotationClass annotationClass, AnnotatedNode node) => + node.metadata.firstWhereOrNull((m) => m.name.name == annotationClass.className); /// Utility class that allows partial instantiation of annotations, to support reading /// annotation data in a context without a resolved AST. See [isIncomplete] for more info. @@ -52,7 +53,7 @@ class InstantiatedMeta { /// /// The instantiated annotation will be available via [value]. static InstantiatedMeta? fromNode(AnnotatedNode node) { - final metaNode = _getMatchingAnnotation(node); + final metaNode = _getMatchingAnnotationFromGeneric(node); if (metaNode == null) return null; final unsupportedArguments = []; @@ -134,12 +135,9 @@ T? instantiateAnnotationTyped( AnnotatedNode node, { dynamic Function(Expression argument)? onUnsupportedArgument, }) { - // TODO DRY up - final annotationClass = getAnnotationClassFromGeneric(); - if (annotationClass == null) return null; + final annotationClass = _AnnotationClass.fromGeneric(); - final annotation = - node.metadata.firstWhereOrNull((m) => m.name.name == annotationClass.className); + final annotation = _getMatchingAnnotation(annotationClass, node); if (annotation == null) return null; final args = parseAnnotationArgs(annotation, onUnsupportedArgument: onUnsupportedArgument); @@ -213,23 +211,23 @@ enum _AnnotationClass { final String className; const _AnnotationClass(this.className); -} -@visibleForTesting -_AnnotationClass? getAnnotationClassFromGeneric() { - if (_isSubtypeOf()) return _AnnotationClass.props; - if (_isSubtypeOf()) return _AnnotationClass.abstractProps; - // ignore: deprecated_member_use_from_same_package - if (_isSubtypeOf()) return _AnnotationClass.propsMixin; - if (_isSubtypeOf()) return _AnnotationClass.state; - if (_isSubtypeOf()) return _AnnotationClass.abstractState; - // ignore: deprecated_member_use_from_same_package - if (_isSubtypeOf()) return _AnnotationClass.stateMixin; - if (_isSubtypeOf()) return _AnnotationClass.component2; - // ignore: deprecated_member_use_from_same_package - if (_isSubtypeOf()) return _AnnotationClass.component; - if (_isSubtypeOf()) return _AnnotationClass.accessor; - return null; + static _AnnotationClass fromGeneric() { + if (_isSubtypeOf()) return _AnnotationClass.props; + if (_isSubtypeOf()) return _AnnotationClass.abstractProps; + // ignore: deprecated_member_use_from_same_package + if (_isSubtypeOf()) return _AnnotationClass.propsMixin; + if (_isSubtypeOf()) return _AnnotationClass.state; + if (_isSubtypeOf()) return _AnnotationClass.abstractState; + // ignore: deprecated_member_use_from_same_package + if (_isSubtypeOf()) return _AnnotationClass.stateMixin; + if (_isSubtypeOf()) return _AnnotationClass.component2; + // ignore: deprecated_member_use_from_same_package + if (_isSubtypeOf()) return _AnnotationClass.component; + if (_isSubtypeOf()) return _AnnotationClass.accessor; + + throw ArgumentError('Unsupported generic: $T. Must correspond to a type in this enum.'); + } } bool _isSubtypeOf() => _SubtypeOfHelper() is _SubtypeOfHelper; From 9bd168ce5b9854dc5051cfa791ec326ef21dbc3b Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 10:44:43 -0700 Subject: [PATCH 13/17] Fix unused import --- lib/src/builder/parsing/meta.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/builder/parsing/meta.dart b/lib/src/builder/parsing/meta.dart index d30ffe13c..3e59f0cd5 100644 --- a/lib/src/builder/parsing/meta.dart +++ b/lib/src/builder/parsing/meta.dart @@ -15,7 +15,6 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:build/build.dart' show log; import 'package:collection/collection.dart' show IterableExtension; -import 'package:meta/meta.dart'; import 'package:over_react/src/component_declaration/annotations.dart' as a; From 0958614fbd414ce1672381e6c6ced686c9daa2ff Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 10:45:10 -0700 Subject: [PATCH 14/17] Remove invalid tests --- test/vm_tests/builder/parsing/meta_test.dart | 77 -------------------- 1 file changed, 77 deletions(-) delete mode 100644 test/vm_tests/builder/parsing/meta_test.dart diff --git a/test/vm_tests/builder/parsing/meta_test.dart b/test/vm_tests/builder/parsing/meta_test.dart deleted file mode 100644 index a353a17c3..000000000 --- a/test/vm_tests/builder/parsing/meta_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2020 Workiva Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -@TestOn('vm') -import 'package:over_react/src/builder/parsing/meta.dart'; -import 'package:test/test.dart'; - -import '../ast_test_util.dart'; - -void main() { - group('InstantiatedMeta', () { - test('instantiates and provides access to an annotation and node', () { - final member = parseAndGetSingleMember(r''' - @TestAnnotation("hello") - var a; - '''); - final meta = InstantiatedMeta.fromNode(member)!; - - expect(meta.metaNode.name.name, 'TestAnnotation'); - expect(meta.value, isNotNull); - expect(meta.value.positional, 'hello'); - }); - - test('partially instantiates an "incomplete" annotation', () { - final member = parseAndGetSingleMember(r''' - @TestAnnotation(someIdentifier, named: "hello") - var a; - '''); - final meta = InstantiatedMeta.fromNode(member)!; - - expect(meta.metaNode.name.name, 'TestAnnotation'); - - expect(meta.isIncomplete, isTrue); - expect(meta.unsupportedArguments, hasLength(1)); - expect(() => meta.value, throwsStateError); - - expect(meta.potentiallyIncompleteValue, isNotNull, - reason: 'should still have attempted to instantiate the incomplete annotation'); - expect(meta.potentiallyIncompleteValue.named, equals('hello'), - reason: 'should still have passed the supported argument'); - expect(meta.potentiallyIncompleteValue.positional, isNull, - reason: 'should have used null for unsupported argument'); - }); - - test('gracefully handles a node without a matching annotation and returns null', () { - final member = parseAndGetSingleMember(r''' - var a; - '''); - final meta = InstantiatedMeta.fromNode(member); - - expect(meta, isNull); - }); - }); -} - -class TestAnnotation { - final Object? positional; - final Object? named; - final Object? namedConstructorOnly; - - const TestAnnotation(this.positional, {this.named}) : namedConstructorOnly = null; - - const TestAnnotation.namedConstructor({this.namedConstructorOnly}) - : positional = null, - named = null; -} From 002e69304862e3050e5750b671668bb655134b67 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 10:51:58 -0700 Subject: [PATCH 15/17] Add doc comments --- .../vendor/transformer_utils/src/analyzer_helpers.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart index 649d080e1..5f3f644fa 100644 --- a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart +++ b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart @@ -69,6 +69,7 @@ dynamic getValue(Expression expression, 'Must be a uninterpolated string, boolean, integer, or null literal.'); } +/// The parsed arguments of an annotation constructor call, from [parseAnnotationArgs]. class AnnotationArgs { final Map named; final List positional; @@ -76,6 +77,9 @@ class AnnotationArgs { AnnotationArgs({required this.named, required this.positional}); } +/// Returns annotation class constructor arguments parsed from [annotation], +/// converting supported literal values via [getValue], and passing unsupported +/// argument values through [onUnsupportedArgument]. AnnotationArgs parseAnnotationArgs(Annotation annotation, {dynamic Function(Expression argument)? onUnsupportedArgument}) { Map namedParameters = {}; From 4fab2df6247cf8a2eee0023723fc502fd60c9bf5 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 10:53:16 -0700 Subject: [PATCH 16/17] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5671378bf..76fa1cd21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # OverReact Changelog +## Unreleased +- Fix analyzer plugin not starting in Dart 3, with AOT compilation error + ## 5.6.1 - Allow w_common 4 #1004 https://github.com/Workiva/over_react/pull/1004 From f04148d636ec2f828e2d3249c9757ac9e70473a0 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Tue, 24 Mar 2026 13:10:32 -0700 Subject: [PATCH 17/17] Update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76fa1cd21..edc8c0c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # OverReact Changelog ## Unreleased -- Fix analyzer plugin not starting in Dart 3, with AOT compilation error +- Analyzer plugin + - Fix startup error (AOT compilation) in Dart 3 + - Allow analyzer 6.x, allowing parsing language versions <=3.7 ## 5.6.1 - Allow w_common 4 #1004 https://github.com/Workiva/over_react/pull/1004