|
| 1 | +// Copyright 2024 Workiva Inc. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +import 'package:analyzer/dart/ast/ast.dart'; |
| 16 | +import 'package:analyzer/dart/ast/visitor.dart'; |
| 17 | +import 'package:analyzer/dart/element/element.dart'; |
| 18 | +import 'package:analyzer/dart/element/type.dart'; |
| 19 | +import 'package:collection/collection.dart'; |
| 20 | +import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/utils/props_utils.dart'; |
| 21 | +import 'package:over_react_codemod/src/util.dart'; |
| 22 | +import 'package:over_react_codemod/src/util/class_suggestor.dart'; |
| 23 | + |
| 24 | +import 'analyzer_plugin_utils.dart'; |
| 25 | + |
| 26 | +/// Suggestor that adds `@Props(disableRequiredPropValidation: {...})` annotations |
| 27 | +/// for props that are set in `connect` components. |
| 28 | +class ConnectRequiredProps extends RecursiveAstVisitor with ClassSuggestor { |
| 29 | + /// Running list of props that should be ignored per mixin that will all be added |
| 30 | + /// at the end in [generatePatches]. |
| 31 | + final _ignoredPropsByMixin = <InterfaceElement, Set<String>>{}; |
| 32 | + |
| 33 | + @override |
| 34 | + visitCascadeExpression(CascadeExpression node) { |
| 35 | + super.visitCascadeExpression(node); |
| 36 | + |
| 37 | + // Verify the builder usage is within the `connect` method call. |
| 38 | + final connect = node.thisOrAncestorMatching<MethodInvocation>( |
| 39 | + (n) => n is MethodInvocation && n.methodName.name == 'connect'); |
| 40 | + if (connect == null) return; |
| 41 | + |
| 42 | + // Verify the builder usage is within one of the targeted connect args. |
| 43 | + final connectArgs = |
| 44 | + connect.argumentList.arguments.whereType<NamedExpression>(); |
| 45 | + final connectArg = node.thisOrAncestorMatching<NamedExpression>((n) => |
| 46 | + n is NamedExpression && |
| 47 | + connectArgs.contains(n) && |
| 48 | + connectArgNames.contains(n.name.label.name)); |
| 49 | + if (connectArg == null) return; |
| 50 | + |
| 51 | + final cascadedProps = getCascadedProps(node).toList(); |
| 52 | + |
| 53 | + for (final field in cascadedProps) { |
| 54 | + final propsElement = |
| 55 | + node.staticType?.typeOrBound.tryCast<InterfaceType>()?.element; |
| 56 | + if (propsElement == null) continue; |
| 57 | + |
| 58 | + // Keep a running list of props to ignore per props mixin. |
| 59 | + final fieldName = field.name.name; |
| 60 | + _ignoredPropsByMixin.putIfAbsent(propsElement, () => {}).add(fieldName); |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + @override |
| 65 | + Future<void> generatePatches() async { |
| 66 | + _ignoredPropsByMixin.clear(); |
| 67 | + final result = await context.getResolvedUnit(); |
| 68 | + if (result == null) { |
| 69 | + throw Exception( |
| 70 | + 'Could not get resolved result for "${context.relativePath}"'); |
| 71 | + } |
| 72 | + result.unit.accept(this); |
| 73 | + |
| 74 | + // Add the patches at the end so that all the props to be ignored can be collected |
| 75 | + // from the different args in `connect` before adding patches to avoid duplicate patches. |
| 76 | + _ignoredPropsByMixin.forEach((propsClass, propsToIgnore) { |
| 77 | + final classNode = |
| 78 | + NodeLocator2(propsClass.nameOffset).searchWithin(result.unit); |
| 79 | + if (classNode != null && classNode is NamedCompilationUnitMember) { |
| 80 | + final existingAnnotation = |
| 81 | + classNode.metadata.where((c) => c.name.name == 'Props').firstOrNull; |
| 82 | + |
| 83 | + if (existingAnnotation == null) { |
| 84 | + // Add full @Props annotation if it doesn't exist. |
| 85 | + yieldPatch( |
| 86 | + '@Props($annotationArg: {${propsToIgnore.map((p) => '\'$p\'').join(', ')}})\n', |
| 87 | + classNode.offset, |
| 88 | + classNode.offset); |
| 89 | + } else { |
| 90 | + final existingAnnotationArg = existingAnnotation.arguments?.arguments |
| 91 | + .whereType<NamedExpression>() |
| 92 | + .where((e) => e.name.label.name == annotationArg) |
| 93 | + .firstOrNull; |
| 94 | + |
| 95 | + if (existingAnnotationArg == null) { |
| 96 | + // Add disable validation arg to existing @Props annotation. |
| 97 | + final offset = existingAnnotation.arguments?.leftParenthesis.end; |
| 98 | + if (offset != null) { |
| 99 | + yieldPatch( |
| 100 | + '$annotationArg: {${propsToIgnore.map((p) => '\'$p\'').join(', ')}}${existingAnnotation.arguments?.arguments.isNotEmpty ?? false ? ', ' : ''}', |
| 101 | + offset, |
| 102 | + offset); |
| 103 | + } |
| 104 | + } else { |
| 105 | + // Add props to disable validation for to the existing list of disabled |
| 106 | + // props in the @Props annotation if they aren't already listed. |
| 107 | + final existingList = |
| 108 | + existingAnnotationArg.expression.tryCast<SetOrMapLiteral>(); |
| 109 | + if (existingList != null) { |
| 110 | + final alreadyIgnored = existingList.elements |
| 111 | + .whereType<SimpleStringLiteral>() |
| 112 | + .map((e) => e.stringValue) |
| 113 | + .toList(); |
| 114 | + final newPropsToIgnore = |
| 115 | + propsToIgnore.where((p) => !alreadyIgnored.contains(p)); |
| 116 | + if (newPropsToIgnore.isNotEmpty) { |
| 117 | + final offset = existingList.leftBracket.end; |
| 118 | + yieldPatch( |
| 119 | + '${newPropsToIgnore.map((p) => '\'$p\'').join(', ')}, ', |
| 120 | + offset, |
| 121 | + offset); |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + }); |
| 128 | + } |
| 129 | + |
| 130 | + static const connectArgNames = [ |
| 131 | + 'mapStateToProps', |
| 132 | + 'mapStateToPropsWithOwnProps', |
| 133 | + 'mapDispatchToProps', |
| 134 | + 'mapDispatchToPropsWithOwnProps', |
| 135 | + ]; |
| 136 | + static const annotationArg = 'disableRequiredPropValidation'; |
| 137 | +} |
0 commit comments