diff --git a/bin/null_safety_prep.dart b/bin/null_safety_prep.dart new file mode 100644 index 00000000..5e9a24af --- /dev/null +++ b/bin/null_safety_prep.dart @@ -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/null_safety_prep.dart'; diff --git a/lib/src/dart3_suggestors/null_safety_prep/use_ref_init_migration.dart b/lib/src/dart3_suggestors/null_safety_prep/use_ref_init_migration.dart new file mode 100644 index 00000000..df8eb400 --- /dev/null +++ b/lib/src/dart3_suggestors/null_safety_prep/use_ref_init_migration.dart @@ -0,0 +1,52 @@ +// 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/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:codemod/codemod.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 AstVisitingSuggestor { + @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); + } + } + } +} diff --git a/lib/src/executables/null_safety_prep.dart b/lib/src/executables/null_safety_prep.dart new file mode 100644 index 00000000..6321665c --- /dev/null +++ b/lib/src/executables/null_safety_prep.dart @@ -0,0 +1,45 @@ +// 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/use_ref_init_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:null_safety_prep +"""; + +void main(List args) async { + final parser = ArgParser.allowAnything(); + + final parsedArgs = parser.parse(args); + final dartPaths = allDartPathsExceptHiddenAndGenerated(); + + exitCode = await runInteractiveCodemod( + dartPaths, + aggregate([ + UseRefInitMigration(), + ].map((s) => ignoreable(s))), + defaultYes: true, + args: parsedArgs.rest, + additionalHelpOutput: parser.usage, + changesRequiredOutput: _changesRequiredOutput, + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 7bb30756..696c061f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dev_dependencies: executables: dart2_9_upgrade: dependency_validator_ignore: + null_safety_prep: mui_migration: required_flux_props: rmui_preparation: diff --git a/test/dart3_suggestors/null_safety_prep/use_ref_init_migration_test.dart b/test/dart3_suggestors/null_safety_prep/use_ref_init_migration_test.dart new file mode 100644 index 00000000..a6ad5a0c --- /dev/null +++ b/test/dart3_suggestors/null_safety_prep/use_ref_init_migration_test.dart @@ -0,0 +1,108 @@ +// 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/use_ref_init_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('UseRefInitMigration', () { + 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( + (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( + (props) { + final foo = useRef('bar'); + return (Dom.div()..id = foo.current)(); + }, + UiFactoryConfig(displayName: 'Foo'), + ); + '''), + expectedOutput: withOverReactImport(''' + final Foo = uiFunction( + (props) { + final foo = useRefInit('bar'); + return (Dom.div()..id = foo.current)(); + }, + UiFactoryConfig(displayName: 'Foo'), + ); + '''), + ); + }); + + test( + 'replaces useRef usages with useRefInit when an argument is passed', + () async { + await testSuggestor( + expectedPatchCount: 1, + input: withOverReactImport(''' + final Foo = uiFunction( + (props) { + final foo = useRef('bar'); + return (Dom.div()..id = foo.current)(); + }, + UiFactoryConfig(displayName: 'Foo'), + ); + '''), + expectedOutput: withOverReactImport(''' + final Foo = uiFunction( + (props) { + final foo = useRefInit('bar'); + return (Dom.div()..id = foo.current)(); + }, + UiFactoryConfig(displayName: 'Foo'), + ); + '''), + ); + }); + }); +}