From 279d9b42c2d4f18fbef29efc87b029e49a2d51af Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Mon, 3 Feb 2020 12:33:47 -0700 Subject: [PATCH 01/11] Add standalone PropsMixin / StateMixin migrator + CPLAT-9411 --- .../boilerplate_utilities.dart | 143 ++++++- .../props_meta_migrator.dart | 2 + .../props_mixins_migrator.dart | 66 ++++ lib/src/util.dart | 11 + .../props_mixin_migrator_test.dart | 354 ++++++++++++++++++ 5 files changed, 563 insertions(+), 13 deletions(-) create mode 100644 test/boilerplate_suggestors/props_mixin_migrator_test.dart diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 268d5c6d..83c3f276 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -13,21 +13,56 @@ // limitations under the License. import 'package:analyzer/dart/ast/ast.dart'; +import 'package:meta/meta.dart'; import 'package:over_react_codemod/src/constants.dart'; import 'package:over_react_codemod/src/util.dart'; typedef void YieldPatch( int startingOffset, int endingOffset, String replacement); +@visibleForTesting +bool isPublicForTest = false; + // Stub while is in progress -bool _isPublic(ClassDeclaration node) => false; +bool isPublic(ClassDeclaration node) => isPublicForTest; + +/// A simple evaluation of the annotation(s) of the [classNode] +/// to verify it is either a `@PropsMixin()` or `@StateMixin`. +bool isAPropsOrStateMixin(ClassDeclaration classNode) => + isAPropsMixin(classNode) || isAStateMixin(classNode); + +/// Returns the node of a `@PropsMixin()` annotation for the provided [classNode], if one exists. +AstNode getPropsMixinAnnotationNode(ClassDeclaration classNode) => + classNode.sortedCommentAndAnnotations.singleWhere( + (node) => node?.toSource()?.startsWith('@PropsMixin') == true, + orElse: () => null); + +/// A simple evaluation of the annotation(s) of the [classNode] +/// to verify it is a `@PropsMixin()`. +bool isAPropsMixin(ClassDeclaration classNode) => + getPropsMixinAnnotationNode(classNode) != null; + +/// Returns the node of a `@PropsMixin()` annotation for the provided [classNode], if one exists. +AstNode getStateMixinAnnotationNode(ClassDeclaration classNode) => + classNode.sortedCommentAndAnnotations.singleWhere( + (node) => node?.toSource()?.startsWith('@StateMixin') == true, + orElse: () => null); + +/// A simple evaluation of the annotation(s) of the [classNode] +/// to verify it is a `@StateMixin()`. +bool isAStateMixin(ClassDeclaration classNode) => + getStateMixinAnnotationNode(classNode) != null; + +/// Whether a props or state mixin class [classNode] should be migrated as part of the boilerplate codemod. +bool shouldMigratePropsAndStateMixin(ClassDeclaration classNode) => + isAPropsOrStateMixin(classNode); /// Whether a props or state class class [node] should be migrated as part of the boilerplate codemod. bool shouldMigratePropsAndStateClass(ClassDeclaration node) { return isAssociatedWithComponent2(node) && isAPropsOrStateClass(node) && // Stub while is in progress - !_isPublic(node); + !isPublic(node); } /// A simple RegExp against the parent of the class to verify it is `UiProps` @@ -37,6 +72,14 @@ bool extendsFromUiPropsOrUiState(ClassDeclaration classNode) => .toSource() .contains(RegExp('(UiProps)|(UiState)')); +/// A simple RegExp against the parent of the class to verify it is `UiProps` +/// or `UiState`. +bool implementsUiPropsOrUiState(ClassDeclaration classNode) { + return classNode.implementsClause.interfaces + .map((typeName) => typeName.toSource()) + .any((typeStr) => typeStr.contains(RegExp('(UiProps)|(UiState)'))); +} + /// A simple RegExp against the name of the class to verify it contains `props` /// or `state`. bool isAPropsOrStateClass(ClassDeclaration classNode) => classNode.name @@ -73,9 +116,9 @@ bool isAdvancedPropsOrStateClass(ClassDeclaration classNode) { var propsAndStateClassNamesConvertedToNewBoilerplate = < /*old class name*/ String, /*new mixin name*/ String>{}; -/// Used to switch a props or state class to a mixin. +/// Used to switch a props/state class, or a `@PropsMixin()`/`@StateMixin()` class to a mixin. /// -/// __EXAMPLE:__ +/// __EXAMPLE (Concrete Class):__ /// ```dart /// // Before /// class _$TestProps extends UiProps { @@ -90,6 +133,30 @@ var propsAndStateClassNamesConvertedToNewBoilerplate = /// } /// ``` /// +/// __EXAMPLE (`@PropsMixin`):__ +/// ```dart +/// // Before +/// @PropsMixin() +/// abstract class TestPropsMixin implements UiProps, BarPropsMixin { +/// // To ensure the codemod regression checking works properly, please keep this +/// // field at the top of the class! +/// // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value +/// static const PropsMeta meta = _$metaForTestPropsMixin; +/// +/// @override +/// Map get props; +/// +/// String var1; +/// String var2; +/// } +/// +/// // After +/// mixin TestPropsMixin on UiProps implements BarPropsMixin { +/// String var1; +/// String var2; +/// } +/// ``` +/// /// When a class is migrated, it gets added to [propsAndStateClassNamesConvertedToNewBoilerplate] /// so that suggestors that come after the suggestor that called this function - can know /// whether to yield a patch based on that information. @@ -105,15 +172,65 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, stripPrivateGeneratedPrefix(node.name.toSource()); String newMixinName = originalPublicClassName; - yieldPatch(node.name.token.offset, - node.name.token.offset + privateGeneratedPrefix.length, ''); - - yieldPatch(node.extendsClause.offset, - node.extendsClause.extendsKeyword.charEnd, 'on'); - - if (shouldAddMixinToName) { - yieldPatch(node.name.token.charEnd, node.name.token.charEnd, 'Mixin'); - newMixinName = '${newMixinName}Mixin'; + if (node.extendsClause?.extendsKeyword != null) { + // --- Convert concrete props/state class to a mixin --- // + + yieldPatch(node.name.token.offset, + node.name.token.offset + privateGeneratedPrefix.length, ''); + + yieldPatch(node.extendsClause.offset, + node.extendsClause.extendsKeyword.charEnd, 'on'); + + if (shouldAddMixinToName) { + yieldPatch(node.name.token.charEnd, node.name.token.charEnd, 'Mixin'); + newMixinName = '${newMixinName}Mixin'; + } + } else { + // --- Convert props/state mixin to an actual mixin --- // + + if (node.implementsClause?.implementsKeyword != null) { + final nodeInterfaces = node.implementsClause.interfaces; + // Implements an interface, and does not extend from another class + if (implementsUiPropsOrUiState(node)) { + if (nodeInterfaces.length == 1) { + // Only implements UiProps / UiState + yieldPatch(node.implementsClause.offset, + node.implementsClause.implementsKeyword.charEnd, 'on'); + } else { + // Implements UiProps / UiState along with other interfaces + final uiInterface = nodeInterfaces.singleWhere((interface) => + interface.toSource() == 'UiProps' || + interface.toSource() == 'UiState'); + final otherInterfaces = List.of(nodeInterfaces)..remove(uiInterface); + final otherInterfacesStr = + commaSeparatedAstNodeNames(otherInterfaces); + + yieldPatch(node.implementsClause.offset, node.implementsClause.end, + 'on ${uiInterface.toSource()} implements $otherInterfacesStr'); + } + } else { + // Does not implement UiProps / UiState + final uiInterfaceStr = isAPropsMixin(node) ? 'UiProps' : 'UiState'; + + if (nodeInterfaces.isNotEmpty) { + // But does implement other stuff + final otherInterfacesStr = commaSeparatedAstNodeNames(nodeInterfaces); + + yieldPatch(node.implementsClause.offset, node.implementsClause.end, + 'on $uiInterfaceStr implements $otherInterfacesStr'); + } else { + // Does not implement anything + yieldPatch(node.leftBracket.offset - 1, node.leftBracket.offset - 1, + 'on $uiInterfaceStr'); + } + } + } else { + // Does not implement anything + final uiInterfaceStr = isAPropsMixin(node) ? 'UiProps' : 'UiState'; + + yieldPatch( + node.name.token.end, node.name.token.end, ' on $uiInterfaceStr'); + } } propsAndStateClassNamesConvertedToNewBoilerplate[originalPublicClassName] = diff --git a/lib/src/boilerplate_suggestors/props_meta_migrator.dart b/lib/src/boilerplate_suggestors/props_meta_migrator.dart index 084f23f3..3f707e52 100644 --- a/lib/src/boilerplate_suggestors/props_meta_migrator.dart +++ b/lib/src/boilerplate_suggestors/props_meta_migrator.dart @@ -29,6 +29,8 @@ import 'package:over_react_codemod/src/boilerplate_suggestors/boilerplate_utilit /// // After /// propsMeta.forMixin(FooProps) /// ``` +/// +/// > Related: [PropsMixinMigrator] class PropsMetaMigrator extends GeneralizingAstVisitor with AstVisitingSuggestorMixin implements Suggestor { diff --git a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart index f52e708b..1055deb0 100644 --- a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart +++ b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart @@ -16,12 +16,78 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:codemod/codemod.dart'; +import 'boilerplate_utilities.dart'; + /// Suggestor that updates stand alone props mixins to be actual mixins. +/// +/// > Related: [PropsMetaMigrator] class PropsMixinMigrator extends GeneralizingAstVisitor with AstVisitingSuggestorMixin implements Suggestor { @override visitClassDeclaration(ClassDeclaration node) { super.visitClassDeclaration(node); + + if (!shouldMigratePropsAndStateMixin(node)) return; + + migrateClassToMixin(node, yieldPatch); + _removePropsOrStateGetter(node); + _removePropsOrStateMixinAnnotation(node); + _migrateMixinMetaField(node); + } + + void _removePropsOrStateGetter(ClassDeclaration node) { + final classGetters = node.members + .whereType() + .where((method) => method.isGetter); + final propsOrStateGetter = classGetters.singleWhere( + (getter) => getter.name.name == 'props' || getter.name.name == 'state', + orElse: () => null); + if (propsOrStateGetter != null) { + yieldPatch(propsOrStateGetter.offset, propsOrStateGetter.end + 1, ''); + } + } + + void _removePropsOrStateMixinAnnotation(ClassDeclaration node) { + final propsMixinAnnotationNode = getPropsMixinAnnotationNode(node); + if (propsMixinAnnotationNode != null) { + yieldPatch(propsMixinAnnotationNode.offset, + propsMixinAnnotationNode.end + 1, ''); + } + + final stateMixinAnnotationNode = getStateMixinAnnotationNode(node); + if (stateMixinAnnotationNode != null) { + yieldPatch(stateMixinAnnotationNode.offset, + stateMixinAnnotationNode.end + 1, ''); + } + } + + /// NOTE: Usage of the meta field elsewhere will be migrated via the [PropsMetaMigrator]. + void _migrateMixinMetaField(ClassDeclaration node) { + final classMembers = node.members; + final classFields = classMembers + .whereType() + .map((decl) => decl.fields) + .toList(); + final metaField = classFields.singleWhere( + (field) => field.variables.single.name.name == 'meta', + orElse: () => null); + if (metaField == null) return; + + if (isPublic(node)) { + yieldPatch(metaField.parent.offset, metaField.parent.offset, + '@Deprecated(\'Use `propsMeta.forMixin(${node.name.name})` instead.\')\n'); + } else { + // Remove the meta field, along with any comment lines that preceded it. + final metaFieldDecl = metaField.parent; + final previousMember = metaFieldDecl == classMembers.first + ? null + : classMembers[classMembers.indexOf(metaFieldDecl) - 1]; + final begin = previousMember != null + ? previousMember.end + 1 + : node.leftBracket.offset + 1; + + yieldPatch(begin, metaField.end + 1, ''); + } } } diff --git a/lib/src/util.dart b/lib/src/util.dart index 927fe689..1912c31e 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -446,3 +446,14 @@ String stripPrivateGeneratedPrefix(String value) { ? value.substring(privateGeneratedPrefix.length) : value; } + +/// Takes an iterable of AstNodes and returns their name in a comma separated string. +/// +/// Optionally, you can pass a function to [getName] if you want something other +/// than [AstNode.toSource] to be used to generate the name for the list. +String commaSeparatedAstNodeNames(Iterable nodeList, + {String Function(T node) getName}) { + getName ??= (node) => node.toSource(); + + return nodeList.map(getName).toString().replaceAll(RegExp(r'[\(\)]'), ''); +} diff --git a/test/boilerplate_suggestors/props_mixin_migrator_test.dart b/test/boilerplate_suggestors/props_mixin_migrator_test.dart new file mode 100644 index 00000000..a1101fb0 --- /dev/null +++ b/test/boilerplate_suggestors/props_mixin_migrator_test.dart @@ -0,0 +1,354 @@ +// 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. + +import 'package:over_react_codemod/src/boilerplate_suggestors/boilerplate_utilities.dart'; +import 'package:over_react_codemod/src/boilerplate_suggestors/props_mixins_migrator.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +main() { + group('PropsMixinMigrator', () { + final testSuggestor = getSuggestorTester(PropsMixinMigrator()); + + tearDown(() { + propsAndStateClassNamesConvertedToNewBoilerplate = {}; + }); + + group('does not perform a migration', () { + test('when it encounters an empty file', () { + testSuggestor(expectedPatchCount: 0, input: ''); + }); + + test('when there are no `PropsMixin()` annotations found', () { + testSuggestor( + expectedPatchCount: 0, + input: ''' + abstract class FooPropsMixin implements UiProps { + String foo; + } + ''', + ); + }); + }); + + void sharedTests(MixinType type) { + final typeStr = mixinStrByType[type]; + + group('converting the class to a mixin', () { + group('when the class implements Ui$typeStr', () { + test('only', () { + testSuggestor( + expectedPatchCount: 6, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Ui${typeStr} { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + @override + Map get ${typeStr.toLowerCase()}; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} { + String foo; + } + ''', + ); + + expect(propsAndStateClassNamesConvertedToNewBoilerplate, { + 'Foo${typeStr}Mixin': 'Foo${typeStr}Mixin', + }); + }); + + test('along with other interfaces (Ui$typeStr first)', () { + testSuggestor( + expectedPatchCount: 6, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Ui${typeStr}, Bar${typeStr}Mixin, Baz${typeStr}Mixin { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + @override + Map get ${typeStr.toLowerCase()}; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} implements Bar${typeStr}Mixin, Baz${typeStr}Mixin { + String foo; + } + ''', + ); + + expect(propsAndStateClassNamesConvertedToNewBoilerplate, { + 'Foo${typeStr}Mixin': 'Foo${typeStr}Mixin', + }); + }); + + test('along with other interfaces (Ui$typeStr last)', () { + testSuggestor( + expectedPatchCount: 6, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Bar${typeStr}Mixin, Baz${typeStr}Mixin, Ui${typeStr} { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + @override + Map get ${typeStr.toLowerCase()}; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} implements Bar${typeStr}Mixin, Baz${typeStr}Mixin { + String foo; + } + ''', + ); + + expect(propsAndStateClassNamesConvertedToNewBoilerplate, { + 'Foo${typeStr}Mixin': 'Foo${typeStr}Mixin', + }); + }); + }); + + group('when the class does not implement Ui$typeStr', () { + test('but it does implement other interface(s)', () { + testSuggestor( + expectedPatchCount: 6, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Bar${typeStr}Mixin, Baz${typeStr}Mixin { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + @override + Map get ${typeStr.toLowerCase()}; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} implements Bar${typeStr}Mixin, Baz${typeStr}Mixin { + String foo; + } + ''', + ); + + expect(propsAndStateClassNamesConvertedToNewBoilerplate, { + 'Foo${typeStr}Mixin': 'Foo${typeStr}Mixin', + }); + }); + + test('or any other interface', () { + testSuggestor( + expectedPatchCount: 6, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + Map get ${typeStr.toLowerCase()}; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} { + String foo; + } + ''', + ); + + expect(propsAndStateClassNamesConvertedToNewBoilerplate, { + 'Foo${typeStr}Mixin': 'Foo${typeStr}Mixin', + }); + }); + }); + }); + + group('meta field', () { + group('is removed if the class is not part of the public API', () { + test('and the meta field is the first field in the class', () { + testSuggestor( + expectedPatchCount: 5, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Ui${typeStr} { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} { + String foo; + } + ''', + ); + }); + + test('and the meta field is not the first field in the class', () { + testSuggestor( + expectedPatchCount: 5, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Ui${typeStr} { + // foooooo + final baz = 'bar'; + + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} { + // foooooo + final baz = 'bar'; + + String foo; + } + ''', + ); + }); + + test( + 'and the meta field is the first field in the class, but not the first member', + () { + testSuggestor( + expectedPatchCount: 5, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Ui${typeStr} { + // foooooo + baz() => 'bar'; + + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} { + // foooooo + baz() => 'bar'; + + String foo; + } + ''', + ); + }); + }); + + test('is deprecated if the class is part of the public API', () { + addTearDown(() { + isPublicForTest = false; + }); + isPublicForTest = true; + + testSuggestor( + expectedPatchCount: 5, + input: ''' + /// Some doc comment + @${typeStr}Mixin() + abstract class Foo${typeStr}Mixin implements Ui${typeStr} { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + String foo; + } + ''', + expectedOutput: ''' + /// Some doc comment + mixin Foo${typeStr}Mixin on Ui${typeStr} { + // To ensure the codemod regression checking works properly, please keep this + // field at the top of the class! + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + @Deprecated('Use `propsMeta.forMixin(Foo${typeStr}Mixin)` instead.') + static const ${typeStr}Meta meta = _\$metaForFoo${typeStr}Mixin; + + String foo; + } + ''', + ); + }); + }); + } + + group( + 'performs a migration when there is a `@PropsMixin()` annotation present:', + () { + sharedTests(MixinType.props); + }); + + group( + 'performs a migration when there is a `@StateMixin()` annotation present:', + () { + sharedTests(MixinType.state); + }); + }); +} + +enum MixinType { props, state } + +const mixinStrByType = { + MixinType.props: 'Props', + MixinType.state: 'State', +}; From d961294adc9a5a77fff10eb5b904f9cc81d9572f Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:24:44 -0700 Subject: [PATCH 02/11] Use joinByName extension method + Originally implemented via #76 --- Dockerfile | 2 +- .../boilerplate_utilities.dart | 23 ++++++++++++++----- ...simple_props_and_state_class_migrator.dart | 1 - lib/src/util.dart | 11 --------- pubspec.yaml | 4 ++-- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 49ded276..0e28cbc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM google/dart:2.4 as build +FROM drydock-prod.workiva.net/workiva/dart2_base_image:1 as build # Build Environment Vars ARG BUILD_ID diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 83c3f276..a18baaa0 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -202,11 +202,9 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, interface.toSource() == 'UiProps' || interface.toSource() == 'UiState'); final otherInterfaces = List.of(nodeInterfaces)..remove(uiInterface); - final otherInterfacesStr = - commaSeparatedAstNodeNames(otherInterfaces); yieldPatch(node.implementsClause.offset, node.implementsClause.end, - 'on ${uiInterface.toSource()} implements $otherInterfacesStr'); + 'on ${uiInterface.toSource()} implements ${otherInterfaces.joinByName()}'); } } else { // Does not implement UiProps / UiState @@ -214,10 +212,8 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, if (nodeInterfaces.isNotEmpty) { // But does implement other stuff - final otherInterfacesStr = commaSeparatedAstNodeNames(nodeInterfaces); - yieldPatch(node.implementsClause.offset, node.implementsClause.end, - 'on $uiInterfaceStr implements $otherInterfacesStr'); + 'on $uiInterfaceStr implements ${nodeInterfaces.joinByName()}'); } else { // Does not implement anything yieldPatch(node.leftBracket.offset - 1, node.leftBracket.offset - 1, @@ -236,3 +232,18 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, propsAndStateClassNamesConvertedToNewBoilerplate[originalPublicClassName] = newMixinName; } + +extension IterableAstUtils on Iterable { + /// Utility to join an `Iterable` based on the `name` of the `name` field + /// rather than the `toString()` of the object. + String joinByName( + {String startingString, String endingString, String seperator}) { + final itemString = map((t) => t.name.name).join('${seperator ?? ','} '); + final returnString = StringBuffer() + ..write(startingString != null ? '${startingString.trimRight()} ' : '') + ..write(itemString) + ..write(endingString != null ? '${endingString.trimLeft()}' : ''); + + return returnString.toString(); + } +} diff --git a/lib/src/boilerplate_suggestors/simple_props_and_state_class_migrator.dart b/lib/src/boilerplate_suggestors/simple_props_and_state_class_migrator.dart index afbdb787..c6fd453c 100644 --- a/lib/src/boilerplate_suggestors/simple_props_and_state_class_migrator.dart +++ b/lib/src/boilerplate_suggestors/simple_props_and_state_class_migrator.dart @@ -16,7 +16,6 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:codemod/codemod.dart'; -import '../util.dart'; import 'boilerplate_utilities.dart'; /// Suggestor that updates props and state classes to new boilerplate. diff --git a/lib/src/util.dart b/lib/src/util.dart index 1912c31e..927fe689 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -446,14 +446,3 @@ String stripPrivateGeneratedPrefix(String value) { ? value.substring(privateGeneratedPrefix.length) : value; } - -/// Takes an iterable of AstNodes and returns their name in a comma separated string. -/// -/// Optionally, you can pass a function to [getName] if you want something other -/// than [AstNode.toSource] to be used to generate the name for the list. -String commaSeparatedAstNodeNames(Iterable nodeList, - {String Function(T node) getName}) { - getName ??= (node) => node.toSource(); - - return nodeList.map(getName).toString().replaceAll(RegExp(r'[\(\)]'), ''); -} diff --git a/pubspec.yaml b/pubspec.yaml index 108f090d..0b914150 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ authors: - Sydney Jodon environment: - sdk: ">=2.4.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: analyzer: '>=0.37.0 <0.39.0' @@ -33,7 +33,7 @@ dependencies: dev_dependencies: dart_dev: ^2.0.1 - dart_style: ^1.2.0 + dart_style: ^1.3.3 dependency_validator: ^1.2.3 mockito: ^4.0.0 pedantic: ^1.4.0 From e908e7a56ab95e79ce259b1d47514e4b96064e84 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:37:29 -0700 Subject: [PATCH 03/11] Use metadata instead of sortedCommentAndAnnotations --- .../boilerplate_utilities.dart | 26 +++++++++---------- .../props_mixins_migrator.dart | 4 +-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index a18baaa0..be9f200f 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -26,32 +26,30 @@ bool isPublicForTest = false; // Stub while is in progress bool isPublic(ClassDeclaration node) => isPublicForTest; +/// Returns the annotation node associated with the provided [classNode] +/// that matches the provided [annotationName], if one exists. +AstNode getAnnotationNode(ClassDeclaration classNode, String annotationName) { + if (classNode.metadata.isEmpty) return null; + + return classNode.metadata.singleWhere( + (node) => node.name.name == annotationName, + orElse: () => null); +} + /// A simple evaluation of the annotation(s) of the [classNode] /// to verify it is either a `@PropsMixin()` or `@StateMixin`. bool isAPropsOrStateMixin(ClassDeclaration classNode) => isAPropsMixin(classNode) || isAStateMixin(classNode); -/// Returns the node of a `@PropsMixin()` annotation for the provided [classNode], if one exists. -AstNode getPropsMixinAnnotationNode(ClassDeclaration classNode) => - classNode.sortedCommentAndAnnotations.singleWhere( - (node) => node?.toSource()?.startsWith('@PropsMixin') == true, - orElse: () => null); - /// A simple evaluation of the annotation(s) of the [classNode] /// to verify it is a `@PropsMixin()`. bool isAPropsMixin(ClassDeclaration classNode) => - getPropsMixinAnnotationNode(classNode) != null; - -/// Returns the node of a `@PropsMixin()` annotation for the provided [classNode], if one exists. -AstNode getStateMixinAnnotationNode(ClassDeclaration classNode) => - classNode.sortedCommentAndAnnotations.singleWhere( - (node) => node?.toSource()?.startsWith('@StateMixin') == true, - orElse: () => null); + getAnnotationNode(classNode, 'PropsMixin') != null; /// A simple evaluation of the annotation(s) of the [classNode] /// to verify it is a `@StateMixin()`. bool isAStateMixin(ClassDeclaration classNode) => - getStateMixinAnnotationNode(classNode) != null; + getAnnotationNode(classNode, 'StateMixin') != null; /// Whether a props or state mixin class [classNode] should be migrated as part of the boilerplate codemod. bool shouldMigratePropsAndStateMixin(ClassDeclaration classNode) => diff --git a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart index 1055deb0..f315aac0 100644 --- a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart +++ b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart @@ -49,13 +49,13 @@ class PropsMixinMigrator extends GeneralizingAstVisitor } void _removePropsOrStateMixinAnnotation(ClassDeclaration node) { - final propsMixinAnnotationNode = getPropsMixinAnnotationNode(node); + final propsMixinAnnotationNode = getAnnotationNode(node, 'PropsMixin'); if (propsMixinAnnotationNode != null) { yieldPatch(propsMixinAnnotationNode.offset, propsMixinAnnotationNode.end + 1, ''); } - final stateMixinAnnotationNode = getStateMixinAnnotationNode(node); + final stateMixinAnnotationNode = getAnnotationNode(node, 'StateMixin'); if (stateMixinAnnotationNode != null) { yieldPatch(stateMixinAnnotationNode.offset, stateMixinAnnotationNode.end + 1, ''); From 86e808a04643f6c20c5a0caa50cb07a29392e28f Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:44:10 -0700 Subject: [PATCH 04/11] Use metadata to confirm a props or state class + Instead of matching the words in the class name itself --- .../boilerplate_utilities.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index be9f200f..380e3ddd 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -78,11 +78,20 @@ bool implementsUiPropsOrUiState(ClassDeclaration classNode) { .any((typeStr) => typeStr.contains(RegExp('(UiProps)|(UiState)'))); } -/// A simple RegExp against the name of the class to verify it contains `props` -/// or `state`. -bool isAPropsOrStateClass(ClassDeclaration classNode) => classNode.name - .toSource() - .contains(RegExp('([A-Za-z]+Props)|([A-Za-z]+State)')); +/// A simple evaluation of the annotation(s) of the [classNode] +/// to verify it is either `@Props()` or `@State()` annotated class. +bool isAPropsOrStateClass(ClassDeclaration classNode) => + isAPropsClass(classNode) || isAStateClass(classNode); + +/// A simple evaluation of the annotation(s) of the [classNode] +/// to verify it is a `@Props()` annotated class. +bool isAPropsClass(ClassDeclaration classNode) => + getAnnotationNode(classNode, 'Props') != null; + +/// A simple evaluation of the annotation(s) of the [classNode] +/// to verify it is a `@State()` annotated class. +bool isAStateClass(ClassDeclaration classNode) => + getAnnotationNode(classNode, 'State') != null; /// Detects if the Props or State class is considered simple. /// From 6e66c423262d19a6f615a32e8aa204ee4a3bf01b Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:44:35 -0700 Subject: [PATCH 05/11] Avoid the use of toSource() --- .../boilerplate_utilities.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 380e3ddd..a44a1448 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -66,15 +66,14 @@ bool shouldMigratePropsAndStateClass(ClassDeclaration node) { /// A simple RegExp against the parent of the class to verify it is `UiProps` /// or `UiState`. bool extendsFromUiPropsOrUiState(ClassDeclaration classNode) => - classNode.extendsClause.superclass.name - .toSource() + classNode.extendsClause.superclass.name.name .contains(RegExp('(UiProps)|(UiState)')); /// A simple RegExp against the parent of the class to verify it is `UiProps` /// or `UiState`. bool implementsUiPropsOrUiState(ClassDeclaration classNode) { return classNode.implementsClause.interfaces - .map((typeName) => typeName.toSource()) + .map((typeName) => typeName.name.name) .any((typeStr) => typeStr.contains(RegExp('(UiProps)|(UiState)'))); } @@ -102,7 +101,7 @@ bool isSimplePropsOrStateClass(ClassDeclaration classNode) { // Only validate props or state classes assert(isAPropsOrStateClass(classNode)); - final superClass = classNode.extendsClause.superclass.name.toSource(); + final superClass = classNode.extendsClause.superclass.name.name; if (superClass != 'UiProps' && superClass != 'UiState') return false; if (classNode.withClause != null) return false; @@ -175,8 +174,7 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, yieldPatch(node.classKeyword.offset, node.classKeyword.charEnd, 'mixin'); - final originalPublicClassName = - stripPrivateGeneratedPrefix(node.name.toSource()); + final originalPublicClassName = stripPrivateGeneratedPrefix(node.name.name); String newMixinName = originalPublicClassName; if (node.extendsClause?.extendsKeyword != null) { @@ -206,12 +204,12 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, } else { // Implements UiProps / UiState along with other interfaces final uiInterface = nodeInterfaces.singleWhere((interface) => - interface.toSource() == 'UiProps' || - interface.toSource() == 'UiState'); + interface.name.name == 'UiProps' || + interface.name.name == 'UiState'); final otherInterfaces = List.of(nodeInterfaces)..remove(uiInterface); yieldPatch(node.implementsClause.offset, node.implementsClause.end, - 'on ${uiInterface.toSource()} implements ${otherInterfaces.joinByName()}'); + 'on ${uiInterface.name.name} implements ${otherInterfaces.joinByName()}'); } } else { // Does not implement UiProps / UiState From 07cfc70aa3b57b93714c6e08324320a617fc1da4 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:47:31 -0700 Subject: [PATCH 06/11] Harden parsing logic --- lib/src/boilerplate_suggestors/boilerplate_utilities.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index a44a1448..eb035730 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -74,7 +74,7 @@ bool extendsFromUiPropsOrUiState(ClassDeclaration classNode) => bool implementsUiPropsOrUiState(ClassDeclaration classNode) { return classNode.implementsClause.interfaces .map((typeName) => typeName.name.name) - .any((typeStr) => typeStr.contains(RegExp('(UiProps)|(UiState)'))); + .any({'UiProps', 'UiState'}.contains); } /// A simple evaluation of the annotation(s) of the [classNode] From d9096d3c67e304d542a1134365425a225ea4874a Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:51:34 -0700 Subject: [PATCH 07/11] Remove and/or explain magic numbers --- .../props_mixins_migrator.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart index f315aac0..8f475df7 100644 --- a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart +++ b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart @@ -44,21 +44,31 @@ class PropsMixinMigrator extends GeneralizingAstVisitor (getter) => getter.name.name == 'props' || getter.name.name == 'state', orElse: () => null); if (propsOrStateGetter != null) { - yieldPatch(propsOrStateGetter.offset, propsOrStateGetter.end + 1, ''); + yieldPatch(propsOrStateGetter.offset, propsOrStateGetter.end, ''); } } void _removePropsOrStateMixinAnnotation(ClassDeclaration node) { final propsMixinAnnotationNode = getAnnotationNode(node, 'PropsMixin'); if (propsMixinAnnotationNode != null) { - yieldPatch(propsMixinAnnotationNode.offset, - propsMixinAnnotationNode.end + 1, ''); + yieldPatch( + propsMixinAnnotationNode.offset, + // +1 to ensure that any comments that were on the line immediately before the annotation + // we are removing - end up on the line immediately before the mixin declaration instead + // of having a newline separating them. + propsMixinAnnotationNode.end + 1, + ''); } final stateMixinAnnotationNode = getAnnotationNode(node, 'StateMixin'); if (stateMixinAnnotationNode != null) { - yieldPatch(stateMixinAnnotationNode.offset, - stateMixinAnnotationNode.end + 1, ''); + yieldPatch( + stateMixinAnnotationNode.offset, + // +1 to ensure that any comments that were on the line immediately before the annotation + // we are removing - end up on the line immediately before the mixin declaration instead + // of having a newline separating them. + stateMixinAnnotationNode.end + 1, + ''); } } @@ -87,7 +97,7 @@ class PropsMixinMigrator extends GeneralizingAstVisitor ? previousMember.end + 1 : node.leftBracket.offset + 1; - yieldPatch(begin, metaField.end + 1, ''); + yieldPatch(begin, metaFieldDecl.end, ''); } } } From adf2600b03077c0ca73b93a67d24cf29637d9559 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 08:55:18 -0700 Subject: [PATCH 08/11] Use .precedingComments --- lib/src/boilerplate_suggestors/props_mixins_migrator.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart index 8f475df7..f24a117e 100644 --- a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart +++ b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart @@ -90,12 +90,8 @@ class PropsMixinMigrator extends GeneralizingAstVisitor } else { // Remove the meta field, along with any comment lines that preceded it. final metaFieldDecl = metaField.parent; - final previousMember = metaFieldDecl == classMembers.first - ? null - : classMembers[classMembers.indexOf(metaFieldDecl) - 1]; - final begin = previousMember != null - ? previousMember.end + 1 - : node.leftBracket.offset + 1; + final begin = metaFieldDecl.beginToken.precedingComments?.offset ?? + metaField.offset; yieldPatch(begin, metaFieldDecl.end, ''); } From 47c753e5396ad3e05e6ef8e6a4ff710c90cdbc07 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 5 Feb 2020 09:27:58 -0700 Subject: [PATCH 09/11] Include abstract annotations --- lib/src/boilerplate_suggestors/boilerplate_utilities.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index eb035730..4b391b36 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -85,12 +85,14 @@ bool isAPropsOrStateClass(ClassDeclaration classNode) => /// A simple evaluation of the annotation(s) of the [classNode] /// to verify it is a `@Props()` annotated class. bool isAPropsClass(ClassDeclaration classNode) => - getAnnotationNode(classNode, 'Props') != null; + getAnnotationNode(classNode, 'Props') != null || + getAnnotationNode(classNode, 'AbstractProps') != null; /// A simple evaluation of the annotation(s) of the [classNode] /// to verify it is a `@State()` annotated class. bool isAStateClass(ClassDeclaration classNode) => - getAnnotationNode(classNode, 'State') != null; + getAnnotationNode(classNode, 'State') != null || + getAnnotationNode(classNode, 'AbstractState') != null; /// Detects if the Props or State class is considered simple. /// From 4a30dbf7c15376ed85a695395848bc659e6a5d58 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 6 Feb 2020 08:40:17 -0700 Subject: [PATCH 10/11] Remove dead code --- lib/src/boilerplate_suggestors/boilerplate_utilities.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 0378b6b7..6f0085d4 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -224,10 +224,6 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, // But does implement other stuff yieldPatch(node.implementsClause.offset, node.implementsClause.end, 'on $uiInterfaceStr implements ${nodeInterfaces.joinByName()}'); - } else { - // Does not implement anything - yieldPatch(node.leftBracket.offset - 1, node.leftBracket.offset - 1, - 'on $uiInterfaceStr'); } } } else { From 260e55c09288f30318ea29fe4df3487a281fdcaa Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 6 Feb 2020 08:46:37 -0700 Subject: [PATCH 11/11] Address CR feedback --- .../boilerplate_utilities.dart | 15 +++++++------ .../props_mixins_migrator.dart | 22 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 6f0085d4..52feea35 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -29,9 +29,7 @@ bool isPublic(ClassDeclaration node) => isPublicForTest; /// Returns the annotation node associated with the provided [classNode] /// that matches the provided [annotationName], if one exists. AstNode getAnnotationNode(ClassDeclaration classNode, String annotationName) { - if (classNode.metadata.isEmpty) return null; - - return classNode.metadata.singleWhere( + return classNode.metadata.firstWhere( (node) => node.name.name == annotationName, orElse: () => null); } @@ -65,9 +63,12 @@ bool shouldMigratePropsAndStateClass(ClassDeclaration node) { /// A simple RegExp against the parent of the class to verify it is `UiProps` /// or `UiState`. -bool extendsFromUiPropsOrUiState(ClassDeclaration classNode) => - classNode.extendsClause.superclass.name.name - .contains(RegExp('(UiProps)|(UiState)')); +bool extendsFromUiPropsOrUiState(ClassDeclaration classNode) { + return { + 'UiProps', + 'UiState', + }.contains(classNode.extendsClause.superclass.name.name); +} /// A simple RegExp against the parent of the class to verify it is `UiProps` /// or `UiState`. @@ -208,7 +209,7 @@ void migrateClassToMixin(ClassDeclaration node, YieldPatch yieldPatch, node.implementsClause.implementsKeyword.charEnd, 'on'); } else { // Implements UiProps / UiState along with other interfaces - final uiInterface = nodeInterfaces.singleWhere((interface) => + final uiInterface = nodeInterfaces.firstWhere((interface) => interface.name.name == 'UiProps' || interface.name.name == 'UiState'); final otherInterfaces = List.of(nodeInterfaces)..remove(uiInterface); diff --git a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart index f24a117e..414738a0 100644 --- a/lib/src/boilerplate_suggestors/props_mixins_migrator.dart +++ b/lib/src/boilerplate_suggestors/props_mixins_migrator.dart @@ -40,7 +40,7 @@ class PropsMixinMigrator extends GeneralizingAstVisitor final classGetters = node.members .whereType() .where((method) => method.isGetter); - final propsOrStateGetter = classGetters.singleWhere( + final propsOrStateGetter = classGetters.firstWhere( (getter) => getter.name.name == 'props' || getter.name.name == 'state', orElse: () => null); if (propsOrStateGetter != null) { @@ -53,10 +53,10 @@ class PropsMixinMigrator extends GeneralizingAstVisitor if (propsMixinAnnotationNode != null) { yieldPatch( propsMixinAnnotationNode.offset, - // +1 to ensure that any comments that were on the line immediately before the annotation - // we are removing - end up on the line immediately before the mixin declaration instead - // of having a newline separating them. - propsMixinAnnotationNode.end + 1, + // Use the offset of the next token to ensure that any comments that were on the line + // immediately before the annotation we are removing - end up on the line immediately + // before the mixin declaration instead of having a newline separating them. + propsMixinAnnotationNode.endToken.next.offset, ''); } @@ -64,10 +64,10 @@ class PropsMixinMigrator extends GeneralizingAstVisitor if (stateMixinAnnotationNode != null) { yieldPatch( stateMixinAnnotationNode.offset, - // +1 to ensure that any comments that were on the line immediately before the annotation - // we are removing - end up on the line immediately before the mixin declaration instead - // of having a newline separating them. - stateMixinAnnotationNode.end + 1, + // Use the offset of the next token to ensure that any comments that were on the line + // immediately before the annotation we are removing - end up on the line immediately + // before the mixin declaration instead of having a newline separating them. + stateMixinAnnotationNode.endToken.next.offset, ''); } } @@ -79,8 +79,8 @@ class PropsMixinMigrator extends GeneralizingAstVisitor .whereType() .map((decl) => decl.fields) .toList(); - final metaField = classFields.singleWhere( - (field) => field.variables.single.name.name == 'meta', + final metaField = classFields.firstWhere( + (field) => field.variables.first.name.name == 'meta', orElse: () => null); if (metaField == null) return;