Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPLAT-9308 Codemod Utility to tell if Something is Public API #74

Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
53d0862
Create SemverHelper class
sydneyjodon-wk Jan 30, 2020
4900548
Create utility function
sydneyjodon-wk Jan 30, 2020
070c5f9
Format
sydneyjodon-wk Jan 30, 2020
723a406
Add SemverHelper constructor
sydneyjodon-wk Feb 4, 2020
cadf6ae
Add doc comment
sydneyjodon-wk Feb 4, 2020
d806385
Add tests
sydneyjodon-wk Feb 4, 2020
c5e7e74
Merge branch 'CPLAT-9205-new-boilerplate-codemod' into CPLAT-9308-get…
sydneyjodon-wk Feb 4, 2020
7027deb
Merge base branch in
sydneyjodon-wk Feb 4, 2020
900a557
Update migrator tests
sydneyjodon-wk Feb 4, 2020
7c1f44e
Remove unused imports
sydneyjodon-wk Feb 4, 2020
457deb6
Merge CPLAT-9205-new-boilerplate-codemod into CPLAT-9308-getPublicExp…
aaronlademann-wf Feb 5, 2020
f9d81d9
Merge remote-tracking branch 'origin/CPLAT-9205-new-boilerplate-codem…
aaronlademann-wf Feb 5, 2020
8abec86
Merge branch 'CPLAT-9205-new-boilerplate-codemod' into CPLAT-9308-get…
sydneyjodon-wk Feb 6, 2020
c61a5c4
Address reviewer feedback
sydneyjodon-wk Feb 6, 2020
d53c197
Remove duplicate import
sydneyjodon-wk Feb 6, 2020
82e39f7
Add doc comment
sydneyjodon-wk Feb 6, 2020
c5430ab
Merge branch 'CPLAT-9205-new-boilerplate-codemod' into CPLAT-9308-get…
sydneyjodon-wk Feb 6, 2020
7d844b0
Fix errors from merge
sydneyjodon-wk Feb 6, 2020
5783dcb
Address reviewer feedback
sydneyjodon-wk Feb 11, 2020
23dc002
Merge branch 'CPLAT-9205-new-boilerplate-codemod' into CPLAT-9308-get…
sydneyjodon-wk Feb 11, 2020
8775714
Address reviewer feedback
sydneyjodon-wk Feb 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions lib/src/boilerplate_suggestors/boilerplate_utilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,40 @@ 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 <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) != null;
}

class SemverHelper {
Map _exportList;

SemverHelper(Map jsonReport) {
_exportList = jsonReport['exports'];
greglittlefield-wf marked this conversation as resolved.
Show resolved Hide resolved
}

/// Returns semver report information for [node] if it is publicly exported.
///
/// If [node] is not publicly exported, returns `null`.
Map<String, dynamic> getPublicExportLocations(ClassDeclaration node) {
greglittlefield-wf marked this conversation as resolved.
Show resolved Hide resolved
assert(_exportList != null);
greglittlefield-wf marked this conversation as resolved.
Show resolved Hide resolved

final className = node.name.name;

for (final key in _exportList.keys) {
final value = _exportList[key];

if (value['type'] == 'class' && value['grammar']['name'] == className) {
return value;
}
}

return null;
}
}

/// Returns the annotation node associated with the provided [classNode]
/// that matches the provided [annotationName], if one exists.
Expand Down Expand Up @@ -57,7 +86,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
91 changes: 91 additions & 0 deletions test/boilerplate_suggestors/boilerplate_utilities_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,5 +267,96 @@ main() {
});
});
});

group('isPublic() and getPublicExportLocations()', () {
test('if props class is not in export list', () {
semverHelper = SemverHelper({
'exports': {
'lib/web_skin_dart.dart/ButtonProps': {
'type': 'class',
'grammar': {
'name': 'ButtonProps',
'meta': ['@Props()']
}
}
}
});

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), null);
expect(isPublic(classNode), false);
});
});
});

test('if props class is in export list', () {
semverHelper = SemverHelper({
"exports": {
"lib/web_skin_dart.dart/ButtonProps": {
"type": "class",
"grammar": {
"name": "ButtonProps",
"meta": ["@Props()"]
}
},
"lib/web_skin_dart.dart/FooProps": {
"type": "class",
"grammar": {
"name": "FooProps",
"meta": ["@Props()"]
}
},
"lib/web_skin_dart.dart/BarProps": {
"type": "class",
"grammar": {
"name": "BarProps",
"meta": ["@Props()"]
}
},
"lib/web_skin_dart.dart/DropdownSelectProps": {
"type": "class",
"grammar": {
"name": "DropdownSelectProps",
"meta": ["@Props()"]
}
}
}
});

final input = '''
@Props()
class DropdownSelectProps extends UiProps{
String foo;
int bar;
}
''';
final expectedResult = {
"type": "class",
"grammar": {
"name": "DropdownSelectProps",
"meta": ["@Props()"]
}
};

CompilationUnit unit = parseString(content: input).unit;
expect(unit.declarations.whereType<ClassDeclaration>().length, 1);

unit.declarations.whereType<ClassDeclaration>().forEach((classNode) {
expect(
semverHelper.getPublicExportLocations(classNode), expectedResult);
expect(isPublic(classNode), true);
});
});
});
}
24 changes: 14 additions & 10 deletions test/boilerplate_suggestors/props_mixin_migrator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:convert';
import 'dart:io';

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';
Expand All @@ -23,6 +26,12 @@ main() {
final converter = ClassToMixinConverter();
final testSuggestor = getSuggestorTester(PropsMixinMigrator(converter));

setUpAll(() async {
semverHelper = SemverHelper(jsonDecode(
await File('test/boilerplate_suggestors/report.json')
.readAsString()));
});

tearDown(() {
converter.setConvertedClassNames({});
});
Expand Down Expand Up @@ -297,33 +306,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
32 changes: 32 additions & 0 deletions test/boilerplate_suggestors/report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"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/BarPropsMixin": {
"type": "class",
"grammar": {
"name": "BarPropsMixin",
"meta": ["@Props()"]
}
},
"lib/web_skin_dart.dart/BarStateMixin": {
"type": "class",
"grammar": {
"name": "BarStateMixin",
"meta": ["@Props()"]
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:convert';
import 'dart:io';

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';
Expand All @@ -24,6 +27,12 @@ main() {
final testSuggestor =
getSuggestorTester(SimplePropsAndStateClassMigrator(converter));

setUpAll(() async {
semverHelper = SemverHelper(jsonDecode(
await File('test/boilerplate_suggestors/report.json')
greglittlefield-wf marked this conversation as resolved.
Show resolved Hide resolved
.readAsString()));
});

tearDown(() {
converter.setConvertedClassNames({});
});
Expand Down Expand Up @@ -116,7 +125,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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:convert';
import 'dart:io';

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';

Expand All @@ -23,6 +27,12 @@ main() {
StubbedPropsAndStateClassRemover(),
);

setUpAll(() async {
semverHelper = SemverHelper(jsonDecode(
await File('test/boilerplate_suggestors/report.json')
.readAsString()));
});

group('does not perform a migration', () {
test('when it encounters an empty file', () {
testSuggestor(expectedPatchCount: 0, input: '');
Expand Down Expand Up @@ -84,7 +94,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<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),
);
}
}

// 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;
}
''',
);
});
});

Expand Down