diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 96ef8e7c..e4145244 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -20,11 +20,37 @@ import 'package:over_react_codemod/src/util.dart'; typedef YieldPatch = void Function( int startingOffset, int endingOffset, String replacement); -@visibleForTesting -bool isPublicForTest = false; +SemverHelper semverHelper; -// Stub while is in progress -bool isPublic(ClassDeclaration node) => isPublicForTest; +/// Returns whether or not [node] is publicly exported. +bool isPublic(ClassDeclaration node) { + assert(semverHelper != null); + return semverHelper.getPublicExportLocations(node).isNotEmpty; +} + +class SemverHelper { + final Map _exportList; + + SemverHelper(Map jsonReport) + : _exportList = jsonReport['exports'], + assert(jsonReport['exports'] != null); + + /// Returns a list of locations where [node] is publicly exported. + /// + /// If [node] is not publicly exported, returns an empty list. + List getPublicExportLocations(ClassDeclaration node) { + final className = node.name.name; + final List locations = List(); + + _exportList.forEach((key, value) { + if (value['type'] == 'class' && value['grammar']['name'] == className) { + locations.add(key); + } + }); + + return locations; + } +} /// Returns the annotation node associated with the provided [classNode] /// that matches the provided [annotationName], if one exists. @@ -57,7 +83,6 @@ bool shouldMigratePropsAndStateMixin(ClassDeclaration classNode) => bool shouldMigratePropsAndStateClass(ClassDeclaration node) { return isAssociatedWithComponent2(node) && isAPropsOrStateClass(node) && - // Stub while is in progress !isPublic(node); } diff --git a/lib/src/executables/boilerplate_upgrade.dart b/lib/src/executables/boilerplate_upgrade.dart index 315567d1..49bd6976 100644 --- a/lib/src/executables/boilerplate_upgrade.dart +++ b/lib/src/executables/boilerplate_upgrade.dart @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:convert'; import 'dart:io'; import 'package:codemod/codemod.dart'; @@ -32,7 +33,7 @@ const _changesRequiredOutput = ''' Then, review the the changes, address any FIXMEs, and commit. '''; -void main(List args) { +Future main(List args) async { final query = FileQuery.dir( pathFilter: (path) { return isDartFile(path) && !isGeneratedDartFile(path); @@ -42,6 +43,10 @@ void main(List args) { final classToMixinConverter = ClassToMixinConverter(); + // TODO: determine file path of semver report + semverHelper = SemverHelper(jsonDecode( + await File('lib/src/boilerplate_suggestors/report.json').readAsString())); + // General plan: // - Things that need to be accomplished (very simplified) // 1. Make props / state class a mixin diff --git a/test/boilerplate_suggestors/advanced_props_and_state_class_migrator_test.dart b/test/boilerplate_suggestors/advanced_props_and_state_class_migrator_test.dart index 0e7b80bf..b1873762 100644 --- a/test/boilerplate_suggestors/advanced_props_and_state_class_migrator_test.dart +++ b/test/boilerplate_suggestors/advanced_props_and_state_class_migrator_test.dart @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:convert'; + import 'package:over_react_codemod/src/boilerplate_suggestors/advanced_props_and_state_class_migrator.dart'; import 'package:over_react_codemod/src/boilerplate_suggestors/boilerplate_utilities.dart'; import 'package:test/test.dart'; import '../util.dart'; +import 'boilerplate_utilities_test.dart'; void main() { group('AdvancedPropsAndStateClassMigrator', () { @@ -24,6 +27,10 @@ void main() { final testSuggestor = getSuggestorTester(AdvancedPropsAndStateClassMigrator(converter)); + setUpAll(() { + semverHelper = SemverHelper(jsonDecode(reportJson)); + }); + tearDown(() { converter.setConvertedClassNames({}); }); @@ -65,6 +72,43 @@ void main() { expect(converter.convertedClassNames, isEmpty); }); + + test('advanced classes are public API', () { + testSuggestor( + expectedPatchCount: 0, + input: r''' + @Factory() + UiFactory Bar = + // ignore: undefined_identifier + $Bar; + + @Props() + class BarProps extends ADifferentPropsClass { + String foo; + int bar; + } + + @State() + class BarState extends ADifferentStateClass { + String foo; + int bar; + } + + @Component2() + class BarComponent extends UiStatefulComponent2 { + @override + render() { + return Dom.ul()( + Dom.li()('Foo: ', props.foo), + Dom.li()('Bar: ', props.bar), + ); + } + } + ''', + ); + + expect(converter.convertedClassNames, isEmpty); + }); }); group('operates when the classes are advanced', () { diff --git a/test/boilerplate_suggestors/boilerplate_utilities_test.dart b/test/boilerplate_suggestors/boilerplate_utilities_test.dart index 4c2281ce..39fa00ec 100644 --- a/test/boilerplate_suggestors/boilerplate_utilities_test.dart +++ b/test/boilerplate_suggestors/boilerplate_utilities_test.dart @@ -12,11 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:convert'; + import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:over_react_codemod/src/boilerplate_suggestors/boilerplate_utilities.dart'; import 'package:test/test.dart'; +const reportJson = r'''{ + "exports": { + "lib/web_skin_dart.dart/ButtonProps": { + "type": "class", + "grammar": { + "name": "ButtonProps", + "meta": ["@Props()"] + } + }, + "lib/web_skin_dart.dart/BarProps": { + "type": "class", + "grammar": { + "name": "BarProps", + "meta": ["@Props()"] + } + }, + "lib/web_skin_dart.dart/BarState": { + "type": "class", + "grammar": { + "name": "BarState", + "meta": ["@State()"] + } + }, + "lib/web_skin_dart.dart/BarPropsMixin": { + "type": "class", + "grammar": { + "name": "BarPropsMixin", + "meta": ["@Props()"] + } + }, + "lib/web_skin_dart.dart/BarStateMixin": { + "type": "class", + "grammar": { + "name": "BarStateMixin", + "meta": ["@State()"] + } + }, + "lib/another_file.dart/ButtonProps": { + "type": "class", + "grammar": { + "name": "ButtonProps", + "meta": ["@Props()"] + } + } + } +}'''; + void main() { group('Boilerplate Utilities', () { group('isPropsUsageSimple', () { @@ -267,5 +316,50 @@ void main() { }); }); }); + + group('isPublic() and getPublicExportLocations()', () { + setUpAll(() { + semverHelper = SemverHelper(jsonDecode(reportJson)); + }); + + test('if props class is not in export list', () { + final input = ''' + @Props() + class _\$FooProps extends UiProps{ + String foo; + int bar; + } + '''; + + CompilationUnit unit = parseString(content: input).unit; + expect(unit.declarations.whereType().length, 1); + + unit.declarations.whereType().forEach((classNode) { + expect(semverHelper.getPublicExportLocations(classNode), isEmpty); + expect(isPublic(classNode), false); + }); + }); + + test('if props class is in export list', () { + final input = ''' + @Props() + class ButtonProps extends UiProps{ + String foo; + int bar; + } + '''; + + CompilationUnit unit = parseString(content: input).unit; + expect(unit.declarations.whereType().length, 1); + + unit.declarations.whereType().forEach((classNode) { + expect(semverHelper.getPublicExportLocations(classNode), [ + 'lib/web_skin_dart.dart/ButtonProps', + 'lib/another_file.dart/ButtonProps' + ]); + expect(isPublic(classNode), true); + }); + }); + }); }); } diff --git a/test/boilerplate_suggestors/props_mixin_migrator_test.dart b/test/boilerplate_suggestors/props_mixin_migrator_test.dart index de9a4aa4..ed64be81 100644 --- a/test/boilerplate_suggestors/props_mixin_migrator_test.dart +++ b/test/boilerplate_suggestors/props_mixin_migrator_test.dart @@ -12,17 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:convert'; + 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'; +import 'boilerplate_utilities_test.dart'; main() { group('PropsMixinMigrator', () { final converter = ClassToMixinConverter(); final testSuggestor = getSuggestorTester(PropsMixinMigrator(converter)); + setUpAll(() { + semverHelper = SemverHelper(jsonDecode(reportJson)); + }); + tearDown(() { converter.setConvertedClassNames({}); }); @@ -297,33 +304,28 @@ main() { }); 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} { + abstract class Bar${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; + static const ${typeStr}Meta meta = _\$metaForBar${typeStr}Mixin; String foo; } ''', expectedOutput: ''' /// Some doc comment - mixin Foo${typeStr}Mixin on Ui${typeStr} { + mixin Bar${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; + @Deprecated('Use `propsMeta.forMixin(Bar${typeStr}Mixin)` instead.') + static const ${typeStr}Meta meta = _\$metaForBar${typeStr}Mixin; String foo; } diff --git a/test/boilerplate_suggestors/simple_props_and_state_class_migrator_test.dart b/test/boilerplate_suggestors/simple_props_and_state_class_migrator_test.dart index 38960ec2..14417acc 100644 --- a/test/boilerplate_suggestors/simple_props_and_state_class_migrator_test.dart +++ b/test/boilerplate_suggestors/simple_props_and_state_class_migrator_test.dart @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:convert'; + import 'package:over_react_codemod/src/boilerplate_suggestors/boilerplate_utilities.dart'; import 'package:over_react_codemod/src/boilerplate_suggestors/simple_props_and_state_class_migrator.dart'; import 'package:test/test.dart'; import '../util.dart'; +import 'boilerplate_utilities_test.dart'; main() { group('SimplePropsAndStateClassMigrator', () { @@ -24,6 +27,10 @@ main() { final testSuggestor = getSuggestorTester(SimplePropsAndStateClassMigrator(converter)); + setUpAll(() { + semverHelper = SemverHelper(jsonDecode(reportJson)); + }); + tearDown(() { converter.setConvertedClassNames({}); }); @@ -116,7 +123,36 @@ main() { expect(converter.convertedClassNames, isEmpty); }); - // TODO add a test for when the class is publicly exported + test('when the props class is publicly exported', () { + testSuggestor( + expectedPatchCount: 0, + input: ''' + @Factory() + UiFactory Bar = + // ignore: undefined_identifier + \$Bar; + + @Props() + class BarProps extends UiProps { + String foo; + int bar; + } + + @Component2() + class BarComponent extends UiComponent2 { + @override + render() { + return Dom.ul()( + Dom.li()('Foo: ', props.foo), + Dom.li()('Bar: ', props.bar), + ); + } + } + ''', + ); + + expect(converter.convertedClassNames, isEmpty); + }); group('the classes are not simple', () { test('and there are both a props and a state class', () { diff --git a/test/boilerplate_suggestors/stubbed_props_and_state_class_remover_test.dart b/test/boilerplate_suggestors/stubbed_props_and_state_class_remover_test.dart index 73a8e9cb..8ef7e0cd 100644 --- a/test/boilerplate_suggestors/stubbed_props_and_state_class_remover_test.dart +++ b/test/boilerplate_suggestors/stubbed_props_and_state_class_remover_test.dart @@ -12,10 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:convert'; + +import 'package:over_react_codemod/src/boilerplate_suggestors/boilerplate_utilities.dart'; import 'package:over_react_codemod/src/boilerplate_suggestors/stubbed_props_and_state_class_remover.dart'; import 'package:test/test.dart'; import '../util.dart'; +import 'boilerplate_utilities_test.dart'; main() { group('StubbedPropsAndStateClassRemover', () { @@ -23,6 +27,10 @@ main() { StubbedPropsAndStateClassRemover(), ); + setUpAll(() { + semverHelper = SemverHelper(jsonDecode(reportJson)); + }); + group('does not perform a migration', () { test('when it encounters an empty file', () { testSuggestor(expectedPatchCount: 0, input: ''); @@ -84,7 +92,39 @@ main() { }); test('when the stubbed "companion" class(es) are publicly exported', () { - // TODO add a test for when the class is publicly exported + testSuggestor( + expectedPatchCount: 0, + input: ''' + @Factory() + UiFactory Bar = + // ignore: undefined_identifier + \$Bar; + + @Props() + class _\$_BarProps extends UiProps { + String foo; + int bar; + } + + @Component2() + class BarComponent extends UiComponent2 { + @override + render() { + return Dom.ul()( + Dom.li()('Foo: ', props.foo), + Dom.li()('Bar: ', props.bar), + ); + } + } + + // AF-3369 This will be removed once the transition to Dart 2 is complete. + // ignore: mixin_of_non_class, undefined_class + class BarProps extends _\$_BarProps with _\$_BarPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _\$metaFor_BarProps; + } + ''', + ); }); });