Skip to content

Commit

Permalink
Add suggestor that replaces useRef with useRefInit
Browse files Browse the repository at this point in the history
when an argument is passed
  • Loading branch information
aaronlademann-wf committed Feb 29, 2024
1 parent 9912a6f commit 6ff4783
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 0 deletions.
15 changes: 15 additions & 0 deletions bin/useRefInit_migration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 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.

export 'package:over_react_codemod/src/executables/useRefInit_migration.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 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:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:over_react_codemod/src/util/class_suggestor.dart';

/// Suggestor that finds instances of `useRef` function invocations that
/// pass an argument, and replaces them with `useRefInit` to prep for
/// null safety.
///
/// Example:
///
/// ```dart
/// // Before
/// final ref = useRef(someNonNulLValue);
/// // After
/// final ref = useRefInit(someNonNulLValue);
/// ```
class UseRefInitMigration extends RecursiveAstVisitor with ClassSuggestor {
ResolvedUnitResult? _result;

@override
visitArgumentList(ArgumentList node) {
super.visitArgumentList(node);

if (node.arguments.isEmpty) return;

dynamic possibleInvocation = node.parent;
if (possibleInvocation is MethodInvocation) {
String fnName = '';
if (possibleInvocation.function is SimpleIdentifier) {
fnName = (possibleInvocation.function as SimpleIdentifier).name;
}

if (fnName == 'useRef') {
yieldPatch('useRefInit',
possibleInvocation.function.offset, possibleInvocation.function.end);
}
}
}

@override
Future<void> generatePatches() async {
_result = await context.getResolvedUnit();
if (_result == null) {
throw Exception(
'Could not get resolved result for "${context.relativePath}"');
}
_result!.unit.accept(this);
}
}
43 changes: 43 additions & 0 deletions lib/src/executables/useRefInit_migration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2024 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 'dart:io';

import 'package:args/args.dart';
import 'package:codemod/codemod.dart';
import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/useRefInit_migration.dart';
import 'package:over_react_codemod/src/ignoreable.dart';
import 'package:over_react_codemod/src/util.dart';

const _changesRequiredOutput = """
To update your code, run the following commands in your repository:
pub global activate over_react_codemod
pub global run over_react_codemod:dom_callback_null_args
""";

void main(List<String> args) async {
final parser = ArgParser.allowAnything();

final parsedArgs = parser.parse(args);
final dartPaths = allDartPathsExceptHiddenAndGenerated();

exitCode = await runInteractiveCodemod(
dartPaths,
ignoreable(UseRefInitMigration()),
defaultYes: true,
args: parsedArgs.rest,
additionalHelpOutput: parser.usage,
changesRequiredOutput: _changesRequiredOutput,
);
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ executables:
intl_message_migration:
sort_intl:
unify_package_rename:
useRefInit_migration:

dependency_validator:
ignore:
Expand Down
105 changes: 105 additions & 0 deletions test/dart3_suggestors/null_safety_prep/useRefInit_migration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2024 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/dart3_suggestors/null_safety_prep/useRefInit_migration.dart';
import 'package:test/test.dart';

import '../../resolved_file_context.dart';
import '../../util.dart';
import '../../util/component_usage_migrator_test.dart';

void main() {
final resolvedContext = SharedAnalysisContext.overReact;

// Warm up analysis in a setUpAll so that if getting the resolved AST times out
// (which is more common for the WSD context), it fails here instead of failing the first test.
setUpAll(resolvedContext.warmUpAnalysis);

group('DomCallbackNullArgs', () {
late SuggestorTester testSuggestor;

setUp(() {
testSuggestor = getSuggestorTester(
UseRefInitMigration(),
resolvedContext: resolvedContext,
);
});

test(
'leaves useRef function invocations alone when the argument list is empty',
() async {
await testSuggestor(
expectedPatchCount: 0,
input: withOverReactImport('''
final Foo = uiFunction<UiProps>(
(props) {
final foo = useRef();
print(foo);
return null;
},
UiFactoryConfig(displayName: 'Foo'),
);
'''),
);
});

test('replaces useRef usages with useRefInit when an argument is passed', () async {
await testSuggestor(
expectedPatchCount: 1,
input: withOverReactImport('''
final Foo = uiFunction<UiProps>(
(props) {
final foo = useRef('bar');
return (Dom.div()..id = foo.current)();
},
UiFactoryConfig(displayName: 'Foo'),
);
'''),
expectedOutput: withOverReactImport('''
final Foo = uiFunction<UiProps>(
(props) {
final foo = useRefInit('bar');
return (Dom.div()..id = foo.current)();
},
UiFactoryConfig(displayName: 'Foo'),
);
'''),
);
});

test('replaces useRef<Generic> usages with useRefInit<Generic> when an argument is passed', () async {
await testSuggestor(
expectedPatchCount: 1,
input: withOverReactImport('''
final Foo = uiFunction<UiProps>(
(props) {
final foo = useRef<String>('bar');
return (Dom.div()..id = foo.current)();
},
UiFactoryConfig(displayName: 'Foo'),
);
'''),
expectedOutput: withOverReactImport('''
final Foo = uiFunction<UiProps>(
(props) {
final foo = useRefInit<String>('bar');
return (Dom.div()..id = foo.current)();
},
UiFactoryConfig(displayName: 'Foo'),
);
'''),
);
});
});
}

0 comments on commit 6ff4783

Please sign in to comment.