Skip to content

Commit

Permalink
Add suggestor to replace null argument in dom callback
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronlademann-wf committed Feb 29, 2024
1 parent 9912a6f commit dfced0c
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 0 deletions.
15 changes: 15 additions & 0 deletions bin/dom_callback_null_args.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/dom_callback_null_args.dart';
128 changes: 128 additions & 0 deletions lib/src/dart3_suggestors/null_safety_prep/dom_callback_null_args.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:over_react_codemod/src/util/class_suggestor.dart';

/// Suggestor that replaces a `null` literal argument passed to a "DOM" callback
/// with a generated `SyntheticEvent` object of the expected type.
///
/// Example:
///
/// ```dart
/// final props = domProps();
/// // Before
/// props.onClick(null);
/// // After
/// props.onClick(createSyntheticMouseEvent());
/// ```
class DomCallbackNullArgs extends RecursiveAstVisitor with ClassSuggestor {
ResolvedUnitResult? _result;

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

if (node.arguments.isEmpty) return;
dynamic firstArg = node.arguments.elementAt(0);
if (firstArg is! NullLiteral) return;

dynamic possibleCallback = node.parent;
if (possibleCallback is FunctionExpressionInvocation) {
String fnName = '';
if (possibleCallback.function is PropertyAccess) {
fnName =
(possibleCallback.function as PropertyAccess).propertyName.name;
} else if (possibleCallback.function is SimpleIdentifier) {
fnName = (possibleCallback.function as SimpleIdentifier).name;
}

if (callbackToSyntheticEventTypeMap.keys.contains(fnName)) {
dynamic possibleSyntheticEventCallbackFn =
possibleCallback.staticInvokeType;
if (possibleSyntheticEventCallbackFn is FunctionType) {
final syntheticEventTypeName = possibleSyntheticEventCallbackFn
.parameters.firstOrNull?.type.element?.name;
yieldPatch('create${syntheticEventTypeName}()',
firstArg.literal.offset, firstArg.literal.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);
}

static const callbackToSyntheticEventTypeMap = {
'onAnimationEnd': 'SyntheticAnimationEvent',
'onAnimationIteration': 'SyntheticAnimationEvent',
'onAnimationStart': 'SyntheticAnimationEvent',
'onCopy': 'SyntheticClipboardEvent',
'onCut': 'SyntheticClipboardEvent',
'onPaste': 'SyntheticClipboardEvent',
'onKeyDown': 'SyntheticKeyboardEvent',
'onKeyPress': 'SyntheticKeyboardEvent',
'onKeyUp': 'SyntheticKeyboardEvent',
'onFocus': 'SyntheticFocusEvent',
'onBlur': 'SyntheticFocusEvent',
'onChange': 'SyntheticFormEvent',
'onInput': 'SyntheticFormEvent',
'onSubmit': 'SyntheticFormEvent',
'onReset': 'SyntheticFormEvent',
'onClick': 'SyntheticMouseEvent',
'onContextMenu': 'SyntheticMouseEvent',
'onDoubleClick': 'SyntheticMouseEvent',
'onDrag': 'SyntheticMouseEvent',
'onDragEnd': 'SyntheticMouseEvent',
'onDragEnter': 'SyntheticMouseEvent',
'onDragExit': 'SyntheticMouseEvent',
'onDragLeave': 'SyntheticMouseEvent',
'onDragOver': 'SyntheticMouseEvent',
'onDragStart': 'SyntheticMouseEvent',
'onDrop': 'SyntheticMouseEvent',
'onMouseDown': 'SyntheticMouseEvent',
'onMouseEnter': 'SyntheticMouseEvent',
'onMouseLeave': 'SyntheticMouseEvent',
'onMouseMove': 'SyntheticMouseEvent',
'onMouseOut': 'SyntheticMouseEvent',
'onMouseOver': 'SyntheticMouseEvent',
'onMouseUp': 'SyntheticMouseEvent',
'onPointerCancel': 'SyntheticPointerEvent',
'onPointerDown': 'SyntheticPointerEvent',
'onPointerEnter': 'SyntheticPointerEvent',
'onPointerLeave': 'SyntheticPointerEvent',
'onPointerMove': 'SyntheticPointerEvent',
'onPointerOver': 'SyntheticPointerEvent',
'onPointerOut': 'SyntheticPointerEvent',
'onPointerUp': 'SyntheticPointerEvent',
'onTouchCancel': 'SyntheticTouchEvent',
'onTouchEnd': 'SyntheticTouchEvent',
'onTouchMove': 'SyntheticTouchEvent',
'onTouchStart': 'SyntheticTouchEvent',
'onTransitionEnd': 'SyntheticTransitionEvent',
'onScroll': 'SyntheticUIEvent',
'onWheel': 'SyntheticWheelEvent',
};
}
43 changes: 43 additions & 0 deletions lib/src/executables/dom_callback_null_args.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/dom_callback_null_args.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(DomCallbackNullArgs()),
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 @@ -43,6 +43,7 @@ executables:
dependency_validator_ignore:
mui_migration:
required_flux_props:
dom_callback_null_args:
rmui_preparation:
rmui_bundle_update:
intl_message_migration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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/dom_callback_null_args.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(
DomCallbackNullArgs(),
resolvedContext: resolvedContext,
);
});

test(
'leaves dom callbacks alone when a non-null value is passed as the first argument',
() async {
await testSuggestor(
expectedPatchCount: 0,
input: withOverReactImport('''
main() {
final props = domProps();
props.onClick(createSyntheticMouseEvent());
final onBlur = props.onBlur;
onBlur(createSyntheticFocusEvent());
}
'''),
);
});

test(
'leaves functions alone when a null value is passed as the first argument if they are not dom callbacks',
() async {
await testSuggestor(
expectedPatchCount: 0,
input: withOverReactImport('''
main() {
void foo(dynamic arg) {}
foo(null);
}
'''),
);
});

group(
'replaces null arg in dom callback with an empty synthetic event of the correct type: ',
() {
DomCallbackNullArgs.callbackToSyntheticEventTypeMap
.forEach((callbackFnName, syntheticEventTypeName) {
test(callbackFnName, () async {
await testSuggestor(
expectedPatchCount: 2,
input: withOverReactImport('''
main() {
final props = domProps();
props.${callbackFnName}(null);
final ${callbackFnName} = props.${callbackFnName};
${callbackFnName}(null);
}
'''),
expectedOutput: withOverReactImport('''
main() {
final props = domProps();
props.${callbackFnName}(create${syntheticEventTypeName}());
final ${callbackFnName} = props.${callbackFnName};
${callbackFnName}(create${syntheticEventTypeName}());
}
'''),
);
});
});
});
});
}

0 comments on commit dfced0c

Please sign in to comment.