Skip to content

Commit

Permalink
Merge pull request #74 from Workiva/CPLAT-9308-getPublicExportLocatio…
Browse files Browse the repository at this point in the history
…ns-utility-function

CPLAT-9308 Codemod Utility to tell if Something is Public API
  • Loading branch information
greglittlefield-wf authored Feb 17, 2020
2 parents bbd95f3 + 8775714 commit 2d47b96
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 18 deletions.
35 changes: 30 additions & 5 deletions lib/src/boilerplate_suggestors/boilerplate_utilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,37 @@ import 'package:source_span/source_span.dart';
typedef YieldPatch = void Function(
int startingOffset, int endingOffset, String replacement);

@visibleForTesting
bool isPublicForTest = false;
SemverHelper semverHelper;

// Stub while <https://jira.atl.workiva.net/browse/CPLAT-9308> 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<String> getPublicExportLocations(ClassDeclaration node) {
final className = node.name.name;
final List<String> 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.
Expand Down Expand Up @@ -59,7 +85,6 @@ bool shouldMigratePropsAndStateMixin(ClassDeclaration classNode) =>
bool shouldMigratePropsAndStateClass(ClassDeclaration node) {
return isAssociatedWithComponent2(node) &&
isAPropsOrStateClass(node) &&
// Stub while <https://jira.atl.workiva.net/browse/CPLAT-9308> is in progress
!isPublic(node);
}

Expand Down
7 changes: 6 additions & 1 deletion lib/src/executables/boilerplate_upgrade.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,7 +33,7 @@ const _changesRequiredOutput = '''
Then, review the the changes, address any FIXMEs, and commit.
''';

void main(List<String> args) {
Future<void> main(List<String> args) async {
final query = FileQuery.dir(
pathFilter: (path) {
return isDartFile(path) && !isGeneratedDartFile(path);
Expand All @@ -42,6 +43,10 @@ void main(List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,25 @@
// 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', () {
final converter = ClassToMixinConverter();
final testSuggestor =
getSuggestorTester(AdvancedPropsAndStateClassMigrator(converter));

setUpAll(() {
semverHelper = SemverHelper(jsonDecode(reportJson));
});

tearDown(() {
converter.setConvertedClassNames({});
});
Expand Down Expand Up @@ -65,6 +72,43 @@ void main() {

expect(converter.convertedClassNames, isEmpty);
});

test('advanced classes are public API', () {
testSuggestor(
expectedPatchCount: 0,
input: r'''
@Factory()
UiFactory<BarProps> 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<BarProps, BarState> {
@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', () {
Expand Down
94 changes: 94 additions & 0 deletions test/boilerplate_suggestors/boilerplate_utilities_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -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<ClassDeclaration>().length, 1);

unit.declarations.whereType<ClassDeclaration>().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<ClassDeclaration>().length, 1);

unit.declarations.whereType<ClassDeclaration>().forEach((classNode) {
expect(semverHelper.getPublicExportLocations(classNode), [
'lib/web_skin_dart.dart/ButtonProps',
'lib/another_file.dart/ButtonProps'
]);
expect(isPublic(classNode), true);
});
});
});
});
}
22 changes: 12 additions & 10 deletions test/boilerplate_suggestors/props_mixin_migrator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
});
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,25 @@
// 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', () {
final converter = ClassToMixinConverter();
final testSuggestor =
getSuggestorTester(SimplePropsAndStateClassMigrator(converter));

setUpAll(() {
semverHelper = SemverHelper(jsonDecode(reportJson));
});

tearDown(() {
converter.setConvertedClassNames({});
});
Expand Down Expand Up @@ -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<BarProps> Bar =
// ignore: undefined_identifier
\$Bar;
@Props()
class BarProps extends UiProps {
String foo;
int bar;
}
@Component2()
class BarComponent extends UiComponent2<BarProps> {
@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', () {
Expand Down
Loading

0 comments on commit 2d47b96

Please sign in to comment.