diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df5edd99..04b400ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,18 +13,22 @@ jobs: checks: uses: Workiva/gha-dart-oss/.github/workflows/checks.yaml@v0.1.12 with: + sdk: 3.12.2 + format-check: false additional-checks: | no_entrypoint_imports build: uses: Workiva/gha-dart-oss/.github/workflows/build.yaml@v0.1.12 + with: + sdk: 3.12.2 test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - sdk: [ 2.19.6 ] + sdk: [ 3.12.2 ] steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v0.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 413f32d6..f97a3af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased +- Upgrade to Dart 3 SDK (`>=3.12.0 <4.0.0`) +- Remove `null_safety_required_props` codemod and related fixtures (null-safety migration tooling no longer needed under Dart 3) + ## 2.38.0 - Add mui_system_props_migration codemod to migrate from system props to sx diff --git a/bin/null_safety_migrator_companion.dart b/bin/null_safety_migrator_companion.dart deleted file mode 100644 index bf2b0410..00000000 --- a/bin/null_safety_migrator_companion.dart +++ /dev/null @@ -1,15 +0,0 @@ -// 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_migrator_companion.dart'; diff --git a/bin/null_safety_prep.dart b/bin/null_safety_prep.dart deleted file mode 100644 index 5e9a24af..00000000 --- a/bin/null_safety_prep.dart +++ /dev/null @@ -1,15 +0,0 @@ -// 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/bin/null_safety_required_props.dart b/bin/null_safety_required_props.dart deleted file mode 100644 index 243b83c1..00000000 --- a/bin/null_safety_required_props.dart +++ /dev/null @@ -1,15 +0,0 @@ -// 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_required_props.dart'; diff --git a/bin/required_flux_props.dart b/bin/required_flux_props.dart deleted file mode 100644 index 5ae74318..00000000 --- a/bin/required_flux_props.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023 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/required_flux_props.dart'; diff --git a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart index 3c5dcb8e..f5446948 100644 --- a/lib/src/boilerplate_suggestors/boilerplate_utilities.dart +++ b/lib/src/boilerplate_suggestors/boilerplate_utilities.dart @@ -123,7 +123,7 @@ class SemverHelper { } final locations = []; - _exportList!.forEach((key, value) { + _exportList.forEach((key, value) { if (value['type'] == 'class' && value['grammar']['name'] == className) { locations.add(key); } diff --git a/lib/src/creator_utils.dart b/lib/src/creator_utils.dart index d4487bc6..7531f648 100644 --- a/lib/src/creator_utils.dart +++ b/lib/src/creator_utils.dart @@ -206,7 +206,7 @@ class DartProjectCreatorTestConfig { } String get testName { - if (_testName != null) return _testName!; + if (_testName != null) return _testName; var name = 'returns exit code ${expectedExitCode} with '; if (pubspecCreators.isEmpty) { diff --git a/lib/src/dart3_suggestors/null_safety_prep/analyzer_plugin_utils.dart b/lib/src/dart3_suggestors/null_safety_prep/analyzer_plugin_utils.dart deleted file mode 100644 index 5afcc755..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/analyzer_plugin_utils.dart +++ /dev/null @@ -1,151 +0,0 @@ -// These utilities were copied from analyzer utils in over_react/analyzer_plugin -// Permalink: https://github.com/Workiva/over_react/blob/a8129f38ea8dfa0023d06250349fc8e86025df3a/tools/analyzer_plugin/lib/src/util/analyzer_util.dart#L4 -// Permalink: https://github.com/Workiva/over_react/blob/a8129f38ea8dfa0023d06250349fc8e86025df3a/tools/analyzer_plugin/lib/src/util/ast_util.dart -// -// 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/element/element.dart'; -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; - -/// Returns the AST node of the variable declaration associated with the [element] within [root], -/// or null if the [element] doesn't correspond to a variable declaration, or if it can't be found in [root]. -VariableDeclaration? lookUpVariable(Element element, AstNode root) { - final node = NodeLocator2(element.nameOffset).searchWithin(root); - if (node is VariableDeclaration && node.declaredElement == element) { - return node; - } - - return null; -} - -/// An object used to locate the [AstNode] associated with a source range. -/// More specifically, they will return the deepest [AstNode] which completely -/// encompasses the specified range with some exceptions: -/// -/// - Offsets that fall between the name and type/formal parameter list of a -/// declaration will return the declaration node and not the parameter list -/// node. -class NodeLocator2 extends UnifyingAstVisitor { - /// The inclusive start offset of the range used to identify the node. - final int _startOffset; - - /// The inclusive end offset of the range used to identify the node. - final int _endOffset; - - /// The found node or `null` if there is no such node. - AstNode? _foundNode; - - /// Initialize a newly created locator to locate the deepest [AstNode] for - /// which `node.offset <= [startOffset]` and `[endOffset] < node.end`. - /// - /// If [endOffset] is not provided, then it is considered the same as the - /// given [startOffset]. - NodeLocator2(int startOffset, [int? endOffset]) - : _startOffset = startOffset, - _endOffset = endOffset ?? startOffset; - - /// Search within the given AST [node] and return the node that was found, - /// or `null` if no node was found. - AstNode? searchWithin(AstNode? node) { - if (node == null) { - return null; - } - try { - node.accept(this); - } catch (_) { - return null; - } - return _foundNode; - } - - @override - void visitConstructorDeclaration(ConstructorDeclaration node) { - // Names do not have AstNodes but offsets at the end should be treated as - // part of the declaration (not parameter list). - if (_startOffset == _endOffset && - _startOffset == (node.name ?? node.returnType).end) { - _foundNode = node; - return; - } - - super.visitConstructorDeclaration(node); - } - - @override - void visitFunctionDeclaration(FunctionDeclaration node) { - // Names do not have AstNodes but offsets at the end should be treated as - // part of the declaration (not parameter list). - if (_startOffset == _endOffset && _startOffset == node.name.end) { - _foundNode = node; - return; - } - - super.visitFunctionDeclaration(node); - } - - @override - void visitMethodDeclaration(MethodDeclaration node) { - // Names do not have AstNodes but offsets at the end should be treated as - // part of the declaration (not parameter list). - if (_startOffset == _endOffset && _startOffset == node.name.end) { - _foundNode = node; - return; - } - - super.visitMethodDeclaration(node); - } - - @override - void visitNode(AstNode node) { - // Don't visit a new tree if the result has been already found. - if (_foundNode != null) { - return; - } - // Check whether the current node covers the selection. - var beginToken = node.beginToken; - var endToken = node.endToken; - // Don't include synthetic tokens. - while (endToken != beginToken) { - // Fasta scanner reports unterminated string literal errors - // and generates a synthetic string token with non-zero length. - // Because of this, check for length > 0 rather than !isSynthetic. - if (endToken.isEof || endToken.length > 0) { - break; - } - endToken = endToken.previous!; - } - var end = endToken.end; - var start = node.offset; - if (end <= _startOffset || start > _endOffset) { - return; - } - // Check children. - try { - node.visitChildren(this); - } catch (_) { - // Ignore the exception and proceed in order to visit the rest of the - // structure. - } - // Found a child. - if (_foundNode != null) { - return; - } - // Check this node. - if (start <= _startOffset && _endOffset < end) { - _foundNode = node; - } - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor.dart b/lib/src/dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor.dart deleted file mode 100644 index 892aa85b..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor.dart +++ /dev/null @@ -1,151 +0,0 @@ -// Adapted from the add_create_ref assist in over_react/analyzer_plugin -// Permalink: https://github.com/Workiva/over_react/blob/a8129f38ea8dfa0023d06250349fc8e86025df3a/tools/analyzer_plugin/lib/src/assist/refs/add_create_ref.dart#L4 -// -// 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/element/element.dart'; -import 'package:collection/collection.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart'; -import 'package:over_react_codemod/src/util/component_usage.dart'; -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; - -import '../../util.dart'; -import '../../util/class_suggestor.dart'; -import 'analyzer_plugin_utils.dart'; - -/// Suggestor to add nullability hints to ref types. -/// -/// (1) For ref prop param types: -/// ``` -/// - (ButtonToolbar()..ref = (ButtonElement r) => ref = r)(); -/// + (ButtonToolbar()..ref = (ButtonElement /*?*/ r) => ref = r)(); -/// ``` -/// -/// (2) For ref variable declarations: -/// ``` -/// - ButtonElement ref; -/// + ButtonElement /*?*/ ref; -/// (ButtonToolbar()..ref = (r) => ref = r)(); -/// ``` -/// -/// (3) For ref prop type casts: -/// ``` -/// - (ButtonToolbar()..ref = (r) => ref = r as ButtonElement)(); -/// + (ButtonToolbar()..ref = (r) => ref = r as ButtonElement /*?*/)(); -/// ``` -/// -/// These hints are needed because the null-safety migration tool does not do -/// well at inferring that ref types should be nullable. -class CallbackRefHintSuggestor extends RecursiveAstVisitor - with ClassSuggestor { - CallbackRefHintSuggestor(); - - late ResolvedUnitResult result; - - @override - Future visitCascadeExpression(CascadeExpression node) async { - super.visitCascadeExpression(node); - - final cascadedProps = node.cascadeSections - .whereType() - .where((assignment) => assignment.leftHandSide is PropertyAccess) - .map((assignment) => PropAssignment(assignment)); - - for (final prop in cascadedProps) { - if (prop.name.name == 'ref') { - final rhs = - prop.rightHandSide.unParenthesized.tryCast(); - if (rhs == null) return null; - - // Add nullability hint to parameter if typed. - final param = rhs.parameters?.parameters.first; - if (param is SimpleFormalParameter) { - final type = param.type; - if (type != null && !nullableHintAlreadyExists(type)) { - yieldPatch(nullableHint, type.end, type.end); - } - } - - final refParamName = param?.name?.toString(); - if (refParamName != null) { - // Add nullability hint to ref variable declarations. - final refCallbackArg = rhs.parameters?.parameters.firstOrNull; - if (refCallbackArg != null) { - final referencesToArg = allDescendantsOfType(rhs.body) - .where((identifier) => - identifier.staticElement == refCallbackArg.declaredElement); - - for (final reference in referencesToArg) { - final parent = reference.parent; - if (parent is AssignmentExpression && - parent.rightHandSide == reference) { - final lhs = parent.leftHandSide; - if (lhs is Identifier) { - final varElement = - // Variable in function component. - lhs.staticElement?.tryCast() ?? - // Variable in class component. - lhs.parent - ?.tryCast() - ?.writeElement - ?.tryCast() - ?.variable; - if (varElement != null) { - final varType = lookUpVariable(varElement, result.unit) - ?.parent - .tryCast() - ?.type; - if (varType != null && - !nullableHintAlreadyExists(varType) && - varType.toSource() != 'dynamic') { - yieldPatch(nullableHint, varType.end, varType.end); - } - } - } - } - } - } - - // Add nullability hint to any casts in the body of the callback ref. - final refCasts = allDescendantsOfType(rhs.body).where( - (expression) => - expression.expression.toSource() == refParamName && - !nullableHintAlreadyExists(expression.type)); - for (final cast in refCasts) { - yieldPatch(nullableHint, cast.type.end, cast.type.end); - } - } - } - } - } - - @override - Future generatePatches() async { - final r = await context.getResolvedUnit(); - if (r == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - result = r; - // Don't make any updates if the file is already null safe. - if (result.libraryElement.isNonNullableByDefault) { - return; - } - - result.unit.visitChildren(this); - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/class_component_required_default_props.dart b/lib/src/dart3_suggestors/null_safety_prep/class_component_required_default_props.dart deleted file mode 100644 index 24eb8a2f..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/class_component_required_default_props.dart +++ /dev/null @@ -1,117 +0,0 @@ -// Adapted from the missing_required_prop diagnostic in over_react/analyzer_plugin -// Permalink: https://github.com/Workiva/over_react/blob/ae8c898650537e49f35f98ad1b065c516207838e/tools/analyzer_plugin/lib/src/util/prop_declarations/defaulted_props.dart - -// 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/utils/props_utils.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/component_usage.dart'; -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:over_react_codemod/src/vendor/over_react_analyzer_plugin/get_all_props.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import '../required_props/codemod/recommender.dart'; -import 'utils/class_component_required_fields.dart'; - -/// Suggestor to assist with preparations for null-safety by adding -/// "requiredness" (`late`) / nullability (`?`/`!`) hints to prop types -/// based on their access within a class component's `defaultProps`. -/// -/// If a prop is defaulted to a non-null value within `defaultProps`, the -/// corresponding prop declaration will gain a `/*late*/` modifier hint to the -/// left of the type, and a non-nullable type hint (`/*!*/`) to the right of -/// the type to assist the `nnbd_migration:migrate` script when it attempts to -/// infer a prop's nullability. -/// -/// **Optionally**, an [sdkVersion] can be passed to the constructor. -/// When set to a version that opts-in to Dart's null safety feature, -/// the `late` / `?` type modifiers will be actual modifiers rather -/// than commented hints. This should only be done using an explicit opt-in -/// flag from the executable as most consumers that have migrated to null-safety -/// will have already run this script prior to the null safety migration and thus -/// the `/*late*/` / `/*?*/` hints will already be converted to actual modifiers. -/// -/// **Before** -/// ```dart -/// mixin FooProps on UiProps { -/// String defaultedNullable; -/// num defaultedNonNullable; -/// } -/// class FooComponent extends UiComponent2 { -/// @override -/// get defaultProps => (newProps() -/// ..defaultedNullable = null -/// ..defaultedNonNullable = 2.1 -/// ); -/// -/// // ... -/// } -/// ``` -/// -/// **After** -/// ```dart -/// mixin FooProps on UiProps { -/// /*late*/ String/*?*/ defaultedNullable; -/// /*late*/ num/*!*/ defaultedNonNullable; -/// } -/// class FooComponent extends UiComponent2 { -/// @override -/// get defaultProps => (newProps() -/// ..defaultedNullable = null -/// ..defaultedNonNullable = 2.1 -/// ); -/// -/// // ... -/// } -/// ``` -class ClassComponentRequiredDefaultPropsMigrator - extends ClassComponentRequiredFieldsMigrator { - final PropRequirednessRecommender? _propRequirednessRecommender; - - ClassComponentRequiredDefaultPropsMigrator( - [Version? sdkVersion, this._propRequirednessRecommender]) - : super('defaultProps', 'getDefaultProps', sdkVersion); - - @override - Future visitCascadeExpression(CascadeExpression node) async { - // Don't make any updates if the file is already null safe. - if (result.libraryElement.isNonNullableByDefault) { - return; - } - - super.visitCascadeExpression(node); - - final isDefaultProps = node.ancestors.any((ancestor) { - if (ancestor is MethodDeclaration) { - return [relevantGetterName, relevantMethodName] - .contains(ancestor.declaredElement?.name); - } - if (ancestor is VariableDeclaration && - (ancestor.parentFieldDeclaration?.isStatic ?? false)) { - return RegExp(RegExp.escape(relevantGetterName), caseSensitive: false) - .hasMatch(ancestor.name.lexeme); - } - return false; - }); - - // If this cascade is not assigning values to defaultProps, bail. - if (!isDefaultProps) return; - - final cascadedDefaultProps = getCascadedProps(node); - - patchFieldDeclarations( - getAllProps, cascadedDefaultProps, node, _propRequirednessRecommender); - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/class_component_required_initial_state.dart b/lib/src/dart3_suggestors/null_safety_prep/class_component_required_initial_state.dart deleted file mode 100644 index 3488f42a..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/class_component_required_initial_state.dart +++ /dev/null @@ -1,104 +0,0 @@ -// Adapted from the missing_required_prop diagnostic in over_react/analyzer_plugin -// Permalink: https://github.com/Workiva/over_react/blob/ae8c898650537e49f35f98ad1b065c516207838e/tools/analyzer_plugin/lib/src/util/prop_declarations/defaulted_props.dart - -// 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/util/component_usage.dart'; -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:over_react_codemod/src/util/get_all_state.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'utils/class_component_required_fields.dart'; - -/// Suggestor to assist with preparations for null-safety by adding -/// "requiredness" (`late`) / nullability (`?`/`!`) hints to state field types -/// based on their access within a class component's `initialState`. -/// -/// If a piece of state is initialized to a non-null value within `initialState`, -/// the corresponding declaration will gain a `/*late*/` modifier hint to -/// the left of the type, and a non-nullable type hint (`/*!*/`) to the right of -/// the type to assist the `nnbd_migration:migrate` script when it attempts to -/// infer a state field's nullability. -/// -/// **Optionally**, an [sdkVersion] can be passed to the constructor. -/// When set to a version that opts-in to Dart's null safety feature, -/// the `late` / `?` type modifiers will be actual modifiers rather -/// than commented hints. This should only be done using an explicit opt-in -/// flag from the executable as most consumers that have migrated to null-safety -/// will have already run this script prior to the null safety migration and thus -/// the `/*late*/` / `/*?*/` hints will already be converted to actual modifiers. -/// -/// **Before** -/// ```dart -/// mixin FooState on UiState { -/// String defaultedNullable; -/// num defaultedNonNullable; -/// } -/// class FooComponent extends UiStatefulComponent2 { -/// @override -/// get initialState => (newState() -/// ..defaultedNullable = null -/// ..defaultedNonNullable = 2.1 -/// ); -/// -/// // ... -/// } -/// ``` -/// -/// **After** -/// ```dart -/// mixin FooState on UiState { -/// /*late*/ String/*?*/ defaultedNullable; -/// /*late*/ num/*!*/ defaultedNonNullable; -/// } -/// class FooComponent extends UiStatefulComponent2 { -/// @override -/// get initialState => (newState() -/// ..defaultedNullable = null -/// ..defaultedNonNullable = 2.1 -/// ); -/// -/// // ... -/// } -/// ``` -class ClassComponentRequiredInitialStateMigrator - extends ClassComponentRequiredFieldsMigrator { - ClassComponentRequiredInitialStateMigrator([Version? sdkVersion]) - : super('initialState', 'getInitialState', sdkVersion); - - @override - Future visitCascadeExpression(CascadeExpression node) async { - // Don't make any updates if the file is already null safe. - if (result.libraryElement.isNonNullableByDefault) { - return; - } - - super.visitCascadeExpression(node); - - final isInitialState = [relevantGetterName, relevantMethodName].contains( - node.thisOrAncestorOfType()?.declaredElement?.name); - - // If this cascade is not assigning values to defaultProps, bail. - if (!isInitialState) return; - - final cascadedInitialState = node.cascadeSections - .whereType() - .where((assignment) => assignment.leftHandSide is PropertyAccess) - .map((assignment) => StateAssignment(assignment)) - .where((prop) => prop.node.writeElement?.displayName != null); - - patchFieldDeclarations(getAllState, cascadedInitialState, node); - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/connect_required_props.dart b/lib/src/dart3_suggestors/null_safety_prep/connect_required_props.dart deleted file mode 100644 index 83aba93d..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/connect_required_props.dart +++ /dev/null @@ -1,141 +0,0 @@ -// 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:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:collection/collection.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/utils/props_utils.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/class_suggestor.dart'; - -import 'analyzer_plugin_utils.dart'; - -/// Suggestor that adds `@Props(disableRequiredPropValidation: {...})` annotations -/// for props that are set in `connect` components. -class ConnectRequiredProps extends RecursiveAstVisitor with ClassSuggestor { - /// Running list of props that should be ignored per mixin that will all be added - /// at the end in [generatePatches]. - final _ignoredPropsByMixin = >{}; - - @override - visitCascadeExpression(CascadeExpression node) { - super.visitCascadeExpression(node); - - // Verify the builder usage is within the `connect` method call. - final connect = node.thisOrAncestorMatching( - (n) => n is MethodInvocation && n.methodName.name == 'connect'); - if (connect == null) return; - - // Verify the builder usage is within one of the targeted connect args. - final connectArgs = - connect.argumentList.arguments.whereType(); - final connectArg = node.thisOrAncestorMatching((n) => - n is NamedExpression && - connectArgs.contains(n) && - connectArgNames.contains(n.name.label.name)); - if (connectArg == null) return; - - final cascadedProps = getCascadedProps(node).toList(); - - for (final field in cascadedProps) { - final propsElement = - node.staticType?.typeOrBound.tryCast()?.element; - if (propsElement == null) continue; - - // Keep a running list of props to ignore per props mixin. - final fieldName = field.name.name; - _ignoredPropsByMixin.putIfAbsent(propsElement, () => {}).add(fieldName); - } - } - - @override - Future generatePatches() async { - _ignoredPropsByMixin.clear(); - final result = await context.getResolvedUnit(); - if (result == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - // Don't make any updates if the file is already null safe. - if (result.libraryElement.isNonNullableByDefault) { - return; - } - result.unit.accept(this); - - // Add the patches at the end so that all the props to be ignored can be collected - // from the different args in `connect` before adding patches to avoid duplicate patches. - _ignoredPropsByMixin.forEach((propsClass, propsToIgnore) { - final classNode = - NodeLocator2(propsClass.nameOffset).searchWithin(result.unit); - if (classNode != null && classNode is NamedCompilationUnitMember) { - final existingAnnotation = - classNode.metadata.where((c) => c.name.name == 'Props').firstOrNull; - - if (existingAnnotation == null) { - // Add full @Props annotation if it doesn't exist. - yieldPatch( - '@Props($annotationArg: {${propsToIgnore.map((p) => '\'$p\'').join(', ')}})\n', - classNode.offset, - classNode.offset); - } else { - final existingAnnotationArg = existingAnnotation.arguments?.arguments - .whereType() - .where((e) => e.name.label.name == annotationArg) - .firstOrNull; - - if (existingAnnotationArg == null) { - // Add disable validation arg to existing @Props annotation. - final offset = existingAnnotation.arguments?.leftParenthesis.end; - if (offset != null) { - yieldPatch( - '$annotationArg: {${propsToIgnore.map((p) => '\'$p\'').join(', ')}}${existingAnnotation.arguments?.arguments.isNotEmpty ?? false ? ', ' : ''}', - offset, - offset); - } - } else { - // Add props to disable validation for to the existing list of disabled - // props in the @Props annotation if they aren't already listed. - final existingList = - existingAnnotationArg.expression.tryCast(); - if (existingList != null) { - final alreadyIgnored = existingList.elements - .whereType() - .map((e) => e.stringValue) - .toList(); - final newPropsToIgnore = - propsToIgnore.where((p) => !alreadyIgnored.contains(p)); - if (newPropsToIgnore.isNotEmpty) { - final offset = existingList.leftBracket.end; - yieldPatch( - '${newPropsToIgnore.map((p) => '\'$p\'').join(', ')}, ', - offset, - offset); - } - } - } - } - } - }); - } - - static const connectArgNames = [ - 'mapStateToProps', - 'mapStateToPropsWithOwnProps', - 'mapDispatchToProps', - 'mapDispatchToPropsWithOwnProps', - ]; - static const annotationArg = 'disableRequiredPropValidation'; -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/dom_callback_null_args.dart b/lib/src/dart3_suggestors/null_safety_prep/dom_callback_null_args.dart deleted file mode 100644 index dd54fc8d..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/dom_callback_null_args.dart +++ /dev/null @@ -1,128 +0,0 @@ -// 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 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', - }; -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor.dart b/lib/src/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor.dart deleted file mode 100644 index 9bbc011e..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor.dart +++ /dev/null @@ -1,177 +0,0 @@ -// 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/token.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/class_suggestor.dart'; - -/// Suggestor that replaces conditional calls to functions declared in props -/// with inline null-aware property access. -/// -/// This is helpful for null-safety migrations because the conditional -/// function calls will otherwise get migrated with `!` modifiers. -/// -/// **Before:** -/// -/// ```dart -/// if (props.someCallback != null) { -/// props.someCallback(someValue); -/// } -/// -/// // Will be migrated to: -/// if (props.someCallback != null) { -/// props.someCallback!(someValue); -/// } -/// ``` -/// -/// **After:** -/// -/// ```dart -/// // This will require no changes during a null-safety migration. -/// props.someCallback?.call(someValue); -/// ``` -class FnPropNullAwareCallSuggestor extends RecursiveAstVisitor - with ClassSuggestor { - ResolvedUnitResult? _result; - - @override - visitExpressionStatement(ExpressionStatement node) { - super.visitExpressionStatement(node); - - if (node.expression is! BinaryExpression) return; - - final relevantExprStatement = - _getPropFunctionExpressionBeingCalledConditionally( - node.expression as BinaryExpression); - final inlineBinaryExpr = - // This cast is safe due to the type checks within `_getPropFunctionExpressionBeingCalledConditionally`. - relevantExprStatement?.expression as BinaryExpression?; - if (inlineBinaryExpr == null) return; - final relevantFnExpr = - // This cast is safe due to the type checks within `_getPropFunctionExpressionBeingCalledConditionally`. - inlineBinaryExpr.rightOperand as FunctionExpressionInvocation; - // This cast is safe due to the type checks within `_getPropFunctionExpressionBeingCalledConditionally`. - final fn = relevantFnExpr.function as PropertyAccess; - - yieldPatch( - '${fn.target}.${fn.propertyName}?.call${relevantFnExpr.argumentList};', - node.offset, - node.end); - } - - @override - visitIfStatement(IfStatement node) { - super.visitIfStatement(node); - - if (node.condition is! BinaryExpression) return; - - final relevantFnExprStatement = - _getPropFunctionExpressionBeingCalledConditionally( - node.condition as BinaryExpression); - final relevantFnExpr = - // This cast is safe due to the type checks within `_getPropFunctionExpressionBeingCalledConditionally`. - relevantFnExprStatement?.expression as FunctionExpressionInvocation?; - if (relevantFnExpr == null) return; - // This cast is safe due to the type checks within `_getPropFunctionExpressionBeingCalledConditionally`. - final fn = relevantFnExpr.function as PropertyAccess?; - if (fn == null) return; - - yieldPatch( - '${fn.target}.${fn.propertyName}?.call${relevantFnExpr.argumentList};', - node.offset, - node.end); - } - - /// Returns the function expression (e.g. `props.onClick(event)`) being called - /// after the null condition is checked. - ExpressionStatement? _getPropFunctionExpressionBeingCalledConditionally( - BinaryExpression condition) { - final parent = condition.parent; - if (parent is! IfStatement) return null; - - final propFunctionBeingNullChecked = - _getPropFunctionBeingNullChecked(condition); - final ifStatement = parent; - if (ifStatement.elseStatement != null) return null; - if (ifStatement.parent?.tryCast()?.elseStatement == - ifStatement) { - // ifStatement is an else-if - return null; - } - final thenStatement = ifStatement.thenStatement; - if (thenStatement is Block && thenStatement.statements.length == 1) { - if (_isMatchingConditionalPropFunctionCallStatement( - thenStatement.statements.single, propFunctionBeingNullChecked)) { - return thenStatement.statements.single as ExpressionStatement?; - } - } else if (thenStatement is ExpressionStatement) { - if (_isMatchingConditionalPropFunctionCallStatement( - thenStatement, propFunctionBeingNullChecked)) { - return thenStatement; - } - } - return null; - } - - bool _isMatchingConditionalPropFunctionCallStatement( - Statement statementWithinThenStatement, - SimpleIdentifier? propFunctionBeingNullChecked) { - if (statementWithinThenStatement is! ExpressionStatement) return false; - final expression = statementWithinThenStatement.expression; - if (expression is! FunctionExpressionInvocation) return false; - final fn = expression.function; - if (fn is! PropertyAccess) return false; - final target = fn.target; - if (target is! SimpleIdentifier) return false; - if (target.name != 'props') return false; - return fn.propertyName.staticElement?.declaration == - propFunctionBeingNullChecked?.staticElement?.declaration; - } - - /// Returns the identifier for the function that is being - /// null checked before being called. - SimpleIdentifier? _getPropFunctionBeingNullChecked( - BinaryExpression condition) { - if (condition.leftOperand is! PrefixedIdentifier) { - return null; - } - final leftOperand = condition.leftOperand as PrefixedIdentifier; - final prefix = leftOperand.prefix; - if (prefix.name != 'props') { - return null; - } - if (leftOperand.identifier.staticType is! FunctionType) { - return null; - } - if (condition.operator.stringValue != '!=' && - condition.operator.next?.keyword != Keyword.NULL) { - return null; - } - return leftOperand.identifier; - } - - @override - Future generatePatches() async { - _result = await context.getResolvedUnit(); - if (_result == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - _result!.unit.accept(this); - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/required_flux_props.dart b/lib/src/dart3_suggestors/null_safety_prep/required_flux_props.dart deleted file mode 100644 index 30108b0a..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/required_flux_props.dart +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright 2023 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/element.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/class_suggestor.dart'; - -/// Suggestor that adds required `store` and/or `actions` prop(s) to the -/// call-site of `FluxUiComponent` instances that omit them since version -/// 5.0.0 of over_react makes flux `store`/`actions` props required. -/// -/// In the case of a component that is rendered in a scope where a store/actions -/// instance is available, but simply not passed along to the component, those -/// instance(s) will be used as the value for `props.store`/`props.actions`, -/// even though the component itself may not make use of them internally. -/// -/// In the case of a component that is rendered in a scope where a store/actions -/// instance is not available, `null` will be used as the value for the prop(s). -class RequiredFluxProps extends RecursiveAstVisitor with ClassSuggestor { - ResolvedUnitResult? _result; - - static const fluxPropsMixinName = 'FluxUiPropsMixin'; - - @visibleForTesting - static String getTodoForPossiblyValidStoreVar(String fluxStoreVarName) { - return ' // TODO: There is a valid flux store value in scope that could be set here (`$fluxStoreVarName`). Should it be set?'; - } - - @override - visitCascadeExpression(CascadeExpression node) { - final cascadeWriteEl = node.staticType?.element; - if (cascadeWriteEl is! ClassElement) return; - const typesToIgnore = { - '_PanelTitleProps', - 'PanelTitleProps', - 'PanelTitleV2Props', - '_PanelToolbarProps', - 'PanelToolbarProps', - }; - if (typesToIgnore.contains(cascadeWriteEl.name)) { - return; - } - final isReturnedAsDefaultProps = node.ancestors - .whereType() - .firstOrNull - ?.name - .lexeme - .contains(RegExp(r'getDefaultProps|defaultProps')) ?? - false; - if (isReturnedAsDefaultProps) return; - - final maybeFluxUiPropsMixin = cascadeWriteEl.mixins - .singleWhereOrNull((e) => e.element.name == fluxPropsMixinName); - if (maybeFluxUiPropsMixin == null) return; - - final fluxActionsType = maybeFluxUiPropsMixin.typeArguments[0]; - final fluxStoreType = maybeFluxUiPropsMixin.typeArguments[1]; - - final cascadingAssignments = - node.cascadeSections.whereType(); - var storeAssigned = cascadingAssignments.any((cascade) { - final lhs = cascade.leftHandSide; - return lhs is PropertyAccess && lhs.propertyName.name == 'store'; - }); - var actionsAssigned = cascadingAssignments.any((cascade) { - final lhs = cascade.leftHandSide; - return lhs is PropertyAccess && lhs.propertyName.name == 'actions'; - }); - - if (!storeAssigned) { - storeAssigned = true; - final storeValue = - _getNameOfVarOrFieldInScopeWithType(node, fluxStoreType); - if (storeValue != null) { - final todoComment = getTodoForPossiblyValidStoreVar(storeValue); - yieldNewCascadeSection(node, '$todoComment\n..store = null'); - } else { - yieldNewCascadeSection(node, '..store = null'); - } - } - - if (!actionsAssigned) { - actionsAssigned = true; - final actionsValue = - _getNameOfVarOrFieldInScopeWithType(node, fluxActionsType) ?? 'null'; - yieldNewCascadeSection(node, '..actions = $actionsValue'); - } - } - - void yieldNewCascadeSection(CascadeExpression node, String newSection) { - final offset = node.target.end; - yieldPatch(newSection, offset, offset); - } - - @override - Future generatePatches() async { - _result = await context.getResolvedUnit(); - if (_result == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - _result!.unit.accept(this); - } -} - -class InScopeVariable { - final String name; - final DartType? type; - - InScopeVariable(this.name, this.type); -} - -String? _getNameOfVarOrFieldInScopeWithType(AstNode node, DartType type) { - if (type is DynamicType || type.isDartCoreNull) return null; - - final mostInScopeVariables = node.ancestors.expand((ancestor) sync* { - if (ancestor is FunctionDeclaration) { - // Function arguments - final element = ancestor.declaredElement; - if (element != null) { - yield* element.parameters.map((p) => InScopeVariable(p.name, p.type)); - } - } else if (ancestor is Block) { - // Variables declared in the block (function body, if/else block, etc.) - yield* ancestor.statements - .whereType() - .expand((d) => d.variables.variables) - .map((v) => InScopeVariable(v.name.lexeme, v.declaredElement?.type)); - } else if (ancestor is ClassDeclaration) { - // Class fields - final element = ancestor.declaredElement; - if (element != null) { - yield* element.fields.map((f) => InScopeVariable(f.name, f.type)); - } - } else if (ancestor is CompilationUnit) { - // Top-level variables - yield* ancestor.declarations - .whereType() - .expand((d) => d.variables.variables) - .map((v) => InScopeVariable(v.name.lexeme, v.declaredElement?.type)); - } - }); - - // Usually we'd grab typeSystem from the ResolvedUnitResult, but we don't have access to that - // in this class, so just get it from the compilation unit. - final typeSystem = - (node.root as CompilationUnit).declaredElement!.library.typeSystem; - bool isMatchingType(DartType? maybeMatchingType) => - maybeMatchingType != null && - maybeMatchingType is! DynamicType && - typeSystem.isAssignableTo(maybeMatchingType, type); - - final inScopeVarName = mostInScopeVariables - .firstWhereOrNull((v) => isMatchingType(v.type)) - ?.name; - - final componentScopePropDetector = _ComponentScopeFluxPropsDetector(); - // Find actions/store in props of class components - componentScopePropDetector.handlePotentialClassComponent( - node.thisOrAncestorOfType()); - // Find actions/store in props of fn components - componentScopePropDetector.handlePotentialFunctionComponent( - node.thisOrAncestorOfType()); - - final inScopePropName = - componentScopePropDetector.found.firstWhereOrNull((el) { - final maybeMatchingType = componentScopePropDetector.getAccessorType(el); - return maybeMatchingType?.element?.name == type.element?.name; - })?.name; - - if (inScopeVarName != null && inScopePropName != null) { - // TODO: Do we need to handle this edge case with something better than returning null? - // No way to determine which should be used - the scoped variable or the field on props - // so return null to avoid setting the incorrect value on the consumer's code. - return null; - } - - if (inScopePropName != null) { - return '${componentScopePropDetector.propsName}.${inScopePropName}'; - } - - return inScopeVarName; -} - -bool _isFnComponentDeclaration(Expression? varInitializer) => - varInitializer is MethodInvocation && - varInitializer.methodName.name.startsWith('uiF'); - -/// A visitor to detect store/actions values in a props class (supports both class and fn components) -class _ComponentScopeFluxPropsDetector { - final Map _foundWithMappedTypes; - - List get found => - _foundWithMappedTypes.keys.toList(); - - _ComponentScopeFluxPropsDetector() : _foundWithMappedTypes = {}; - - String _propsName = 'props'; - - /// The name of the function component props arg, or the class component `props` instance field. - String get propsName => _propsName; - - DartType? getAccessorType(PropertyAccessorElement el) => - _foundWithMappedTypes[el]; - - void _lookForFluxStoreAndActionsInPropsClass(Element? elWithProps) { - if (elWithProps is ClassElement) { - final fluxPropsEl = elWithProps.mixins.singleWhereOrNull( - (e) => e.element.name == RequiredFluxProps.fluxPropsMixinName); - - if (fluxPropsEl != null) { - final actionsType = fluxPropsEl.typeArguments[0]; - final storeType = fluxPropsEl.typeArguments[1]; - fluxPropsEl.accessors.forEach((a) { - final accessorTypeName = a.declaration.variable.type.element?.name; - if (accessorTypeName == 'ActionsT') { - _foundWithMappedTypes.putIfAbsent(a.declaration, () => actionsType); - } else if (accessorTypeName == 'StoresT') { - _foundWithMappedTypes.putIfAbsent(a.declaration, () => storeType); - } - }); - } - } - } - - /// Visit function components - void handlePotentialFunctionComponent(MethodInvocation? node) { - if (node == null) return; - if (!_isFnComponentDeclaration(node)) return; - - final nodeType = node.staticType; - if (nodeType is FunctionType) { - final propsArg = - node.argumentList.arguments.firstOrNull as FunctionExpression?; - final propsArgName = - propsArg?.parameters?.parameterElements.firstOrNull?.name; - if (propsArgName != null) { - _propsName = propsArgName; - } - _lookForFluxStoreAndActionsInPropsClass(nodeType.returnType.element); - } - } - - /// Visit composite (class) components - void handlePotentialClassComponent(ClassDeclaration? node) { - if (node == null) return; - final elWithProps = - node.declaredElement?.supertype?.typeArguments.singleOrNull?.element; - _lookForFluxStoreAndActionsInPropsClass(elWithProps); - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/state_mixin_suggestor.dart b/lib/src/dart3_suggestors/null_safety_prep/state_mixin_suggestor.dart deleted file mode 100644 index 284ceee8..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/state_mixin_suggestor.dart +++ /dev/null @@ -1,72 +0,0 @@ -// 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/visitor.dart'; -import 'package:analyzer/dart/element/element.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:analyzer/dart/ast/ast.dart'; - -import '../../util/class_suggestor.dart'; - -/// Suggestor to assist with preparations for null-safety by adding -/// nullability (`?`) hints to state field types. -/// -/// This is intended to be run after [ClassComponentRequiredInitialStateMigrator] -/// to make the rest of the state fields nullable. -class StateMixinSuggestor extends RecursiveAstVisitor - with ClassSuggestor { - @override - void visitVariableDeclaration(VariableDeclaration node) { - super.visitVariableDeclaration(node); - - final isStateClass = (node.declaredElement?.enclosingElement - ?.tryCast() - ?.allSupertypes - .any((s) => s.element.name == 'UiState') ?? - false); - if (!isStateClass) return; - - final fieldDeclaration = node.parentFieldDeclaration; - if (fieldDeclaration == null) return; - if (fieldDeclaration.isStatic) return; - if (fieldDeclaration.fields.isConst) return; - - final type = fieldDeclaration.fields.type; - if (type != null && - (requiredHintAlreadyExists(type) || nullableHintAlreadyExists(type))) { - return; - } - - // Make state field optional. - if (type != null) { - yieldPatch(nullableHint, type.end, type.end); - } - } - - @override - Future generatePatches() async { - final r = await context.getResolvedUnit(); - if (r == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - - // Don't make any updates if the file is already null safe. - if (r.libraryElement.isNonNullableByDefault) { - return; - } - r.unit.accept(this); - } -} 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 deleted file mode 100644 index 87a84c4f..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/use_ref_init_migration.dart +++ /dev/null @@ -1,61 +0,0 @@ -// 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'; -import 'package:collection/collection.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 ref1 = useRef(someNonNulLValue); -/// final ref2 = useRef(null); -/// // After -/// final ref1 = useRefInit(someNonNulLValue); -/// final ref2 = useRef(); -/// ``` -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') { - final argument = node.arguments.singleOrNull; - if (argument is NullLiteral) { - // Remove unnecessary null argument - yieldPatch('', argument.offset, argument.end); - } else { - yieldPatch('useRefInit', possibleInvocation.function.offset, - possibleInvocation.function.end); - } - } - } - } -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/utils/class_component_required_fields.dart b/lib/src/dart3_suggestors/null_safety_prep/utils/class_component_required_fields.dart deleted file mode 100644 index 71fc2ec3..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/utils/class_component_required_fields.dart +++ /dev/null @@ -1,203 +0,0 @@ -// 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/token.dart'; -import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:collection/collection.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/component_usage.dart'; -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import '../../../util/class_suggestor.dart'; -import '../../required_props/codemod/recommender.dart'; -import '../analyzer_plugin_utils.dart'; - -/// A class shared by the suggestors that manage defaultProps/initialState. -abstract class ClassComponentRequiredFieldsMigrator< - Assignment extends PropOrStateAssignment> - extends RecursiveAstVisitor with ClassSuggestor { - final String relevantGetterName; - final String relevantMethodName; - - /// When set to a version that opts-in to Dart's null safety feature, - /// the `late` / `?` type modifiers will be actual modifiers rather - /// than commented hints. This should only be done using an explicit opt-in - /// flag from the executable as most consumers that have migrated to null-safety - /// will have already run this script prior to the null safety migration and thus - /// the `/*late*/` / `/*?*/` hints will already be converted to actual modifiers. - final Version? sdkVersion; - - ClassComponentRequiredFieldsMigrator( - this.relevantGetterName, this.relevantMethodName, - [this.sdkVersion]); - - late ResolvedUnitResult result; - final Set fieldData = {}; - - void patchFieldDeclarations( - Iterable Function(InterfaceElement) getAll, - Iterable cascadedDefaultPropsOrInitialState, - CascadeExpression node, - [PropRequirednessRecommender? _propRequirednessRecommender]) { - for (final field in cascadedDefaultPropsOrInitialState) { - final isDefaultedToNull = - field.node.rightHandSide.staticType!.isDartCoreNull; - final fieldEl = (field.node.writeElement! as PropertyAccessorElement) - .variable as FieldElement; - final propsOrStateElement = - node.staticType?.typeOrBound.tryCast()?.element; - if (propsOrStateElement == null) continue; - final fieldDeclaration = _getFieldDeclaration(getAll, - propsOrStateElement: propsOrStateElement, fieldName: fieldEl.name); - // The field declaration is likely in another file which our logic currently doesn't handle. - // In this case, don't add an entry to `fieldData`. - if (fieldDeclaration == null) continue; - final element = fieldDeclaration.declaredElement; - - // Don't set as required if the prop is publicly exported. - if (_propRequirednessRecommender != null && element is FieldElement) { - final isPublic = _propRequirednessRecommender - .isPropsPublicForMixingIn(element.enclosingElement); - if (isPublic) continue; - } - - fieldData.add(DefaultedOrInitializedDeclaration( - fieldDeclaration, fieldEl, isDefaultedToNull)); - } - - fieldData.where((data) => !data.patchedDeclaration).forEach((data) { - data.patch(yieldPatch, sdkVersion: sdkVersion); - }); - } - - VariableDeclaration? _getFieldDeclaration( - Iterable Function(InterfaceElement) getAll, - {required InterfaceElement propsOrStateElement, - required String fieldName}) { - // For component1 boilerplate its possible that `fieldEl` won't be found using `lookUpVariable` below - // since its `enclosingElement` will be the generated abstract mixin. So we'll use the provided `getAll` fn to - // cross reference the return value with the `fieldName`to locate the actual prop/state field declaration we want to patch. - final siblingFields = getAll(propsOrStateElement); - final matchingField = - siblingFields.singleWhereOrNull((element) => element.name == fieldName); - if (matchingField == null) return null; - - // NOTE: result.unit will only work if the declaration of the field is in this file - return lookUpVariable(matchingField, result.unit); - } - - @override - Future generatePatches() async { - // Clear so we don't share state across CompilationUnits - fieldData.clear(); - final r = await context.getResolvedUnit(); - if (r == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - result = r; - r.unit.accept(this); - } -} - -class DefaultedOrInitializedDeclaration { - final VariableDeclaration fieldDecl; - final FieldElement fieldEl; - final bool isDefaultedToNull; - final String name; - - DefaultedOrInitializedDeclaration( - this.fieldDecl, this.fieldEl, this.isDefaultedToNull) - : _patchedDeclaration = false, - name = '${fieldDecl.name.lexeme}'; - - /// Whether the declaration has been patched with the late / nullable hints. - bool get patchedDeclaration => _patchedDeclaration; - bool _patchedDeclaration; - - void patch( - void Function(String updatedText, int startOffset, [int? endOffset]) - handleYieldPatch, - {Version? sdkVersion}) { - final parent = fieldDecl.parent! as VariableDeclarationList; - final keyword = parent.keyword; // e.g. var - final type = parent.type; - final fieldNameToken = fieldDecl.name; - if (type != null && - requiredHintAlreadyExists(type) && - (nullableHintAlreadyExists(type) || - nonNullableHintAlreadyExists(type))) { - // Short circuit - it has already been patched - _patchedDeclaration = true; - return; - } - - String? late = - type != null && requiredHintAlreadyExists(type) ? null : '/*late*/'; - String nullability = ''; - if (isDefaultedToNull) { - if (type == null || !nullableHintAlreadyExists(type)) { - nullability = nullableHint; - } - } else { - if (type == null || !nonNullableHintAlreadyExists(type)) { - nullability = nonNullableHint; - } - } - - if (sdkVersion != null && - VersionRange(min: Version.parse('2.12.0')).allows(sdkVersion)) { - if (late != null) { - // If the repo has opted into null safety, patch with the real thing instead of hints - late = 'late'; - // Unless it already has the late keyword applied - if ((type?.parent as VariableDeclarationList?)?.lateKeyword is Token) { - late = null; - } - } - - nullability = isDefaultedToNull ? '?' : ''; - - if (late == null && nullability.isEmpty) { - // Short circuit - it has already been patched - _patchedDeclaration = true; - return; - } - } - - late = late ?? ''; - // dynamic added if type is null b/c we gotta have a type to add the nullable `?`/`!` hints to - even if for some reason the prop/state decl. has no left side type. - final patchedType = - type == null ? 'dynamic$nullability' : '${type.toSource()}$nullability'; - final startOffset = - type?.offset ?? keyword?.offset ?? fieldNameToken.offset; - handleYieldPatch('$late $patchedType ', startOffset, fieldNameToken.offset); - - _patchedDeclaration = true; - } - - @override - bool operator ==(Object other) { - return other is DefaultedOrInitializedDeclaration && - other.fieldEl == this.fieldEl; - } - - @override - int get hashCode => this.fieldEl.hashCode; -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart b/lib/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart deleted file mode 100644 index 72cc40ea..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart +++ /dev/null @@ -1,44 +0,0 @@ -// 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:over_react_codemod/src/util.dart'; - -/// Whether the nullability hint already exists after [type]. -bool nullableHintAlreadyExists(TypeAnnotation type) { - // The nullability hint will follow the type so we need to check the next token to find the comment if it exists. - final commentsPrecedingType = type.endToken.next?.precedingComments?.value(); - return commentsPrecedingType?.contains(nullableHint) ?? false; -} - -const nullableHint = '/*?*/'; - -/// Whether the non-nullable hint already exists after [type]. -bool nonNullableHintAlreadyExists(TypeAnnotation type) { - // The nullability hint will follow the type so we need to check the next token to find the comment if it exists. - final commentsPrecedingType = type.endToken.next?.precedingComments?.value(); - return commentsPrecedingType?.contains(nonNullableHint) ?? false; -} - -const nonNullableHint = '/*!*/'; - -const lateHint = '/*late*/'; - -/// Whether the late hint already exists before [type] -bool requiredHintAlreadyExists(TypeAnnotation type) { - // Since the `/*late*/` comment is possibly adjacent to the prop declaration's doc comments, - // we have to recursively traverse the `precedingComments` in order to determine if the `/*late*/` - // comment actually exists. - return allCommentsForNode(type).any((t) => t.value() == lateHint); -} diff --git a/lib/src/dart3_suggestors/null_safety_prep/utils/props_utils.dart b/lib/src/dart3_suggestors/null_safety_prep/utils/props_utils.dart deleted file mode 100644 index a902342c..00000000 --- a/lib/src/dart3_suggestors/null_safety_prep/utils/props_utils.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:over_react_codemod/src/util/component_usage.dart'; - -/// Returns a list of props from [cascade]. -Iterable getCascadedProps(CascadeExpression cascade) { - return cascade.cascadeSections - .whereType() - .where((assignment) => assignment.leftHandSide is PropertyAccess) - .map((assignment) => PropAssignment(assignment)) - .where((prop) => prop.node.writeElement?.displayName != null); -} diff --git a/lib/src/dart3_suggestors/required_props/bin/aggregate.dart b/lib/src/dart3_suggestors/required_props/bin/aggregate.dart deleted file mode 100644 index 1c88e9a6..00000000 --- a/lib/src/dart3_suggestors/required_props/bin/aggregate.dart +++ /dev/null @@ -1,112 +0,0 @@ -// 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:async'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:collection/collection.dart'; -import 'package:io/io.dart'; -import 'package:logging/logging.dart'; -import '../collect/aggregate.dart'; -import '../collect/logging.dart'; - -/// Aggregates individual data files, like what the collect command does, -/// but as a standalone command. -/// -/// This is leftover from before the collect command also aggregated data, -/// and is not publicly exposed, but is left in place just in case for -/// debugging purposes and potential future use. -/// -/// Also outputs some additional statistics. -Future main(List args) async { - final argParser = ArgParser() - ..addFlag('help', help: 'Print this usage information', negatable: false) - ..addOption( - 'output', - abbr: 'o', - help: 'The file to write output to.', - valueHelp: 'path', - defaultsTo: defaultAggregatedOutputFile, - ); - final parsedArgs = argParser.parse(args); - if (parsedArgs['help'] as bool) { - print(argParser.usage); - exit(ExitCode.success.code); - } - final outputFile = parsedArgs['output']! as String; - final filesToAggregate = parsedArgs.rest; - if (filesToAggregate.isEmpty) { - print('Must specify files to aggregate.\n${argParser.usage}'); - exit(ExitCode.usage.code); - } - - initLogging(); - final logger = Logger('prop_requiredness_aggregate'); - - logger.info('Loading results from files specified in arguments...'); - final allResults = loadResultFiles(filesToAggregate); - - { - // Gather some stats on how often different builder types show up. - final allUsages = allResults.expand((r) => r.usages); - - final countsByBuilderType = - allUsages.countBy((u) => u.usageBuilderType.name); - File('counts_by_builder_type.json') - .writeAsStringSync(jsonEncodeIndented(countsByBuilderType)); - - final countsByBuilderTypeByMixin = allUsages - .multiGroupListsBy((u) => u.mixinData.map((e) => e.mixinId)) - .map((mixinId, usages) => - MapEntry(mixinId, usages.countBy((u) => u.usageBuilderType.name))); - File('counts_by_builder_type_by_mixin.json') - .writeAsStringSync(jsonEncodeIndented(countsByBuilderTypeByMixin)); - } - - logger.info('Aggregating data...'); - final aggregated = aggregateData(allResults); - logger.info('Done.'); - - // logger.fine('Props mixins with the same name:'); - // final mixinIdsByName = aggregated.mixinMetadata.mixinNamesById.keysByValues(); - // mixinIdsByName.forEach((name, mixinIds) { - // if (mixinIds.length > 1) logger.fine('$name: ${mixinIds.map((id) => '\n - $id').join('')}'); - // }); - - File(outputFile).writeAsStringSync(jsonEncodeIndented(aggregated)); - logger.info('Wrote JSON results to $outputFile'); -} - -extension on Iterable { - Map countBy(T Function(E) getBucket) { - final counts = {}; - for (final element in this) { - final bucket = getBucket(element); - counts[bucket] = (counts[bucket] ?? 0) + 1; - } - return counts; - } - - /// Like [groupListsBy] but allows elements to be added to multiple groups. - Map> multiGroupListsBy(Iterable Function(E) keysOf) { - final groups = >{}; - for (final element in this) { - for (final key in keysOf(element)) { - groups.putIfAbsent(key, () => []).add(element); - } - } - return groups; - } -} diff --git a/lib/src/dart3_suggestors/required_props/bin/codemod.dart b/lib/src/dart3_suggestors/required_props/bin/codemod.dart deleted file mode 100644 index c69fe738..00000000 --- a/lib/src/dart3_suggestors/required_props/bin/codemod.dart +++ /dev/null @@ -1,180 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:args/command_runner.dart'; -import 'package:codemod/codemod.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/required_props/codemod/required_props_suggestor.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/args.dart'; -import 'package:over_react_codemod/src/util/command_runner.dart'; -import 'package:over_react_codemod/src/util/package_util.dart'; - -import '../../null_safety_prep/class_component_required_default_props.dart'; -import '../codemod/recommender.dart'; -import '../collect/aggregated_data.sg.dart'; - -abstract class _Options { - static const propRequirednessData = 'prop-requiredness-data'; - static const privateRequirednessThreshold = 'private-requiredness-threshold'; - static const privateMaxAllowedSkipRate = 'private-max-allowed-skip-rate'; - static const publicRequirednessThreshold = 'public-requiredness-threshold'; - static const publicMaxAllowedSkipRate = 'public-max-allowed-skip-rate'; - - static const all = { - propRequirednessData, - privateRequirednessThreshold, - privateMaxAllowedSkipRate, - publicRequirednessThreshold, - publicMaxAllowedSkipRate - }; -} - -abstract class _Flags { - static const trustRequiredAnnotations = 'trust-required-annotations'; - static const all = { - trustRequiredAnnotations, - }; -} - -class CodemodCommand extends Command { - @override - String get description => - "Adds null safety migrator hints to OverReact props using prop requiredness data from 'collect' command."; - - @override - String get name => 'codemod'; - - @override - String get invocation => '$invocationPrefix []'; - - @override - String get usageFooter => ''' -\nInstructions -============ - -1. First, run the 'collect' command to collect data on usages of props declared - in your package (see that command's --help for instructions). - - $parentInvocationPrefix collect --help - -2. Run this command within the package you want to update: - - $invocationPrefix - -3. Inspect the TODO comments left over from the codemod. If you want to adjust - any thresholds or re-collect data, discard changes before re-running the codemod. - -4. Commit the changes made by the codemod. - -5. Proceed with using the Dart null safety migrator tool to migrate your code. - -6. Review TODO comments, adjusting requiredness if desired. You can use a - find-replace with the following regex to remove them: - - ${r'^ *// TODO\(orcm.required_props\):.+(?:\n *// .+)*'} -'''; - - CodemodCommand() { - argParser - ..addOption(_Options.propRequirednessData, - help: - "The file containing prop requiredness data, collected via the 'over_react_codemod:collect' command.", - defaultsTo: 'prop_requiredness.json') - ..addFlag(_Flags.trustRequiredAnnotations, - defaultsTo: true, - help: - 'Whether to migrate @requiredProp and `@nullableRequiredProp` props to late required, regardless of usage data.' - '\nNote that @requiredProp has no effect on function components, so these annotations may be incorrect.') - ..addOption(_Options.privateRequirednessThreshold, - defaultsTo: (0.95).toString(), - help: - 'The minimum rate (0.0-1.0) a private prop must be set to be considered required.') - ..addOption(_Options.privateMaxAllowedSkipRate, - defaultsTo: (0.2).toString(), - help: - 'The maximum allowed rate (0.0-1.0) of dynamic usages of private mixins, for which data collection was skipped.' - '\nIf above this, all props in a mixin will be made optional (with a TODO comment).') - ..addOption(_Options.publicRequirednessThreshold, - defaultsTo: (1).toString(), - help: - 'The minimum rate (0.0-1.0) a public prop must be set to be considered required.') - ..addOption(_Options.publicMaxAllowedSkipRate, - defaultsTo: (0.05).toString(), - help: - 'The maximum allowed rate (0.0-1.0) of dynamic usages of public mixins, for which data collection was skipped.' - '\nIf above this, all props in a mixin will be made optional (with a TODO comment).'); - - argParser.addSeparator('Codemod options'); - addCodemodArgs(argParser); - } - - @override - Future run() async { - final parsedArgs = this.argResults!; - final propRequirednessDataFile = - parsedArgs[_Options.propRequirednessData]! as String; - final codemodArgs = removeFlagArgs( - removeOptionArgs(parsedArgs.arguments, _Options.all), _Flags.all); - - final packageRoot = findPackageRootFor('.'); - await runPubGetIfNeeded(packageRoot); - final dartPaths = allDartPathsExceptHiddenAndGenerated(); - - final results = PropRequirednessResults.fromJson( - jsonDecode(File(propRequirednessDataFile).readAsStringSync())); - final recommender = PropRequirednessRecommender( - results, - privateRequirednessThreshold: - parsedArgs.argValueAsNumber(_Options.privateRequirednessThreshold), - privateMaxAllowedSkipRate: - parsedArgs.argValueAsNumber(_Options.privateMaxAllowedSkipRate), - publicRequirednessThreshold: - parsedArgs.argValueAsNumber(_Options.publicRequirednessThreshold), - publicMaxAllowedSkipRate: - parsedArgs.argValueAsNumber(_Options.publicMaxAllowedSkipRate), - ); - - exitCode = await runInteractiveCodemodSequence( - dartPaths, - [ - ClassComponentRequiredDefaultPropsMigrator(null, recommender), - ], - defaultYes: true, - args: codemodArgs, - additionalHelpOutput: argParser.usage, - ); - - exitCode = await runInteractiveCodemodSequence( - dartPaths, - [ - RequiredPropsSuggestor( - recommender, - trustRequiredAnnotations: - parsedArgs[_Flags.trustRequiredAnnotations] as bool, - ), - ], - defaultYes: true, - args: codemodArgs, - additionalHelpOutput: argParser.usage, - ); - } -} - -extension on ArgResults { - num argValueAsNumber(String name) => num.parse(this[name]); -} diff --git a/lib/src/dart3_suggestors/required_props/bin/collect.dart b/lib/src/dart3_suggestors/required_props/bin/collect.dart deleted file mode 100644 index 0d701836..00000000 --- a/lib/src/dart3_suggestors/required_props/bin/collect.dart +++ /dev/null @@ -1,315 +0,0 @@ -// 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:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:logging/logging.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/required_props/collect/aggregate.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/required_props/collect/analysis.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/required_props/collect/collect.dart'; -import 'package:over_react_codemod/src/util/command.dart'; -import 'package:over_react_codemod/src/util/command_runner.dart'; -import 'package:package_config/package_config.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import '../collect/collected_data.sg.dart'; -import '../collect/logging.dart'; -import '../collect/package/parse_spec.dart'; -import '../collect/package/spec.dart'; -import '../collect/package/version_manager.dart'; - -class CollectCommand extends Command { - @override - String get description => - 'Collects requiredness data for all OverReact props based on usages in the specified packages and all their transitive dependencies.'; - - @override - String get name => 'collect'; - - @override - String get invocation => - '$invocationPrefix [] [...]'; - - @override - String get usageFooter => - '\n$packageSpecFormatsHelpText\n\n$_usageInstructions'; - - String get _usageInstructions => ''' -Instructions -============ - -1. First, identify the least-common consumer(s) of OverReact components exposed by your package. - - (If all your package's components are private, you can skip the rest of this step, - step and just use your package). - - For example, say we're dealing with package A, which is directly consumed by - packages B, E, and F, and so on: - - ${r'A---B---C---D'} - ${r'|\ /'} - ${r'| E----'} - ${r'\'} - ${r' F---G---H'} - - The least-common consumers would be C (covers both B and E) and F, so we'd run: - - $invocationPrefix pub@…:C pub@…:F - - Note: if F were to re-export members of A, which could potentially get used - in G, we'd do G instead of F. - - $invocationPrefix pub@…:C pub@…:G - - Alternatively, we could just run on D and H from the start, but if those - packages include more transitive dependencies, then the analysis step of the - collection process will take a bit longer. - -2. If step 1 yielded more than one package, make sure all of them can resolve to - the latest version of your package. - - If they can't, then data may be missing for recently-added props, or could be - incorrect if props in your package were moved to different files. - - If you're not sure, try either: - - Cloning those packages, ensuring they resolve to the latest locally, - and providing them as local path package specs. - - Running the command and verifying the package versions in the command output - line up. - -3. Run the '$invocationPrefix' command with the packages from step 1, using - one of the package specifier formats listed above. - -4. Use the `codemod` command within the package you want to update - (see that command's --help for instructions): - - cd my_package - $parentInvocationPrefix codemod --help -'''; - - CollectCommand() { - argParser - ..addOption( - 'raw-data-output-directory', - help: 'An optional directory to output raw usage data file to.', - ) - ..addOption( - 'output', - abbr: 'o', - help: 'The file to write aggregated results to.', - valueHelp: 'path', - defaultsTo: defaultAggregatedOutputFile, - ) - ..addFlag( - 'verbose', - defaultsTo: false, - negatable: false, - help: 'Enable verbose output.', - ); - } - - @override - FutureOr? run() async { - final parsedArgs = this.argResults!; - - final aggregatedOutputFile = parsedArgs['output']! as String; - final verbose = parsedArgs['verbose']! as bool; - - var rawDataOutputDirectory = - parsedArgs['raw-data-output-directory'] as String?; - - final packageSpecStrings = parsedArgs.rest; - if (packageSpecStrings.isEmpty) { - usageException('Must specify package(s).'); - } - - initLogging(verbose: verbose); - - late final versionManager = PackageVersionManager.persistentSystemTemp(); - - final logger = Logger('prop_requiredness.collect'); - logger.info('Parsing/initializing package specs...'); - final packages = await Future.wait(packageSpecStrings.map((arg) { - return parsePackageSpec(arg, getVersionManager: () => versionManager); - })); - - logger - .info('Done. Package specs: ${packages.map((p) => '\n- $p').join('')}'); - - logger.info('Processing packages...'); - if (rawDataOutputDirectory != null) { - logger.info( - "Writing raw usage data to directory '$rawDataOutputDirectory'..."); - } - - final allResults = []; - - final processedPackages = {}; - for (final packageSpec in packages) { - logger.info('Processing $packageSpec...'); - final packageName = packageSpec.packageName; - if (processedPackages.contains(packageName)) { - throw Exception('Already processed $packageName'); - } - - final result = (await collectDataForPackage( - packageSpec, - processDependencyPackages: true, - skipIfAlreadyCollected: false, - skipIfNoUsages: false, - packageFilter: (p) => !processedPackages.contains(p.name), - outputDirectory: rawDataOutputDirectory, - ))!; - allResults.add(result); - logger.fine(result); - for (final otherPackage in result.results.otherPackageNames) { - if (processedPackages.contains(otherPackage)) { - throw Exception('$otherPackage was double-processed'); - } - processedPackages.add(otherPackage); - } - processedPackages.add(packageName); - } - logger.info('Done!'); - logger.fine('All results:\n${allResults.map((r) => '- $r\n').join('')}'); - logger.info( - 'All result files: ${allResults.map((r) => r.outputFilePath).join(' ')}'); - - logger.info('Aggregating raw usage data...'); - - final aggregated = aggregateData(allResults.map((r) => r.results).toList()); - - File(aggregatedOutputFile) - ..parent.createSync(recursive: true) - ..writeAsStringSync(jsonEncodeIndented(aggregated)); - logger.info( - 'Wrote aggregated prop requiredness data to ${aggregatedOutputFile}'); - } -} - -final jsonEncodeIndented = const JsonEncoder.withIndent(' ').convert; - -class CollectDataForPackageResult { - final PackageResults results; - final String? outputFilePath; - - CollectDataForPackageResult({ - required this.results, - required this.outputFilePath, - }); - - @override - String toString() => 'CollectDataForPackageResult(${{ - 'outputFilePath': outputFilePath, - 'results.otherPackageNames': results.otherPackageNames.toList(), - }})'; -} - -Future collectDataForPackage( - PackageSpec package, { - bool processDependencyPackages = false, - bool Function(Package)? packageFilter, - bool skipIfAlreadyCollected = true, - bool skipIfNoUsages = true, - String? outputDirectory, -}) async { - final rootPackageName = package.packageName; - final logger = Logger('prop_requiredness.${package.packageAndVersionId}'); - - File? outputFile; - if (outputDirectory != null) { - outputFile = File(p.normalize( - p.join(outputDirectory, '${package.packageAndVersionId}.json'))); - - if (skipIfAlreadyCollected && outputFile.existsSync()) { - final existingResults = tryParseResults(outputFile.readAsStringSync()); - if (existingResults != null && - existingResults.dataVersion == PackageResults.latestDataVersion) { - logger.info('Skipping since data already exists: ${outputFile.path}'); - return CollectDataForPackageResult( - results: existingResults, - outputFilePath: outputFile.path, - ); - } - } - } - - final packageInfo = await getPackageInfo(package); - // Heuristic to help filter out packages that don't contain over_react component usages, - // so we don't have to spend time resolving them. - if (skipIfNoUsages && - !packageInfo.libFiles - .any((l) => File(l).readAsStringSync().contains(')('))) { - logger.fine( - "Skipping package $rootPackageName since it doesn't look like it contains over_react usages"); - return null; - } - - logger.info('Performing pub upgrade to get newer versions of packages...'); - // Get latest dependencies, to get latest versions of other packages. - await runCommandAndThrowIfFailed('dart', ['pub', 'upgrade'], - workingDirectory: packageInfo.root); - - final packageVersionDescriptionsByName = { - rootPackageName: package.sourceDescription, - }; - final pubspecLock = loadYaml( - File(p.join(packageInfo.root, 'pubspec.lock')).readAsStringSync()) as Map; - (pubspecLock['packages'] as Map) - .cast() - .forEach((packageName, info) { - final version = info['version'] as String?; - if (version != null) { - packageVersionDescriptionsByName.putIfAbsent(packageName, () => version); - } - }); - packageVersionDescriptionsByName[rootPackageName] = package.sourceDescription; - logger.info('Package versions: ${packageVersionDescriptionsByName}'); - - logger.info("Analyzing and collecting raw usage data..."); - final units = getResolvedLibUnitsForPackage(package, - includeDependencyPackages: processDependencyPackages, - packageFilter: packageFilter); - - final results = await collectDataForUnits( - units, - rootPackageName: rootPackageName, - allowOtherPackageUnits: processDependencyPackages, - ); - results.packageVersionDescriptionsByName - .addAll(packageVersionDescriptionsByName); - - if (outputFile != null) { - outputFile.parent.createSync(recursive: true); - outputFile.writeAsStringSync(jsonEncode(results)); - logger.fine('Wrote data to ${outputFile.path}'); - } - - return CollectDataForPackageResult( - results: results, - outputFilePath: outputFile?.path, - ); -} - -dynamic tryParseJson(String content) { - try { - return jsonDecode(content); - } catch (_) { - return null; - } -} diff --git a/lib/src/dart3_suggestors/required_props/codemod/recommender.dart b/lib/src/dart3_suggestors/required_props/codemod/recommender.dart deleted file mode 100644 index 79a38f09..00000000 --- a/lib/src/dart3_suggestors/required_props/codemod/recommender.dart +++ /dev/null @@ -1,168 +0,0 @@ -// 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/element/element.dart'; -import '../collect/aggregated_data.sg.dart'; -import '../collect/util.dart'; - -/// A class that can provide recommendations for prop requiredness based on -/// [PropRequirednessResults] data. -class PropRequirednessRecommender { - final PropRequirednessResults _propRequirednessResults; - - final num privateRequirednessThreshold; - final num privateMaxAllowedSkipRate; - final num publicRequirednessThreshold; - final num publicMaxAllowedSkipRate; - - PropRequirednessRecommender( - this._propRequirednessResults, { - required this.privateRequirednessThreshold, - required this.privateMaxAllowedSkipRate, - required this.publicRequirednessThreshold, - required this.publicMaxAllowedSkipRate, - }) { - ({ - 'privateRequirednessThreshold': privateRequirednessThreshold, - 'privateMaxAllowedSkipRate': privateMaxAllowedSkipRate, - 'publicRequirednessThreshold': publicRequirednessThreshold, - 'publicMaxAllowedSkipRate': publicMaxAllowedSkipRate, - }).forEach((name, value) { - _validateWithinRange(value, name: name, min: 0, max: 1); - }); - } - - PropRecommendation? getRecommendation(FieldElement propField) { - final propName = propField.name; - - final mixinResults = _getMixinResult(propField.enclosingElement); - if (mixinResults == null) return null; - - final propResults = mixinResults.propResultsByName[propName]; - if (propResults == null) return null; - - final skipRateReason = _getMixinSkipRateReason(mixinResults); - if (skipRateReason != null) { - return PropRecommendation.optional(skipRateReason); - } - - final totalRequirednessRate = propResults.totalRate; - - final isPublic = mixinResults.visibility.isPublicForUsages; - final requirednessThreshold = - isPublic ? publicRequirednessThreshold : privateRequirednessThreshold; - - if (totalRequirednessRate < requirednessThreshold) { - final reason = RequirednessThresholdOptionalReason(); - return PropRecommendation.optional(reason); - } else { - return const PropRecommendation.required(); - } - } - - MixinResult? _getMixinResult(Element propsElement) { - final packageName = getPackageName(propsElement.source!.uri); - final propsId = uniqueElementId(propsElement); - return _propRequirednessResults.mixinResultsByIdByPackage[packageName] - ?[propsId]; - } - - bool isPropsPublicForMixingIn(Element propsElement) => - _getMixinResult(propsElement)?.visibility.isPublicForMixingIn ?? false; - - SkipRateOptionalReason? _getMixinSkipRateReason(MixinResult mixinResults) { - final skipRate = mixinResults.usageSkipRate; - - final isPublic = mixinResults.visibility.isPublicForUsages; - final maxAllowedSkipRate = - isPublic ? publicMaxAllowedSkipRate : privateMaxAllowedSkipRate; - - return skipRate > maxAllowedSkipRate - ? SkipRateOptionalReason( - skipRate: skipRate, - maxAllowedSkipRate: maxAllowedSkipRate, - isPublic: isPublic) - : null; - } - - SkipRateOptionalReason? getMixinSkipRateReasonForElement( - Element propsElement) { - final mixinResults = _getMixinResult(propsElement); - if (mixinResults == null) return null; - - return _getMixinSkipRateReason(mixinResults); - } -} - -void _validateWithinRange(num value, - {required num min, required num max, required String name}) { - if (value < min || value > max) { - throw ArgumentError.value( - value, name, 'must be between $min and $max (inclusive)'); - } -} - -extension on Visibility { - bool get isPublicForUsages { - switch (this) { - case Visibility.public: - case Visibility.indirectlyPublic: - case Visibility.unknown: - return true; - case Visibility.private: - return false; - } - } - - // ignore: unused_element - bool get isPublicForMixingIn { - switch (this) { - case Visibility.public: - case Visibility.unknown: - return true; - case Visibility.indirectlyPublic: - case Visibility.private: - return false; - } - } -} - -class PropRecommendation { - final bool isRequired; - final OptionalReason? reason; - - const PropRecommendation.required() - : isRequired = true, - reason = null; - - const PropRecommendation.optional(this.reason) : isRequired = false; -} - -abstract class OptionalReason {} - -class SkipRateOptionalReason extends OptionalReason { - final num skipRate; - final num maxAllowedSkipRate; - final bool isPublic; - - SkipRateOptionalReason({ - required this.skipRate, - required this.maxAllowedSkipRate, - required this.isPublic, - }); -} - -class RequirednessThresholdOptionalReason extends OptionalReason { - RequirednessThresholdOptionalReason(); -} diff --git a/lib/src/dart3_suggestors/required_props/codemod/required_props_suggestor.dart b/lib/src/dart3_suggestors/required_props/codemod/required_props_suggestor.dart deleted file mode 100644 index d89c81cf..00000000 --- a/lib/src/dart3_suggestors/required_props/codemod/required_props_suggestor.dart +++ /dev/null @@ -1,195 +0,0 @@ -// 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/token.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:analyzer/dart/element/element.dart'; -import 'package:collection/collection.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/utils/hint_detection.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/class_suggestor.dart'; - -import 'recommender.dart'; - -const _todoWithPrefix = 'TODO(orcm.required_props)'; - -class RequiredPropsSuggestor extends RecursiveAstVisitor - with ClassSuggestor { - final PropRequirednessRecommender _propRequirednessRecommender; - final bool _trustRequiredAnnotations; - - RequiredPropsSuggestor( - this._propRequirednessRecommender, { - required bool trustRequiredAnnotations, - }) : _trustRequiredAnnotations = trustRequiredAnnotations; - - @override - Future generatePatches() async { - final result = await context.getResolvedUnit(); - if (result == null) { - throw Exception( - 'Could not get resolved result for "${context.relativePath}"'); - } - - // Don't make any updates if the file is already null safe. - if (result.libraryElement.isNonNullableByDefault) { - return; - } - result.unit.accept(this); - } - - @override - void visitMixinDeclaration(MixinDeclaration node) { - super.visitMixinDeclaration(node); - handleClassOrMixinElement(node, node.declaredElement); - } - - @override - void visitClassDeclaration(ClassDeclaration node) { - super.visitClassDeclaration(node); - handleClassOrMixinElement(node, node.declaredElement); - } - - void handleClassOrMixinElement( - NamedCompilationUnitMember node, InterfaceElement? element) { - if (element == null) return null; - - // Add a comment to let consumers know that we didn't have good enough data - // to make requiredness decision. - final skipReason = - _propRequirednessRecommender.getMixinSkipRateReasonForElement(element); - if (skipReason != null) { - String formatAsPercent(num number) => - '${(number * 100).toStringAsFixed(0)}%'; - - final skipRatePercent = formatAsPercent(skipReason.skipRate); - final maxAllowedSkipRatePercent = - formatAsPercent(skipReason.maxAllowedSkipRate); - - final commentContents = - "$_todoWithPrefix: This codemod couldn't reliably determine requiredness for these props" - "\n because $skipRatePercent of usages of components with these props" - " (> max allowed $maxAllowedSkipRatePercent for ${skipReason.isPublic ? 'public' : 'private'} props)" - "\n either contained forwarded props or were otherwise too dynamic to analyze." - "\n It may be possible to upgrade some from optional to required, with some manual inspection and testing."; - - final offset = node.firstTokenAfterCommentAndMetadata.offset; - yieldPatch(lineComment(commentContents), offset, offset); - } - } - - @override - void visitVariableDeclaration(VariableDeclaration node) { - super.visitVariableDeclaration(node); - - final fieldDeclaration = node.parentFieldDeclaration; - if (fieldDeclaration == null) return; - if (fieldDeclaration.isStatic) return; - if (fieldDeclaration.fields.isConst) return; - - final element = node.declaredElement; - if (element is! FieldElement) return; - - final type = fieldDeclaration.fields.type; - if (type != null && - (requiredHintAlreadyExists(type) || nullableHintAlreadyExists(type))) { - return; - } - - void yieldLateHintPatch() { - // Don't unnecessarily annotate it as non-nullable; - // let the migrator tool do that. - final offset = fieldDeclaration.firstTokenAfterCommentAndMetadata.offset; - yieldPatch('$lateHint ', offset, offset); - } - - void yieldOptionalHintPatch() { - if (type != null) { - yieldPatch(nullableHint, type.end, type.end); - } - } - - final requiredPropAnnotation = fieldDeclaration.metadata.firstWhereOrNull( - (m) => const {'requiredProp', 'nullableRequiredProp'} - .contains(m.name.name)); - - if (requiredPropAnnotation != null) { - // Always remove the annotation, since it can't be combined with late required props. - yieldPatch( - '', - requiredPropAnnotation.offset, - // Patch the whitespace up until the next token/comment, so that we take - // any newline along with this annotation. - requiredPropAnnotation.endToken.nextTokenOrCommentOffset ?? - requiredPropAnnotation.end); - - if (_trustRequiredAnnotations) { - yieldLateHintPatch(); - return; - } - } - - final recommendation = - _propRequirednessRecommender.getRecommendation(element); - - // No data; either not a prop, it's never actually set on any non-skipped usages, or our data is outdated. - if (recommendation == null) { - final skipReasonForEnclosingClass = _propRequirednessRecommender - .getMixinSkipRateReasonForElement(element.enclosingElement); - - final isPropsClass = skipReasonForEnclosingClass != null || - (node.declaredElement?.enclosingElement - ?.tryCast() - ?.allSupertypes - .any((s) => s.element.name == 'UiProps') ?? - false); - if (isPropsClass) { - // Only comment about missing data if we're not already making this optional - // because the class was skipped. - if (skipReasonForEnclosingClass == null) { - final commentContents = - "$_todoWithPrefix: No data for prop; either it's never set," - " all places it was set were on dynamic usages," - " or requiredness data was collected on a version before this prop was added."; - final offset = - fieldDeclaration.firstTokenAfterCommentAndMetadata.offset; - // Add back the indent we "stole" from the field by inserting our comment at its start. - yieldPatch(lineComment(commentContents) + ' ', offset, offset); - } - // Mark as optional - yieldOptionalHintPatch(); - } - return; - } - - if (recommendation.isRequired) { - yieldLateHintPatch(); - } else { - yieldOptionalHintPatch(); - } - } -} - -extension on Token { - /// The offset of the next token or comment - /// (since comments can occur before the next token) - /// following this token, or null if nothing follows it. - int? get nextTokenOrCommentOffset { - final next = this.next; - if (next == null) return null; - final nextTokenOrComment = next.precedingComments ?? next; - return nextTokenOrComment.offset; - } -} diff --git a/lib/src/dart3_suggestors/required_props/collect/aggregate.dart b/lib/src/dart3_suggestors/required_props/collect/aggregate.dart deleted file mode 100644 index d0af29e4..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/aggregate.dart +++ /dev/null @@ -1,409 +0,0 @@ -// 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:collection'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'aggregated_data.sg.dart'; -import 'collected_data.sg.dart'; - -const defaultAggregatedOutputFile = 'prop_requiredness.json'; - -final jsonEncodeIndented = const JsonEncoder.withIndent(' ').convert; - -List loadResultFiles(Iterable resultFiles) { - return resultFiles.map(File.new).map((file) { - PackageResults results; - try { - results = PackageResults.fromJson( - (jsonDecode(file.readAsStringSync()) as Map).cast()); - } catch (e, st) { - throw Exception('Error parsing results from file $file: $e\n$st'); - } - if (results.dataVersion != PackageResults.latestDataVersion) { - throw Exception('Outdated data version in $file'); - } - return results; - }).toList(); -} - -/// Aggregates individual prop usage data from [allResults] into prop -/// requiredness data. -PropRequirednessResults aggregateData( - List allResults, { - bool excludeOtherDynamicUsages = true, - bool excludeUsagesWithForwarded = true, - bool topLevelFactoryUsagesOnly = true, - bool outputDebugData = true, -}) { - final logger = Logger('aggregateData'); - - logger.finer('Checking for duplicates...'); - // Validate that there are no duplicates in the data set - { - final resultsByPackageName = >{}; - for (final result in allResults) { - for (final packageName in [ - result.packageName, - ...result.otherPackageNames - ]) { - resultsByPackageName.putIfAbsent(packageName, () => {}).add(result); - } - } - var duplicateResultsMessages = []; - resultsByPackageName.forEach((packageName, results) { - if (results.length != 1) { - duplicateResultsMessages.add( - 'Results for package $packageName were found in more than one results set:' - ' ${results.map((r) => 'PackageResults(packageName:$packageName)').toList()}'); - } - }); - if (duplicateResultsMessages.isNotEmpty) { - throw Exception( - 'Duplicate results:\n${duplicateResultsMessages.join('\n')}'); - } - } - - final mixinIdsByVisibilityByPackage = { - for (final result in allResults) ...result.mixinIdsByVisibilityByPackage, - }; - - final mismatchedMixinIdsByUsagePackage = >{}; - for (final result in allResults) { - for (final usage in result.usages) { - final usagePackage = usage.usagePackage; - for (final mixinData in usage.mixinData) { - final mixinPackage = mixinData.mixinPackage; - if (mixinPackage != usagePackage) { - final mixinId = mixinData.mixinId; - final mixinName = mixinData.mixinName; - final mixinIdsByVisibility = - mixinIdsByVisibilityByPackage[mixinPackage]; - if (mixinIdsByVisibility != null && - getVisibilityForMixinIdOrCompanion(mixinIdsByVisibility, - mixinId: mixinId, mixinName: mixinName) == - null) { - mismatchedMixinIdsByUsagePackage - .putIfAbsent(usagePackage, () => {}) - .add(mixinId); - } - } - } - } - } - - if (mismatchedMixinIdsByUsagePackage.isNotEmpty) { - logger.warning( - "Found usages of mixins in other packages that don't have declaration data:" - " ${mismatchedMixinIdsByUsagePackage.keys.toList()}"); - mismatchedMixinIdsByUsagePackage.forEach((packageName, mixinIds) { - logger.warning( - "- $packageName:\n${mixinIds.map((i) => ' - $i').join('\n')}"); - }); - } - - final usageStatsByMixinId = {}; - final allMixinIds = {}; - - final mixinNamesById = {}; - final mixinPackagesById = {}; - - UsageSkipReason? getUsageSkipReason(Usage usage) { - if (topLevelFactoryUsagesOnly && - usage.usageBuilderType != BuilderType.topLevelFactory) { - return UsageSkipReason.nonTopLevelFactory; - } - if (excludeUsagesWithForwarded && usage.usageHasForwardedProps) { - return UsageSkipReason.hasForwardedProps; - } - if (excludeOtherDynamicUsages && usage.usageHasOtherDynamicProps) { - return UsageSkipReason.hasOtherDynamicProps; - } - return null; - } - - final allUsages = allResults.expand((r) => r.usages); - - logger.finer('Tallying usages...'); - for (final usage in allUsages) { - final skipReason = getUsageSkipReason(usage); - - for (final mixin in usage.mixinData) { - allMixinIds.add(mixin.mixinId); - mixinNamesById[mixin.mixinId] = mixin.mixinName; - mixinPackagesById[mixin.mixinId] = mixin.mixinPackage; - - final isSamePackage = usage.usagePackage == mixin.mixinPackage; - - final categorizedStats = usageStatsByMixinId.putIfAbsent( - mixin.mixinId, CategorizedPropsMixinUsageStats.new); - - if (skipReason != null) { - categorizedStats.skippedUsages - ..countSkippedUsage(skipReason) - ..debugSkippedUsages.add(usage.usageId); - } else { - categorizedStats.skippedUsages.countNonSkippedUsage(); - for (final stats in [ - categorizedStats.total, - if (isSamePackage) - categorizedStats.samePackage - else - categorizedStats.otherPackage, - ]) { - stats.addPropsCounts(mixin.mixinPropsSet); - stats.usageCount++; - } - } - } - } - - // Do this in a second pass after we've processed all props and - // CategorizedPropsMixinUsageStats.allPropNames is complete for each mixin. - const unsetThreshold = 0.9; - const otherNames = {'renderInput', 'options'}; - logger.finer( - 'Collecting debug usages where props weren\'t set, using threshold $unsetThreshold'); - for (final usage in allUsages) { - if (getUsageSkipReason(usage) != null) continue; - - for (final mixin in usage.mixinData) { - final isSamePackage = usage.usagePackage == mixin.mixinPackage; - final categorizedStats = usageStatsByMixinId[mixin.mixinId]; - // We skipped it above. - if (categorizedStats == null) continue; - for (final propName in categorizedStats.allPropNames) { - if (!mixin.mixinPropsSet.contains(propName)) { - for (final stats in [ - categorizedStats.total, - if (isSamePackage) - categorizedStats.samePackage - else - categorizedStats.otherPackage, - ]) { - final rateForProp = stats.rateForProp(propName); - if (otherNames.contains(propName) || - (rateForProp != null && rateForProp >= unsetThreshold)) { - stats.debugUnsetPropUsages - .putIfAbsent(propName, () => []) - .add(usage.usageId); - } - } - } - } - } - } - - final results = PropRequirednessResults( - excludeOtherDynamicUsages: excludeOtherDynamicUsages, - excludeUsagesWithForwarded: excludeUsagesWithForwarded, - mixinResultsByIdByPackage: {}, - mixinMetadata: MixinMetadata( - mixinNamesById: mixinNamesById, - mixinPackagesById: mixinPackagesById, - ), - ); - - logger.finer('Aggregating final results...'); - for (final mixinId in allMixinIds) { - final stats = usageStatsByMixinId[mixinId]; - if (stats == null) continue; - - final mixinPackage = mixinPackagesById[mixinId]!; - final mixinIdsByVisibilityForPackage = - mixinIdsByVisibilityByPackage[mixinPackage]; - - final Visibility visibility; - if (mixinIdsByVisibilityForPackage == null) { - // If there's no data for public mixins for a package, - // then we don't know if it's public or not. - // We should have this data for all packages we've processed, but it can currently be null - // for packages that don't have any public entrypoints. - visibility = Visibility.unknown; - } else { - visibility = getVisibilityForMixinIdOrCompanion( - mixinIdsByVisibilityForPackage, - mixinId: mixinId, - mixinName: mixinNamesById[mixinId]!) ?? - Visibility.private; - } - - final propResultsByName = {}; - for (final propName in stats.allPropNames) { - final samePackageRate = stats.samePackage.rateForProp(propName); - final samePackageUsageCount = stats.samePackage.countForProp(propName); - - final otherPackageRate = stats.otherPackage.rateForProp(propName); - final otherPackageUsageCount = stats.otherPackage.countForProp(propName); - - // If we're processing this prop, it'll be non-null for total. - final totalRate = stats.total.rateForProp(propName)!; - final totalUsageCount = stats.total.countForProp(propName); - - propResultsByName[propName] = PropResult( - samePackageRate: samePackageRate, - otherPackageRate: otherPackageRate, - totalRate: totalRate, - samePackageUsageCount: samePackageUsageCount, - otherPackageUsageCount: otherPackageUsageCount, - totalUsageCount: totalUsageCount, - debugSamePackageUnsetUsages: outputDebugData - ? stats.samePackage.debugUnsetPropUsages[propName] - : null, - debugOtherPackageUnsetUsages: outputDebugData - ? stats.otherPackage.debugUnsetPropUsages[propName] - : null, - ); - } - - results.mixinResultsByIdByPackage - .putIfAbsent(mixinPackage, () => {})[mixinId] = MixinResult( - visibility: visibility, - usageSkipCount: stats.skippedUsages.skippedCount, - usageSkipRate: stats.skippedUsages.skipRate, - propResultsByName: propResultsByName, - debugSkippedUsages: - outputDebugData ? stats.skippedUsages.debugSkippedUsages : null, - ); - } - - logger.finer('Done.'); - - return results; -} - -Visibility? getVisibilityForMixinIdOrCompanion( - Map> mixinIdsByVisibility, { - required String mixinId, - required String mixinName, -}) { - late final companionId = (() { - const legacyBoilerplatePrefix = r'_$'; - if (mixinName.startsWith(legacyBoilerplatePrefix)) { - // Hack around legacy boilerplate mixins always being private; - // see if the public companion class is public. - final publicName = mixinName.substring(legacyBoilerplatePrefix.length); - return mixinId.replaceFirst(mixinName, publicName); - } - return null; - })(); - - final visibility = _getVisibility(mixinIdsByVisibility, mixinId); - late final companionVisibility = companionId == null - ? null - : _getVisibility(mixinIdsByVisibility, companionId); - - return visibility ?? companionVisibility; -} - -Visibility? _getVisibility( - Map> mixinIdsByVisibility, String someMixinId) { - // Prioritize public over indirectly exposed. - final visibilitiesInPriorityorder = - (LinkedHashSet.of({Visibility.public})..addAll(Visibility.values)); - return visibilitiesInPriorityorder.firstWhereOrNull((visibility) { - return mixinIdsByVisibility[visibility]?.contains(someMixinId) ?? false; - }); -} - -class CategorizedPropsMixinUsageStats { - final samePackage = PropsMixinUsageStats(); - final otherPackage = PropsMixinUsageStats(); - final total = PropsMixinUsageStats(); - - final skippedUsages = SkippedUsageStats(); - - Iterable get allPropNames => total.countsForProps.keys; -} - -class SkippedUsageStats { - var _nonSkippedUsageCount = 0; - final _skippedCountsByReason = {}; - - final List debugSkippedUsages = []; - - int get nonSkippedCount => _nonSkippedUsageCount; - - int get skippedCount => - _skippedCountsByReason.values.fold(0, (a, b) => a + b); - - int get totalCount => nonSkippedCount + skippedCount; - - num get skipRate { - if (totalCount == 0) { - throw StateError('Cannot compute skip rate when totalCount is 0.'); - } - return skippedCount / totalCount; - } - - void countNonSkippedUsage() { - _nonSkippedUsageCount++; - } - - void countSkippedUsage(UsageSkipReason reason) { - _skippedCountsByReason[reason] = (_skippedCountsByReason[reason] ?? 0) + 1; - } -} - -enum UsageSkipReason { - nonTopLevelFactory, - hasOtherDynamicProps, - hasForwardedProps, -} - -class PropsMixinUsageStats { - int usageCount = 0; - Map countsForProps = {}; - - Map> debugUnsetPropUsages = {}; - - void addPropsCounts(Iterable propNames) { - for (final propName in propNames) { - countsForProps[propName] = (countsForProps[propName] ?? 0) + 1; - } - } - - int countForProp(String propName) => countsForProps[propName] ?? 0; - - num? rateForProp(String propName) { - // Return null instead of a non-finite number. - if (usageCount == 0) return null; - return countForProp(propName) / usageCount; - } -} - -class PropsMixin { - final String mixinId; - final String packageName; - final String mixinName; - - PropsMixin._({ - required this.mixinId, - required this.packageName, - required this.mixinName, - }); - - factory PropsMixin.fromId(String mixinId) { - final mixinName = mixinId.split(' - ').first; - final packageName = RegExp(r'\bpackage:([^/]+)/').firstMatch(mixinId)![1]!; - return PropsMixin._( - mixinId: mixinId, - packageName: packageName, - mixinName: mixinName, - ); - } -} diff --git a/lib/src/dart3_suggestors/required_props/collect/aggregated_data.sg.dart b/lib/src/dart3_suggestors/required_props/collect/aggregated_data.sg.dart deleted file mode 100644 index b450d00b..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/aggregated_data.sg.dart +++ /dev/null @@ -1,112 +0,0 @@ -// 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:json_annotation/json_annotation.dart'; - -part 'aggregated_data.sg.g.dart'; - -@JsonSerializable() -class PropRequirednessResults { - final bool excludeOtherDynamicUsages; - final bool excludeUsagesWithForwarded; - - final Map> mixinResultsByIdByPackage; - - final MixinMetadata mixinMetadata; - - factory PropRequirednessResults.fromJson(Map json) => - _$PropRequirednessResultsFromJson(json); - - PropRequirednessResults({ - required this.excludeOtherDynamicUsages, - required this.excludeUsagesWithForwarded, - required this.mixinResultsByIdByPackage, - required this.mixinMetadata, - }); - - Map toJson() => _$PropRequirednessResultsToJson(this); -} - -@JsonSerializable() -class MixinMetadata { - final Map mixinNamesById; - final Map mixinPackagesById; - - MixinMetadata({ - required this.mixinNamesById, - required this.mixinPackagesById, - }); - - factory MixinMetadata.fromJson(Map json) => - _$MixinMetadataFromJson(json); - - Map toJson() => _$MixinMetadataToJson(this); -} - -@JsonSerializable(includeIfNull: false) -class MixinResult { - final Visibility visibility; - final int usageSkipCount; - final num usageSkipRate; - final Map propResultsByName; - final List? debugSkippedUsages; - - MixinResult({ - required this.visibility, - required this.usageSkipCount, - required this.usageSkipRate, - required this.propResultsByName, - this.debugSkippedUsages, - }); - - factory MixinResult.fromJson(Map json) => - _$MixinResultFromJson(json); - - Map toJson() => _$MixinResultToJson(this); -} - -@JsonSerializable(includeIfNull: false) -class PropResult { - final num? samePackageRate; - final num? otherPackageRate; - final num totalRate; - final int samePackageUsageCount; - final int otherPackageUsageCount; - final int totalUsageCount; - final List? debugSamePackageUnsetUsages; - final List? debugOtherPackageUnsetUsages; - - PropResult({ - required this.samePackageRate, - required this.otherPackageRate, - required this.totalRate, - required this.samePackageUsageCount, - required this.otherPackageUsageCount, - required this.totalUsageCount, - this.debugSamePackageUnsetUsages, - this.debugOtherPackageUnsetUsages, - }); - - factory PropResult.fromJson(Map json) => - _$PropResultFromJson(json); - - Map toJson() => _$PropResultToJson(this); -} - -enum Visibility { - public, - indirectlyPublic, - private, - unknown, -} diff --git a/lib/src/dart3_suggestors/required_props/collect/aggregated_data.sg.g.dart b/lib/src/dart3_suggestors/required_props/collect/aggregated_data.sg.g.dart deleted file mode 100644 index f8c2f697..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/aggregated_data.sg.g.dart +++ /dev/null @@ -1,126 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ignore_for_file: implicit_dynamic_parameter - -part of 'aggregated_data.sg.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PropRequirednessResults _$PropRequirednessResultsFromJson( - Map json) => - PropRequirednessResults( - excludeOtherDynamicUsages: json['excludeOtherDynamicUsages'] as bool, - excludeUsagesWithForwarded: json['excludeUsagesWithForwarded'] as bool, - mixinResultsByIdByPackage: - (json['mixinResultsByIdByPackage'] as Map).map( - (k, e) => MapEntry( - k, - (e as Map).map( - (k, e) => - MapEntry(k, MixinResult.fromJson(e as Map)), - )), - ), - mixinMetadata: - MixinMetadata.fromJson(json['mixinMetadata'] as Map), - ); - -Map _$PropRequirednessResultsToJson( - PropRequirednessResults instance) => - { - 'excludeOtherDynamicUsages': instance.excludeOtherDynamicUsages, - 'excludeUsagesWithForwarded': instance.excludeUsagesWithForwarded, - 'mixinResultsByIdByPackage': instance.mixinResultsByIdByPackage, - 'mixinMetadata': instance.mixinMetadata, - }; - -MixinMetadata _$MixinMetadataFromJson(Map json) => - MixinMetadata( - mixinNamesById: Map.from(json['mixinNamesById'] as Map), - mixinPackagesById: - Map.from(json['mixinPackagesById'] as Map), - ); - -Map _$MixinMetadataToJson(MixinMetadata instance) => - { - 'mixinNamesById': instance.mixinNamesById, - 'mixinPackagesById': instance.mixinPackagesById, - }; - -MixinResult _$MixinResultFromJson(Map json) => MixinResult( - visibility: $enumDecode(_$VisibilityEnumMap, json['visibility']), - usageSkipCount: json['usageSkipCount'] as int, - usageSkipRate: json['usageSkipRate'] as num, - propResultsByName: - (json['propResultsByName'] as Map).map( - (k, e) => MapEntry(k, PropResult.fromJson(e as Map)), - ), - debugSkippedUsages: (json['debugSkippedUsages'] as List?) - ?.map((e) => e as String) - .toList(), - ); - -Map _$MixinResultToJson(MixinResult instance) { - final val = { - 'visibility': _$VisibilityEnumMap[instance.visibility]!, - 'usageSkipCount': instance.usageSkipCount, - 'usageSkipRate': instance.usageSkipRate, - 'propResultsByName': instance.propResultsByName, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('debugSkippedUsages', instance.debugSkippedUsages); - return val; -} - -const _$VisibilityEnumMap = { - Visibility.public: 'public', - Visibility.indirectlyPublic: 'indirectlyPublic', - Visibility.private: 'private', - Visibility.unknown: 'unknown', -}; - -PropResult _$PropResultFromJson(Map json) => PropResult( - samePackageRate: json['samePackageRate'] as num?, - otherPackageRate: json['otherPackageRate'] as num?, - totalRate: json['totalRate'] as num, - samePackageUsageCount: json['samePackageUsageCount'] as int, - otherPackageUsageCount: json['otherPackageUsageCount'] as int, - totalUsageCount: json['totalUsageCount'] as int, - debugSamePackageUnsetUsages: - (json['debugSamePackageUnsetUsages'] as List?) - ?.map((e) => e as String) - .toList(), - debugOtherPackageUnsetUsages: - (json['debugOtherPackageUnsetUsages'] as List?) - ?.map((e) => e as String) - .toList(), - ); - -Map _$PropResultToJson(PropResult instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('samePackageRate', instance.samePackageRate); - writeNotNull('otherPackageRate', instance.otherPackageRate); - val['totalRate'] = instance.totalRate; - val['samePackageUsageCount'] = instance.samePackageUsageCount; - val['otherPackageUsageCount'] = instance.otherPackageUsageCount; - val['totalUsageCount'] = instance.totalUsageCount; - writeNotNull( - 'debugSamePackageUnsetUsages', instance.debugSamePackageUnsetUsages); - writeNotNull( - 'debugOtherPackageUnsetUsages', instance.debugOtherPackageUnsetUsages); - return val; -} diff --git a/lib/src/dart3_suggestors/required_props/collect/analysis.dart b/lib/src/dart3_suggestors/required_props/collect/analysis.dart deleted file mode 100644 index 7d65f5ed..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/analysis.dart +++ /dev/null @@ -1,135 +0,0 @@ -// 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:analyzer/dart/analysis/analysis_context_collection.dart'; -import 'package:analyzer/dart/analysis/results.dart'; -import 'package:glob/glob.dart'; -import 'package:glob/list_local_fs.dart'; -import 'package:logging/logging.dart'; -import 'package:package_config/package_config.dart'; -import 'package:path/path.dart' as p; - -import 'package/spec.dart'; - -Future getPackageInfo(PackageSpec package) async { - final directory = await package.getDirectory(); - final pubspecFile = File(p.join(directory.path, 'pubspec.yaml')); - if (!pubspecFile.existsSync()) { - throw Exception('Expected to find a pubspec in ${pubspecFile.path}'); - } - final pubspecContent = await pubspecFile.readAsString(); - - final libDirectory = p.canonicalize(p.join(directory.path, 'lib')); - final canonicalizedPaths = - allDartFilesWithin(libDirectory).map(p.canonicalize).toList(); - if (canonicalizedPaths.isEmpty) { - throw Exception( - "No Dart files found in lib directory '$libDirectory'. Something probably went wrong."); - } - - return PackageInfo( - root: directory.path, - libFiles: canonicalizedPaths, - pubspecContent: pubspecContent, - libDirectory: libDirectory, - ); -} - -List allDartFilesWithin(String path) { - return Glob('**.dart', recursive: true) - .listSync(root: path) - .whereType() - .map((file) => file.path) - .toList(); -} - -class PackageInfo { - final String root; - final String libDirectory; - final List libFiles; - final String pubspecContent; - - PackageInfo({ - required this.root, - required this.libDirectory, - required this.libFiles, - required this.pubspecContent, - }); -} - -Stream getResolvedLibUnitsForPackage( - PackageSpec package, { - required bool includeDependencyPackages, - bool Function(Package)? packageFilter, -}) async* { - final logger = - Logger('getResolvedLibUnitsForPackage.${package.packageAndVersionId}'); - - final analyzeStopWatch = Stopwatch()..start(); - - final packageRoot = await package.getDirectory(); - final libDirectory = p.canonicalize(p.join(packageRoot.path, 'lib')); - final collection = AnalysisContextCollection(includedPaths: [libDirectory]); - final context = collection.contexts.single; - - Iterable? otherPackagesFiles; - if (includeDependencyPackages) { - final packagesFile = context.contextRoot.packagesFile; - if (packagesFile == null) { - throw Exception( - 'No packages file found for context with root ${context.contextRoot.workspace.root}'); - } - final resourceProvider = context.contextRoot.resourceProvider; - final packageConfig = - await loadPackageConfigUri(packagesFile.toUri(), loader: (uri) async { - return resourceProvider - .getFile(resourceProvider.pathContext.fromUri(uri)) - .readAsBytesSync(); - }); - final otherPackageRootPaths = packageConfig.packages - .where((p) => p.name != package.packageName) - .where((p) => packageFilter?.call(p) ?? true) - .map((p) => resourceProvider.pathContext.fromUri(p.packageUriRoot)); - otherPackagesFiles = - otherPackageRootPaths.map(p.canonicalize).expand(allDartFilesWithin); - } - - final filesToAnalyze = [ - ...context.contextRoot.analyzedFiles(), - ...?otherPackagesFiles, - ]; - - logger.finer('Processing units in ${package.packageName} package...'); - for (final path in filesToAnalyze) { - if (!path.endsWith('.dart')) continue; - - // Don't use collection.contextFor(path) since it fails for files in other packages. - final result = await context.currentSession.getResolvedUnit(path); - if (result is ResolvedUnitResult) { - if (result.exists) { - yield result; - } else { - logger.warning('File does not exist: $path'); - } - } else { - logger.warning('Issue resolving $path $result'); - } - } - - logger.finer( - 'Done. Analysis (and async iteration) took ${analyzeStopWatch.elapsed}'); - analyzeStopWatch.stop(); -} diff --git a/lib/src/dart3_suggestors/required_props/collect/collect.dart b/lib/src/dart3_suggestors/required_props/collect/collect.dart deleted file mode 100644 index 93bf0919..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/collect.dart +++ /dev/null @@ -1,312 +0,0 @@ -// 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/element/element.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/component_usage.dart'; -import 'package:over_react_codemod/src/vendor/over_react_analyzer_plugin/get_all_props.dart'; - -import 'collected_data.sg.dart'; -import 'logging.dart'; -import 'util.dart'; - -Future collectDataForUnits( - Stream units, { - required String rootPackageName, - required bool allowOtherPackageUnits, -}) async { - final logger = Logger('prop_requiredness.$rootPackageName'); - - final otherPackagesProcessed = {}; - final packageVersionDescriptionsByName = {}; - final allUsages = []; - final allMixinUsagesByMixinId = >{}; - final mixinIdsByVisibilityByPackage = - >>{}; - - await for (final unitResult in units) { - if (unitResult.uri.path.endsWith('.over_react.g.dart')) continue; - if (unitResult.libraryElement.isInSdk) continue; - - logProgress(); - - //logger.finest('Processing ${unitResult.uri}'); - - final unitElement = unitResult.unit.declaredElement; - if (unitElement == null) { - logger.warning('Failed to resolve ${unitResult.uri}'); - continue; - } - - final packageName = getPackageName(unitResult.uri); - if (packageName == null) { - throw Exception('Unexpected non-package URI: ${unitResult.uri}'); - } - - if (packageName != rootPackageName) { - if (!allowOtherPackageUnits) { - throw StateError( - 'Expected all units to be part of package $rootPackageName,' - ' but got one from package $packageName: ${unitResult.uri}'); - } - otherPackagesProcessed.add(packageName); - } - - allUsages.addAll(collectUsageDataForUnit( - unitResult: unitResult, - packageName: packageName, - )); - - // We'll get redundant results for libraries with multiple compilation units, - // but it doesn't matter since we're using a set, and it's not worth optimizing. - if (_isPublicPackageUri(unitResult.uri)) { - mixinIdsByVisibilityByPackage - .putIfAbsent(packageName, () => {}) - .putIfAbsent(Visibility.public, () => {}) - // Add exported props classes. - .addAll(unitResult.libraryElement.exportNamespace.definedNames.values - .whereType() - // Note that this is public relative to the library, not necessarily the package. - .where((element) => element.isPublic) - // Filter out non-props classes/mixins so we don't collect too much data. - .where((element) => element.name.contains('Props')) - .map(uniqueElementId)); - - // Add factories that indirectly expose props classes. - mixinIdsByVisibilityByPackage - .putIfAbsent(packageName, () => {}) - .putIfAbsent(Visibility.indirectlyPublic, () => {}) - .addAll(unitResult.libraryElement.exportNamespace.definedNames.values - .whereType() - .where((element) => element.isGetter) - // Note that this is public relative to the library, not necessarily the package. - .where((element) => element.isPublic) - // Filter out non-props classes/mixins so we don't collect too much data. - .map((element) { - final potentialPropsElement = element.returnType.typeOrBound - .tryCast() - ?.returnType - .element; - if (potentialPropsElement != null && - (potentialPropsElement.name?.contains('Props') ?? false)) { - return uniqueElementId(potentialPropsElement); - } - return null; - }).whereNotNull()); - } - - _collectMixinUsagesByMixin(unitElement) - .forEach((usedMixinId, usedByMixinIds) { - allMixinUsagesByMixinId - .putIfAbsent(usedMixinId, () => {}) - .addAll(usedByMixinIds); - }); - } - - return PackageResults( - packageName: rootPackageName, - otherPackageNames: otherPackagesProcessed, - packageVersionDescriptionsByName: packageVersionDescriptionsByName, - dataVersion: PackageResults.latestDataVersion, - usages: allUsages, - mixinIdsByVisibilityByPackage: mixinIdsByVisibilityByPackage, - allMixinUsagesByMixinId: allMixinUsagesByMixinId, - ); -} - -/// Returns [uri] is a package URI with a public (not under src/) path. -bool _isPublicPackageUri(Uri uri) { - if (!uri.isScheme('package')) return false; - // First path segment is the package name - return uri.pathSegments[1] != 'src'; -} - -List collectUsageDataForUnit({ - required ResolvedUnitResult unitResult, - required String packageName, -}) { - final logger = Logger('collectUsageData'); - - final allUsages = []; - - String uniqueNodeId(AstNode node) { - // Use line/column instead of the raw offset for easier debugging. - final location = unitResult.lineInfo.getLocation(node.offset); - return '${unitResult.uri}#$location'; - } - - unitResult.unit.accept(ComponentUsageVisitor((componentUsage) { - if (componentUsage.isDom) return; - - final usageId = uniqueNodeId(componentUsage.node); - - BuilderType builderType; - if (componentUsage.factory == null) { - builderType = BuilderType.otherBuilder; - } else if (componentUsage.factoryTopLevelVariableElement != null) { - builderType = BuilderType.topLevelFactory; - } else { - builderType = BuilderType.otherFactory; - } - - final dynamicPropsCategories = componentUsage.cascadedMethodInvocations - .map((c) { - final methodName = c.methodName.name; - late final arg = c.node.argumentList.arguments.firstOrNull; - - switch (methodName) { - case 'addUnconsumedProps': - return DynamicPropsCategory.forwarded; - case 'addAll': - case 'addProps': - if (arg is MethodInvocation && - (arg.methodName.name == 'getPropsToForward' || - arg.methodName.name == 'copyUnconsumedProps')) { - return DynamicPropsCategory.forwarded; - } - return DynamicPropsCategory.other; - case 'modifyProps': - if ((arg is MethodInvocation && - arg.methodName.name == 'addPropsToForward') || - (arg is Identifier && arg.name == 'addUnconsumedProps')) { - return DynamicPropsCategory.forwarded; - } - return DynamicPropsCategory.other; - } - - return null; - }) - .whereNotNull() - .toSet(); - final usageHasOtherDynamicProps = - dynamicPropsCategories.contains(DynamicPropsCategory.other); - final usageHasForwardedProps = - dynamicPropsCategories.contains(DynamicPropsCategory.forwarded); - - final builderPropsType = componentUsage - .builder.staticType?.typeOrBound.element - ?.tryCast(); - if (builderPropsType == null) { - logger - .warning('Could not resolve props; skipping usage. Usage: $usageId'); - return; - } - - List mixinData; - { - final assignedProps = - componentUsage.cascadedProps.where((p) => !p.isPrefixed).toSet(); - final assignedPropNames = assignedProps.map((p) => p.name.name).toSet(); - final unaccountedForPropNames = {...assignedPropNames}; - - // TODO maybe store mixin metadata separately? - - // [1] Use prop mixin elements and not the props, to account for setters that don't show up as prop fields - // (e.g., props that do conversion in getter/setter, props that alias other props). - final allPropMixins = - getAllPropsClassesOrMixins(builderPropsType).toSet(); // [1] - mixinData = allPropMixins.map((mixin) { - // [1] - final mixinPropsSet = assignedPropNames - .where((propName) => - mixin.getField(propName) != null || - mixin.getSetter(propName) != null) - .toSet(); - unaccountedForPropNames.removeAll(mixinPropsSet); - - final mixinPackage = getPackageName(mixin.librarySource.uri); - if (mixinPackage == null) { - throw Exception('Unexpected non-package URI: ${unitResult.uri}'); - } - - return UsageMixinData( - mixinPackage: mixinPackage, - mixinId: uniqueElementId(mixin), - mixinName: mixin.name, - // Note that for overridden props, they'll show up in multiple mixins - mixinPropsSet: mixinPropsSet, - ); - }).toList(); - - final unaccountedForProps = assignedProps - .where((p) => unaccountedForPropNames.contains(p.name.name)); - for (final prop in unaccountedForProps) { - final propsMixin = prop.staticElement?.enclosingElement; - if (propsMixin == null) continue; - - if (const { - 'ReactPropsMixin', - 'UbiquitousDomPropsMixin', - 'CssClassPropsMixin' - }.contains(propsMixin.name)) { - continue; - } - - // Edge-case: the deprecated FluxUiProps isn't picked up as a normal props class. - // We don't care about those for this script, so just bail. - if (propsMixin.name == 'FluxUiProps') { - continue; - } - - logger.warning( - 'Could not find corresponding mixin for prop ${prop.node.toSource()} for $usageId.' - ' enclosingElement from usage: ${uniqueElementId(propsMixin)},' - ' allPropsMixins from usage: ${allPropMixins.map(uniqueElementId).toList()}'); - } - } - - allUsages.add(Usage( - usageId: usageId, - usageUri: unitResult.uri.toString(), - usageDebugInfo: UsageDebugInfo( - usageBuilderSource: componentUsage.builder.toSource(), - ), - usagePackage: packageName, - usageHasOtherDynamicProps: usageHasOtherDynamicProps, - usageHasForwardedProps: usageHasForwardedProps, - usageBuilderType: builderType, - mixinData: mixinData, - )); - })); - - return allUsages; -} - -Map> _collectMixinUsagesByMixin( - CompilationUnitElement unitElement) { - final mixinUsagesByMixin = >{}; - - for (final cl in [unitElement.classes, unitElement.mixins].expand((i) => i)) { - final id = uniqueElementId(cl); - for (final mixin in getAllPropsClassesOrMixins(cl)) { - mixinUsagesByMixin.putIfAbsent(uniqueElementId(mixin), () => []).add(id); - } - } - - return mixinUsagesByMixin; -} - -extension ConditionalFunctionExtension1 on R Function(A) { - R? callIfNotNull(A? arg) => arg == null ? null : this(arg); -} - -enum DynamicPropsCategory { - other, - forwarded, -} diff --git a/lib/src/dart3_suggestors/required_props/collect/collected_data.sg.dart b/lib/src/dart3_suggestors/required_props/collect/collected_data.sg.dart deleted file mode 100644 index 87fa1349..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/collected_data.sg.dart +++ /dev/null @@ -1,125 +0,0 @@ -// 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:convert'; - -import 'package:json_annotation/json_annotation.dart'; - -import 'aggregated_data.sg.dart' show Visibility; -export 'aggregated_data.sg.dart' show Visibility; - -part 'collected_data.sg.g.dart'; - -@JsonSerializable() -class PackageResults { - static String latestDataVersion = '13'; - - final String dataVersion; - - final String packageName; - final Set otherPackageNames; - final Map packageVersionDescriptionsByName; - final List usages; - final Map>> mixinIdsByVisibilityByPackage; - final Map> allMixinUsagesByMixinId; - - factory PackageResults.fromJson(Map json) => - _$PackageResultsFromJson(json); - - PackageResults({ - required this.dataVersion, - required this.packageName, - required this.otherPackageNames, - required this.packageVersionDescriptionsByName, - required this.usages, - required this.mixinIdsByVisibilityByPackage, - required this.allMixinUsagesByMixinId, - }); - - Map toJson() => _$PackageResultsToJson(this); -} - -PackageResults? tryParseResults(String potentialJson) { - try { - return PackageResults.fromJson( - (jsonDecode(potentialJson) as Map).cast()); - } catch (_) { - return null; - } -} - -@JsonSerializable() -class Usage { - final String usageId; - final String usageUri; - final String usagePackage; - final UsageDebugInfo? usageDebugInfo; - final bool usageHasOtherDynamicProps; - final bool usageHasForwardedProps; - final BuilderType usageBuilderType; - final List mixinData; - - Usage({ - required this.usageId, - required this.usageUri, - required this.usagePackage, - this.usageDebugInfo, - required this.usageHasOtherDynamicProps, - required this.usageHasForwardedProps, - required this.usageBuilderType, - required this.mixinData, - }); - - factory Usage.fromJson(Map json) => _$UsageFromJson(json); - - Map toJson() => _$UsageToJson(this); -} - -@JsonSerializable() -class UsageDebugInfo { - final String usageBuilderSource; - - UsageDebugInfo({required this.usageBuilderSource}); - - factory UsageDebugInfo.fromJson(Map json) => - _$UsageDebugInfoFromJson(json); - - Map toJson() => _$UsageDebugInfoToJson(this); -} - -@JsonSerializable() -class UsageMixinData { - final String mixinPackage; - final String mixinId; - final String mixinName; - final Set mixinPropsSet; - - UsageMixinData({ - required this.mixinPackage, - required this.mixinId, - required this.mixinName, - required this.mixinPropsSet, - }); - - factory UsageMixinData.fromJson(Map json) => - _$UsageMixinDataFromJson(json); - - Map toJson() => _$UsageMixinDataToJson(this); -} - -enum BuilderType { - topLevelFactory, - otherFactory, - otherBuilder, -} diff --git a/lib/src/dart3_suggestors/required_props/collect/collected_data.sg.g.dart b/lib/src/dart3_suggestors/required_props/collect/collected_data.sg.g.dart deleted file mode 100644 index 5e0a9ac8..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/collected_data.sg.g.dart +++ /dev/null @@ -1,121 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ignore_for_file: implicit_dynamic_parameter - -part of 'collected_data.sg.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PackageResults _$PackageResultsFromJson(Map json) => - PackageResults( - dataVersion: json['dataVersion'] as String, - packageName: json['packageName'] as String, - otherPackageNames: (json['otherPackageNames'] as List) - .map((e) => e as String) - .toSet(), - packageVersionDescriptionsByName: Map.from( - json['packageVersionDescriptionsByName'] as Map), - usages: (json['usages'] as List) - .map((e) => Usage.fromJson(e as Map)) - .toList(), - mixinIdsByVisibilityByPackage: - (json['mixinIdsByVisibilityByPackage'] as Map).map( - (k, e) => MapEntry( - k, - (e as Map).map( - (k, e) => MapEntry($enumDecode(_$VisibilityEnumMap, k), - (e as List).map((e) => e as String).toSet()), - )), - ), - allMixinUsagesByMixinId: - (json['allMixinUsagesByMixinId'] as Map).map( - (k, e) => - MapEntry(k, (e as List).map((e) => e as String).toSet()), - ), - ); - -Map _$PackageResultsToJson(PackageResults instance) => - { - 'dataVersion': instance.dataVersion, - 'packageName': instance.packageName, - 'otherPackageNames': instance.otherPackageNames.toList(), - 'packageVersionDescriptionsByName': - instance.packageVersionDescriptionsByName, - 'usages': instance.usages, - 'mixinIdsByVisibilityByPackage': instance.mixinIdsByVisibilityByPackage - .map((k, e) => MapEntry(k, - e.map((k, e) => MapEntry(_$VisibilityEnumMap[k]!, e.toList())))), - 'allMixinUsagesByMixinId': instance.allMixinUsagesByMixinId - .map((k, e) => MapEntry(k, e.toList())), - }; - -const _$VisibilityEnumMap = { - Visibility.public: 'public', - Visibility.indirectlyPublic: 'indirectlyPublic', - Visibility.private: 'private', - Visibility.unknown: 'unknown', -}; - -Usage _$UsageFromJson(Map json) => Usage( - usageId: json['usageId'] as String, - usageUri: json['usageUri'] as String, - usagePackage: json['usagePackage'] as String, - usageDebugInfo: json['usageDebugInfo'] == null - ? null - : UsageDebugInfo.fromJson( - json['usageDebugInfo'] as Map), - usageHasOtherDynamicProps: json['usageHasOtherDynamicProps'] as bool, - usageHasForwardedProps: json['usageHasForwardedProps'] as bool, - usageBuilderType: - $enumDecode(_$BuilderTypeEnumMap, json['usageBuilderType']), - mixinData: (json['mixinData'] as List) - .map((e) => UsageMixinData.fromJson(e as Map)) - .toList(), - ); - -Map _$UsageToJson(Usage instance) => { - 'usageId': instance.usageId, - 'usageUri': instance.usageUri, - 'usagePackage': instance.usagePackage, - 'usageDebugInfo': instance.usageDebugInfo, - 'usageHasOtherDynamicProps': instance.usageHasOtherDynamicProps, - 'usageHasForwardedProps': instance.usageHasForwardedProps, - 'usageBuilderType': _$BuilderTypeEnumMap[instance.usageBuilderType]!, - 'mixinData': instance.mixinData, - }; - -const _$BuilderTypeEnumMap = { - BuilderType.topLevelFactory: 'topLevelFactory', - BuilderType.otherFactory: 'otherFactory', - BuilderType.otherBuilder: 'otherBuilder', -}; - -UsageDebugInfo _$UsageDebugInfoFromJson(Map json) => - UsageDebugInfo( - usageBuilderSource: json['usageBuilderSource'] as String, - ); - -Map _$UsageDebugInfoToJson(UsageDebugInfo instance) => - { - 'usageBuilderSource': instance.usageBuilderSource, - }; - -UsageMixinData _$UsageMixinDataFromJson(Map json) => - UsageMixinData( - mixinPackage: json['mixinPackage'] as String, - mixinId: json['mixinId'] as String, - mixinName: json['mixinName'] as String, - mixinPropsSet: (json['mixinPropsSet'] as List) - .map((e) => e as String) - .toSet(), - ); - -Map _$UsageMixinDataToJson(UsageMixinData instance) => - { - 'mixinPackage': instance.mixinPackage, - 'mixinId': instance.mixinId, - 'mixinName': instance.mixinName, - 'mixinPropsSet': instance.mixinPropsSet.toList(), - }; diff --git a/lib/src/dart3_suggestors/required_props/collect/logging.dart b/lib/src/dart3_suggestors/required_props/collect/logging.dart deleted file mode 100644 index aab177f0..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/logging.dart +++ /dev/null @@ -1,50 +0,0 @@ -// 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:io/ansi.dart'; -import 'package:logging/logging.dart'; - -/// Flag to help keep logs and progress output on separate lines. -var lastLogWasProgress = false; - -void logProgress([String character = '.']) { - lastLogWasProgress = true; - stderr.write(character); -} - -void initLogging({bool verbose = false}) { - Logger.root.level = verbose ? Level.FINEST : Level.INFO; - Logger.root.onRecord.listen((record) { - if (lastLogWasProgress) stderr.writeln(); - lastLogWasProgress = false; - - AnsiCode color; - if (record.level < Level.WARNING) { - color = cyan; - } else if (record.level < Level.SEVERE) { - color = yellow; - } else { - color = red; - } - final message = StringBuffer()..write(color.wrap('[${record.level}] ')); - if (verbose) message.write('${record.loggerName}: '); - message.write(record.message); - print(message.toString()); - - if (record.error != null) print(record.error); - if (record.stackTrace != null) print(record.stackTrace); - }); -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/git.dart b/lib/src/dart3_suggestors/required_props/collect/package/git.dart deleted file mode 100644 index b46b9b2e..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/git.dart +++ /dev/null @@ -1,85 +0,0 @@ -// 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 'dart:math'; - -import 'package:logging/logging.dart'; -import 'package:over_react_codemod/src/util/command.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import 'spec.dart'; -import 'temp.dart'; - -Future gitRefPackageSpec(String repoUrl, String gitRef) async { - final cloneDirectory = await gitClone(repoUrl); - - Future runGitInheritStdio(List args) => - runCommandAndThrowIfFailedInheritIo('git', args, - workingDirectory: cloneDirectory.path); - - Future runGit(List args) => - runCommandAndThrowIfFailed('git', args, - workingDirectory: cloneDirectory.path); - - // Clear any local changes, such as a pubspec.lock that got updated upon pub get. - await runGit(['reset', '--hard']); - await runGitInheritStdio(['fetch', 'origin', gitRef]); - await runGit(['checkout', '--detach', 'FETCH_HEAD']); - - final commit = await runGit(['rev-parse', 'HEAD']); - final description = await - // Try using a tag first - runGit(['describe', '--exact-match', '--tags', 'HEAD']) - // then fall back to the commit - .onError((_, __) => commit); - - final packageName = (loadYamlNode( - File(p.join(cloneDirectory.path, 'pubspec.yaml')).readAsStringSync()) - as YamlMap)['name'] as String; - - return PackageSpec( - packageName: packageName, - versionId: commit, - sourceDescription: 'Git ref $gitRef: $description', - getDirectory: () async => cloneDirectory, - ); -} - -Future gitClone(String repoUrl, {String? parentDirectory}) async { - parentDirectory ??= packageTempDirectory().path; - - // 'git@example.com/foo/bar.git' -> ['foo', 'bar.git'] - // 'https://example.com/foo/bar.git' -> ['foo', 'bar.git'] - final cloneSubdirectory = - p.joinAll(repoUrl.split(':').last.split('/').takeLast(2)); - - final cloneDirectory = Directory(p.join(parentDirectory, cloneSubdirectory)); - if (!cloneDirectory.existsSync()) { - cloneDirectory.parent.createSync(recursive: true); - Logger('gitClone').fine('Cloning $repoUrl...'); - await runCommandAndThrowIfFailedInheritIo( - 'git', ['clone', repoUrl, cloneDirectory.path]); - } - - return cloneDirectory; -} - -extension on List { - List takeLast(int amount) { - RangeError.checkNotNegative(amount); - return sublist(max(length - amount, 0)); - } -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/local.dart b/lib/src/dart3_suggestors/required_props/collect/package/local.dart deleted file mode 100644 index 454ab4fb..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/local.dart +++ /dev/null @@ -1,32 +0,0 @@ -// 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:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import 'spec.dart'; - -PackageSpec localPathPackageSpec(String packageRoot) { - final packageName = (loadYamlNode( - File(p.join(packageRoot, 'pubspec.yaml')).readAsStringSync()) - as YamlMap)['name'] as String; - return PackageSpec( - packageName: packageName, - versionId: 'local-path', - sourceDescription: 'local path', - getDirectory: () async => Directory(packageRoot), - ); -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/metadata.dart b/lib/src/dart3_suggestors/required_props/collect/package/metadata.dart deleted file mode 100644 index 8e15984e..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/metadata.dart +++ /dev/null @@ -1,74 +0,0 @@ -// 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:async'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; - -import 'transport.dart'; - -Future getLatestVersionOfPackage(String packageName, - {required String host}) async { - final packageInfo = - await getPackageInfo(packageName: packageName, host: host); - final latestVersion = (packageInfo['latest'] as Map)['version'] as String; - return latestVersion; -} - -// Fetch pub packages from a pub server -// Adapted from https://github.com/Workiva/cp_labs/blob/3c436d14cfaf958820dcf0a7ae44425f155c2bdb/tool/nsdash/bin/src/pub.dart#L10 -Future> fetchAllPackageNames(String host) async { - final logger = Logger('fetchPackages'); - logger.fine('Loading list of all packages from $host...'); - - Uri? uri = Uri.parse('$host/api/packages'); - var page = 0; - - final packageNames = []; - // Get ALL packages from a server - while (uri != null) { - if (page != 0) { - logger.finer('Fetching additional page $page: $uri'); - } - - page++; - // request the url - var response = await httpClient.newRequest().get(uri: uri); - if (response.status != 200) { - throw HttpException('${response.status} ${response.statusText}', - uri: uri); - } - final json = response.body.asJson() as Map; - - // get the next_url top level property if it exists, set url - final nextUrl = json['next_url'] as String?; - uri = nextUrl == null ? null : Uri.parse(nextUrl); - - for (final p in json['packages'] as List) { - final name = p['name'] as String; - packageNames.add(name); - } - } - logger.finer('Done. Loaded ${packageNames.length} packages from $page pages'); - return packageNames; -} - -Future getPackageInfo( - {required String packageName, required String host}) async { - final uri = Uri.parse(p.url.join(host, 'api/packages', packageName)); - final response = await httpClient.newRequest().get(uri: uri); - return (await response.body.asJson()) as Map; -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/parse_spec.dart b/lib/src/dart3_suggestors/required_props/collect/package/parse_spec.dart deleted file mode 100644 index 3e8e71d8..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/parse_spec.dart +++ /dev/null @@ -1,97 +0,0 @@ -// 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:collection/collection.dart'; - -import 'git.dart'; -import 'local.dart'; -import 'pub.dart'; -import 'spec.dart'; -import 'version_manager.dart'; - -const packageSpecFormatsHelpText = r''' -Supported package spec formats: -- Hosted pub package with optional version (uses latest if omitted): - - `pub@pub.dev:over_react` - - `pub@pub.dev:over_react#5.2.0` -- Git URL with optional revision: - - `git@github.com:Workiva/over_react.git` - - `https://github.com/Workiva/over_react.git` - - `git@github.com:Workiva/over_react.git#5.2.0` -- Local file path: - - `/path/to/over_react` - - `file:///path/to/over_react`'''; - -Future parsePackageSpec( - String packageSpecString, { - required PackageVersionManager Function() getVersionManager, -}) async { - Never invalidPackageSpec([String additionalMessage = '']) => - throw PackageSpecParseException(''' -Could not resolve package spec '$packageSpecString'.$additionalMessage - -$packageSpecFormatsHelpText'''); - - final uri = Uri.tryParse(packageSpecString); - if ((uri != null && uri.isScheme('https://')) || - packageSpecString.startsWith('git@')) { - final parts = packageSpecString.split('#'); - final repoUrl = parts[0]; - var ref = parts.skip(1).firstOrNull ?? ''; - if (ref.isEmpty) ref = 'HEAD'; - return gitRefPackageSpec(repoUrl, ref); - } - - if (packageSpecString.startsWith('pub@')) { - final pattern = RegExp(r'pub@(.+):(\w+)(?:#(.+))?$'); - final match = pattern.firstMatch(packageSpecString); - if (match == null) { - throw Exception( - "Pub formats must be 'pub@:(#version)'"); - } - var host = match[1]!; - if (!Uri.parse(host).hasScheme) { - host = 'https://$host'; - } - final packageName = match[2]!; - final version = match[3] ?? ''; - return pubPackageSpec( - packageName: packageName, - version: version.isEmpty ? null : version, - versionManager: getVersionManager(), - host: host.toString(), - ); - } - - if (uri != null && (!uri.hasScheme || uri.isScheme('file'))) { - final path = uri.toFilePath(); - if (!Directory(path).existsSync()) { - invalidPackageSpec(' If this is local path, it does not exist.'); - } - return localPathPackageSpec(path); - } - - invalidPackageSpec(); -} - -class PackageSpecParseException implements Exception { - final String message; - - PackageSpecParseException(this.message); - - @override - String toString() => 'PackageSpecParseException: $message'; -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/pub.dart b/lib/src/dart3_suggestors/required_props/collect/package/pub.dart deleted file mode 100644 index 3d2a11df..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/pub.dart +++ /dev/null @@ -1,59 +0,0 @@ -// 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:path/path.dart' as p; - -import 'metadata.dart'; -import 'spec.dart'; -import 'version_manager.dart'; - -PackageSpec packageSpecFromPackageVersion( - PackageVersion version, PackageVersionManager versionManager, - {String? sourceDescription}) { - sourceDescription ??= version.toString(); - return PackageSpec( - packageName: version.packageName, - versionId: version.version, - sourceDescription: sourceDescription, - getDirectory: () => versionManager.getExtractedFolder(version), - ); -} - -Future _resetPubspecLock(Directory directory) async { - // pubspec.lock shouldn't be included in published packages, so always delete it. - final pubspecLockFile = File(p.join(directory.path, 'pubspec.lock')); - if (pubspecLockFile.existsSync()) pubspecLockFile.deleteSync(); -} - -Future pubPackageSpec({ - required String packageName, - String? version, - required PackageVersionManager versionManager, - required String host, -}) async { - final useLatest = version == null; - - final packageVersion = PackageVersion( - hostUrl: host, - packageName: packageName, - version: useLatest - ? await getLatestVersionOfPackage(packageName, host: host) - : version, - ); - return packageSpecFromPackageVersion(packageVersion, versionManager, - sourceDescription: - useLatest ? '$packageVersion (latest version)' : '$packageVersion'); -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/spec.dart b/lib/src/dart3_suggestors/required_props/collect/package/spec.dart deleted file mode 100644 index 8a6a536c..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/spec.dart +++ /dev/null @@ -1,59 +0,0 @@ -// 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'; - -/// A generic representation of a specific version of a package -/// that can also be used in various analysis tasks. -/// -/// Allows decoupling between the way a package is sourced and how it is analyzed. -/// -/// For example, this could be a hosted package from `VersionManager` (see `packageSpecFromPackageVersion`) -/// or any other package source (e.g., a cloned Git revision, a local working copy). -class PackageSpec { - /// The name of the package. - /// - /// This must match the package name in this package's pubspec.yaml. - final String packageName; - - /// A unique ID that can differentiate this package from others with the same [packageName]. - /// - /// Must contain only characters that can be used in a valid filename. - final String versionId; - - /// A human-readable description of the source of this package and version. - final String sourceDescription; - - /// Returns a future with a directory that has been populated with this package's contents. - /// - /// This directory should only be read from. - /// - /// For example, this function may download and extract a tarball of a hosted package, or check - /// out a revision in a Git clone. - final Future Function() getDirectory; - - /// A unique identifier containing [packageName] and [versionId]. - String get packageAndVersionId => '$packageName.$versionId'; - - PackageSpec({ - required this.packageName, - required this.versionId, - required this.sourceDescription, - required this.getDirectory, - }); - - @override - String toString() => - 'PackageSpec($packageName, $versionId) - $sourceDescription'; -} diff --git a/lib/src/dart3_suggestors/required_props/collect/package/temp.dart b/lib/src/dart3_suggestors/required_props/collect/package/temp.dart deleted file mode 100644 index 55f1d206..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/temp.dart +++ /dev/null @@ -1,20 +0,0 @@ -// 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:path/path.dart' as p; - -Directory packageTempDirectory() => - Directory(p.join(Directory.systemTemp.path, 'over_react_codemod_packages')) - ..createSync(recursive: true); diff --git a/lib/src/dart3_suggestors/required_props/collect/package/transport.dart b/lib/src/dart3_suggestors/required_props/collect/package/transport.dart deleted file mode 100644 index f995e4fc..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/transport.dart +++ /dev/null @@ -1,18 +0,0 @@ -// 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:w_transport/vm.dart'; -import 'package:w_transport/w_transport.dart'; - -final httpClient = HttpClient(transportPlatform: VMTransportPlatform()); diff --git a/lib/src/dart3_suggestors/required_props/collect/package/version_manager.dart b/lib/src/dart3_suggestors/required_props/collect/package/version_manager.dart deleted file mode 100644 index 79798d1d..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/package/version_manager.dart +++ /dev/null @@ -1,136 +0,0 @@ -// 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:async'; -import 'dart:io' hide HttpClient; - -import 'package:logging/logging.dart'; -import 'package:over_react_codemod/src/util/command.dart'; -import 'package:path/path.dart' as p; - -import 'temp.dart'; - -// ------------------------------------------------------------------------------- -// -// Packages downloading and extracting -// -// ------------------------------------------------------------------------------- - -class PackageVersionManager { - static final logger = Logger('PackageVersionManager'); - - final String _cachePath; - - PackageVersionManager(this._cachePath); - - factory PackageVersionManager.persistentSystemTemp() { - final directory = - Directory(p.join(packageTempDirectory().path, 'version_manager')) - ..createSync(recursive: true); - return PackageVersionManager(directory.path); - } - - String get _downloadsFolder => p.join(_cachePath, 'downloads'); - - String get _extractedFolder => p.join(_cachePath, 'extracted'); - - String _hostAsDirectoryName(String url) => - Uri.parse(url).authority.replaceAll(RegExp(r'[^\w.]'), ''); - - String packageVersionName(PackageVersion version) => - '${version.packageName}-${version.version}'; - - String _downloadPath(PackageVersion version) => p.join( - _downloadsFolder, - _hostAsDirectoryName(version.hostUrl), - packageVersionName(version) + '.tar.gz'); - - String _extractedPath(PackageVersion version) => p.join(_extractedFolder, - _hostAsDirectoryName(version.hostUrl), packageVersionName(version)); - - Future _downloadPackage(PackageVersion version) async { - final downloadedFile = File(_downloadPath(version)); - if (!downloadedFile.existsSync()) { - logger.fine('Downloading $version...'); - - downloadedFile.parent.createSync(recursive: true); - await runCommandAndThrowIfFailed( - 'wget', [version.archiveUrl, '-O', downloadedFile.path]); - if (!downloadedFile.existsSync()) { - throw StateError( - 'Downloading file appeared to succeed, but file could not be found: ${downloadedFile.path}'); - } - } else { - logger.fine('Using cached download for $version'); - } - - return downloadedFile; - } - - Future _downloadAndExtractPackage(PackageVersion version) async { - final extractedDirectory = Directory(_extractedPath(version)); - if (!extractedDirectory.existsSync()) { - try { - final downloaded = await _downloadPackage(version); - logger.finer('Extracting $version...'); - extractedDirectory.createSync(recursive: true); - await runCommandAndThrowIfFailed('tar', [ - 'xzv', - '--directory', - extractedDirectory.path, - '--file', - downloaded.path - ]); - } catch (_) { - if (extractedDirectory.existsSync()) { - extractedDirectory.deleteSync(recursive: true); - } - rethrow; - } - } else { - logger.finer('Using already extracted folder for $version'); - final pubspecFile = File(p.join(extractedDirectory.path, 'pubspec.yaml')); - if (!pubspecFile.existsSync()) { - throw Exception('No pubspec file found at ${pubspecFile.path}.' - ' Either this package version is bad, or something went wrong with the download and extraction steps.' - ' Try deleting the following files/directories and running the script again:' - ' ${_downloadPath(version)}, ${extractedDirectory.path}'); - } - } - - return extractedDirectory; - } - - Future getExtractedFolder(PackageVersion version) => - _downloadAndExtractPackage(version); -} - -class PackageVersion { - final String packageName; - final String hostUrl; - final String version; - final String archiveUrl; - - PackageVersion({ - required this.packageName, - required this.hostUrl, - required this.version, - String? archiveUrl, - }) : archiveUrl = archiveUrl ?? - p.url.join( - hostUrl, '/packages/$packageName/versions/$version.tar.gz'); - - @override - String toString() => '$packageName $version (from $hostUrl)'; -} diff --git a/lib/src/dart3_suggestors/required_props/collect/util.dart b/lib/src/dart3_suggestors/required_props/collect/util.dart deleted file mode 100644 index 29c0d764..00000000 --- a/lib/src/dart3_suggestors/required_props/collect/util.dart +++ /dev/null @@ -1,35 +0,0 @@ -// 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/element/element.dart'; - -String? getPackageName(Uri uri) { - if (uri.scheme == 'package') return uri.pathSegments[0]; - return null; -} - -String uniqueElementId(Element element) { - // Use element.location so that we consolidate elements across different contexts - final location = element.location; - if (location != null) { - // Remove duplicate package URI - final components = {...location.components}.toList(); - // Move the package to the end so that the class shows up first, which is easier to read. - final pathIndex = components.indexWhere((c) => c.startsWith('package:')); - final path = pathIndex == -1 ? null : components.removeAt(pathIndex); - return [components.join(';'), if (path != null) path].join(' - '); - } - - return 'root:${element.session?.analysisContext.contextRoot},id:${element.id},${element.source?.uri},${element.name}'; -} diff --git a/lib/src/executables/null_safety_migrator_companion.dart b/lib/src/executables/null_safety_migrator_companion.dart deleted file mode 100644 index cf7335f6..00000000 --- a/lib/src/executables/null_safety_migrator_companion.dart +++ /dev/null @@ -1,95 +0,0 @@ -// 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/class_component_required_initial_state.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/connect_required_props.dart'; -import 'package:over_react_codemod/src/util.dart'; - -import '../dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor.dart'; -import '../dart3_suggestors/null_safety_prep/state_mixin_suggestor.dart'; -import '../util/package_util.dart'; - -const _changesRequiredOutput = """ - To update your code, run the following commands in your repository: - dart pub global activate over_react_codemod - dart pub global run over_react_codemod:null_safety_migrator_companion -"""; - -/// Codemods in this executable add nullability "hints" to assist with a -/// null-safety migration. -/// -/// If it has not already been run, the `null_safety_prep` codemod should -/// also be run when migrating to null-safety. -void main(List args) async { - final parser = ArgParser.allowAnything(); - - final parsedArgs = parser.parse(args); - final packageRoot = findPackageRootFor('.'); - await runPubGetIfNeeded(packageRoot); - final dartPaths = allDartPathsExceptHiddenAndGenerated(); - - exitCode = await runInteractiveCodemodSequence( - dartPaths, - [ - CallbackRefHintSuggestor(), - ], - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); - - if (exitCode != 0) return; - - exitCode = await runInteractiveCodemodSequence( - dartPaths, - [ - ClassComponentRequiredInitialStateMigrator(), - ], - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); - - if (exitCode != 0) return; - - exitCode = await runInteractiveCodemodSequence( - dartPaths, - [ - StateMixinSuggestor(), - ], - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); - - if (exitCode != 0) return; - - exitCode = await runInteractiveCodemodSequence( - dartPaths, - [ - ConnectRequiredProps(), - ], - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); -} diff --git a/lib/src/executables/null_safety_prep.dart b/lib/src/executables/null_safety_prep.dart deleted file mode 100644 index 5f28615d..00000000 --- a/lib/src/executables/null_safety_prep.dart +++ /dev/null @@ -1,56 +0,0 @@ -// 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/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/use_ref_init_migration.dart'; -import 'package:over_react_codemod/src/util.dart'; - -import '../dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor.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 -"""; - -/// Codemods in this executable should be changes that teams -/// can make ahead of moving forward with their null-safety migration. -/// -/// Codemods that do things like add nullability "hints" should be placed -/// within `null_safety_migrator_companion` - and run only when a team is -/// ready to move forward with a null-safety migration. -void main(List args) async { - final parser = ArgParser.allowAnything(); - - final parsedArgs = parser.parse(args); - final dartPaths = allDartPathsExceptHiddenAndGenerated(); - - exitCode = await runInteractiveCodemod( - dartPaths, - aggregate([ - UseRefInitMigration(), - FnPropNullAwareCallSuggestor(), - DomCallbackNullArgs(), - ]), - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); -} diff --git a/lib/src/executables/null_safety_required_props.dart b/lib/src/executables/null_safety_required_props.dart deleted file mode 100644 index 72ed53fa..00000000 --- a/lib/src/executables/null_safety_required_props.dart +++ /dev/null @@ -1,34 +0,0 @@ -// 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/command_runner.dart'; -import 'package:io/io.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/required_props/bin/codemod.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/required_props/bin/collect.dart'; - -void main(List args) async { - final runner = CommandRunner("null_safety_required_props", - "Tooling to codemod over_react prop requiredness in preparation for null safety.") - ..addCommand(CollectCommand()) - ..addCommand(CodemodCommand()); - - try { - await runner.run(args); - } on UsageException catch (e) { - print(e); - exit(ExitCode.usage.code); - } -} diff --git a/lib/src/executables/required_flux_props.dart b/lib/src/executables/required_flux_props.dart deleted file mode 100644 index dbba8707..00000000 --- a/lib/src/executables/required_flux_props.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023 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:logging/logging.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/required_flux_props.dart'; -import 'package:over_react_codemod/src/ignoreable.dart'; -import 'package:over_react_codemod/src/util.dart'; -import 'package:over_react_codemod/src/util/package_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:required_flux_props -"""; - -final _log = Logger('orcm.required_flux_props'); - -Future pubGetForAllPackageRoots(Iterable files) async { - _log.info( - 'Running `pub get` if needed so that all Dart files can be resolved...'); - final packageRoots = files.map(findPackageRootFor).toSet(); - for (final packageRoot in packageRoots) { - await runPubGetIfNeeded(packageRoot); - } -} - -void main(List args) async { - final parser = ArgParser.allowAnything(); - - final parsedArgs = parser.parse(args); - final dartPaths = allDartPathsExceptHidden(); - - await pubGetForAllPackageRoots(dartPaths); - - exitCode = await runInteractiveCodemod( - dartPaths, - aggregate([ - RequiredFluxProps(), - ].map((s) => ignoreable(s))), - defaultYes: true, - args: parsedArgs.rest, - additionalHelpOutput: parser.usage, - changesRequiredOutput: _changesRequiredOutput, - ); -} diff --git a/lib/src/intl_suggestors/intl_importer.dart b/lib/src/intl_suggestors/intl_importer.dart index 636509cf..dc8b99fa 100644 --- a/lib/src/intl_suggestors/intl_importer.dart +++ b/lib/src/intl_suggestors/intl_importer.dart @@ -148,7 +148,7 @@ _InsertionLocation _insertionLocationForPackageImport( final uriContent = importDirective.uri.stringValue; if (uriContent != null) { final uri = Uri.parse(uriContent); - return uri != null && uri.scheme != 'package' && uri.scheme != 'dart'; + return uri.scheme != 'package' && uri.scheme != 'dart'; } return true; }); diff --git a/lib/src/mui_suggestors/system_props_to_sx_migrator.dart b/lib/src/mui_suggestors/system_props_to_sx_migrator.dart index 14cfa404..6bb6e58c 100644 --- a/lib/src/mui_suggestors/system_props_to_sx_migrator.dart +++ b/lib/src/mui_suggestors/system_props_to_sx_migrator.dart @@ -163,7 +163,7 @@ class SystemPropsToSxMigrator extends ComponentUsageMigrator { final fixmes = [ if (anySystemPropSetBeforeForwarding) - 'Previously, it was possible for forwarded system props to overwrite these migrated styles, but not anymore since sx takes precedence over any system props.' + 'Previously, it was possible for forwarded system props to overwrite these migrated styles, but not anymore since sx takes precedence over any system props.' + '\n Double-check that this new behavior is okay.', ]; String getFixmesSource() { diff --git a/lib/src/util/importer.dart b/lib/src/util/importer.dart index 954754b6..44a37eff 100644 --- a/lib/src/util/importer.dart +++ b/lib/src/util/importer.dart @@ -16,9 +16,6 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/source/line_info.dart'; import 'package:codemod/codemod.dart'; import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger('muiImporter'); /// Creates a suggestor that adds [importUri] imports in libraries that reference /// the [importNamespace] (including in parts) but don't yet import it. diff --git a/lib/src/util/unused_import_remover.dart b/lib/src/util/unused_import_remover.dart index 07aeb559..fc92d46f 100644 --- a/lib/src/util/unused_import_remover.dart +++ b/lib/src/util/unused_import_remover.dart @@ -15,9 +15,6 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/syntactic_entity.dart'; import 'package:codemod/codemod.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger('unusedWsdImportRemover'); /// Creates a suggestor that removes unused [package] imports. Suggestor unusedImportRemoverSuggestorBuilder(String package) { diff --git a/pubspec.yaml b/pubspec.yaml index 3360b91a..3ac28c13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ description: > environment: - sdk: '>=2.19.0 <3.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: analyzer: ^5.0.0 @@ -16,10 +16,8 @@ dependencies: codemod: ^1.0.1 collection: ^1.15.0 glob: ^2.0.1 - json_annotation: ^4.8.0 logging: ^1.0.1 meta: ^1.16.0 - package_config: ^2.1.0 path: ^1.8.0 pub_semver: ^2.0.0 source_span: ^1.8.1 @@ -27,7 +25,6 @@ dependencies: yaml_edit: ^2.0.0 file: ^6.1.2 io: ^1.0.0 - w_transport: ^5.2.1 dev_dependencies: async: ^2.0.0 @@ -45,12 +42,8 @@ dev_dependencies: executables: dart2_9_upgrade: dependency_validator_ignore: - null_safety_migrator_companion: - null_safety_required_props: - null_safety_prep: mui_migration: mui_system_props_migration: - required_flux_props: rmui_preparation: rmui_bundle_update: intl_message_migration: diff --git a/test/dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor_test.dart b/test/dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor_test.dart deleted file mode 100644 index 828be85a..00000000 --- a/test/dart3_suggestors/null_safety_prep/callback_ref_hint_suggestor_test.dart +++ /dev/null @@ -1,344 +0,0 @@ -// 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/callback_ref_hint_suggestor.dart'; -import 'package:test/test.dart'; - -import '../../mui_suggestors/components/shared.dart'; -import '../../resolved_file_context.dart'; -import '../../util.dart'; -import '../../util/component_usage_migrator_test.dart'; - -void main() { - final resolvedContext = SharedAnalysisContext.wsd; - - // 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('CallbackRefHintSuggestor', () { - final testSuggestor = getSuggestorTester( - CallbackRefHintSuggestor(), - resolvedContext: resolvedContext, - ); - - group('adds nullability hint to ref prop typed parameters', () { - test('', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (ButtonElement r) => ref = r)(); - (Dom.div()..ref = (ButtonElement r) { ref = r; })(); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (ButtonElement /*?*/ r) => ref = r)(); - (Dom.div()..ref = (ButtonElement /*?*/ r) { ref = r; })(); - ref; - } - '''), - ); - }); - - test('for builders', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (ButtonElement r) => ref = r); - (Dom.div()..ref = (ButtonElement r) { ref = r; }); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (ButtonElement /*?*/ r) => ref = r); - (Dom.div()..ref = (ButtonElement /*?*/ r) { ref = r; }); - ref; - } - '''), - ); - }); - }); - - group('adds nullability hint to casts in a callback ref body', () { - test('', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (r) => ref = r as ButtonElement)(); - (Dom.div()..ref = (r) { ref = r as ButtonElement; })(); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (r) => ref = r as ButtonElement /*?*/)(); - (Dom.div()..ref = (r) { ref = r as ButtonElement /*?*/; })(); - ref; - } - '''), - ); - }); - - test('for builders', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (r) => ref = r as ButtonElement); - (Dom.div()..ref = (r) { ref = r as ButtonElement; }); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - (ButtonToolbar()..ref = (r) => ref = r as ButtonElement /*?*/); - (Dom.div()..ref = (r) { ref = r as ButtonElement /*?*/; }); - ref; - } - '''), - ); - }); - - test('only for casts of the ref param', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - final a = 1; - (ButtonToolbar() - ..ref = (r) { - ref = r as int; - ref = a as ButtonElement; - ref as int; - ref = r as ButtonElement; - })(); - (Dom.div() - ..ref = (ButtonElement r) { - ref = r as int; - ref = a as ButtonElement; - })(); - (ButtonToolbar()..ref = (ButtonElement r) => ref = a as ButtonElement)(); - (ButtonToolbar()..ref = (_) => ref = a as ButtonElement)(); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - var ref; - final a = 1; - (ButtonToolbar() - ..ref = (r) { - ref = r as int /*?*/; - ref = a as ButtonElement; - ref as int; - ref = r as ButtonElement /*?*/; - })(); - (Dom.div() - ..ref = (ButtonElement /*?*/ r) { - ref = r as int /*?*/; - ref = a as ButtonElement; - })(); - (ButtonToolbar()..ref = (ButtonElement /*?*/ r) => ref = a as ButtonElement)(); - (ButtonToolbar()..ref = (_) => ref = a as ButtonElement)(); - ref; - } - '''), - ); - }); - }); - - group('adds nullability hint to class ref variables', () { - test('', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - ButtonElement ref1; - content() { - ButtonElement ref2; - ButtonElement ref3; - (ButtonToolbar()..ref = (r) => ref1 = r)(); - (Dom.div()..ref = (r) { - ButtonElement ref4; - ref2 = r; - final a = ButtonElement(); - ref3 = a; - ref4 = r; - ref4; - }); - ref1; - ref2; - ref3; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - ButtonElement /*?*/ ref1; - content() { - ButtonElement /*?*/ ref2; - ButtonElement ref3; - (ButtonToolbar()..ref = (r) => ref1 = r)(); - (Dom.div()..ref = (r) { - ButtonElement /*?*/ ref4; - ref2 = r; - final a = ButtonElement(); - ref3 = a; - ref4 = r; - ref4; - }); - ref1; - ref2; - ref3; - } - '''), - ); - }); - - test('unless there is no type on the declaration', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - dynamic ref1; - var ref2; - (ButtonToolbar()..ref = (r) { - ref1 = r; - ref2 = r; - }); - ref1; - ref2; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - dynamic ref1; - var ref2; - (ButtonToolbar()..ref = (r) { - ref1 = r; - ref2 = r; - }); - ref1; - ref2; - } - '''), - ); - }); - }); - - test('does not add hints if they already exist', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - ButtonElement /*?*/ ref; - (ButtonToolbar()..ref = (ButtonElement /*?*/ r) => ref = r)(); - (Dom.div()..ref = (r) { ref = r as ButtonElement /*?*/; })(); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - ButtonElement /*?*/ ref; - (ButtonToolbar()..ref = (ButtonElement /*?*/ r) => ref = r)(); - (Dom.div()..ref = (r) { ref = r as ButtonElement /*?*/; })(); - ref; - } - '''), - ); - }); - - test('does not add hints for non-ref props', () async { - await testSuggestor( - input: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - ButtonElement ref; - (ButtonToolbar()..onClick = (r) { ref = r as ButtonElement; })(); - ref; - } - '''), - expectedOutput: withOverReactAndWsdImports(/*language=dart*/ ''' - content() { - ButtonElement ref; - (ButtonToolbar()..onClick = (r) { ref = r as ButtonElement; })(); - ref; - } - '''), - ); - }); - - group('makes no update if file is already on a null safe Dart version', () { - final resolvedContext = SharedAnalysisContext.overReactNullSafe; - - // 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); - - late SuggestorTester nullSafeTestSuggestor; - - setUp(() { - nullSafeTestSuggestor = getSuggestorTester( - CallbackRefHintSuggestor(), - resolvedContext: resolvedContext, - ); - }); - - test('', () async { - await nullSafeTestSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(/*language=dart*/ ''' - import 'dart:html'; - - content() { - var ref; - (Dom.div()..ref = (ButtonElement r) { ref = r; })(); - ref; - } - '''), - ); - }); - - test('unless there is a lang version comment', () async { - await nullSafeTestSuggestor( - input: withOverReactImport(/*language=dart*/ ''' - import 'dart:html'; - - content() { - var ref; - (Dom.div()..ref = (ButtonElement r) { ref = r; })(); - ref; - } - ''', filePrefix: '// @dart=2.11\n'), - expectedOutput: withOverReactImport(/*language=dart*/ ''' - import 'dart:html'; - - content() { - var ref; - (Dom.div()..ref = (ButtonElement /*?*/ r) { ref = r; })(); - ref; - } - ''', filePrefix: '// @dart=2.11\n'), - // Ignore error on language version comment. - isExpectedError: (error) => - error.errorCode.name.toLowerCase() == - 'illegal_language_version_override', - ); - }); - }); - }, tags: 'wsd'); -} diff --git a/test/dart3_suggestors/null_safety_prep/class_component_required_default_props_test.dart b/test/dart3_suggestors/null_safety_prep/class_component_required_default_props_test.dart deleted file mode 100644 index b56b35bf..00000000 --- a/test/dart3_suggestors/null_safety_prep/class_component_required_default_props_test.dart +++ /dev/null @@ -1,716 +0,0 @@ -// 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/class_component_required_default_props.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; - -import '../../resolved_file_context.dart'; -import '../../util.dart'; -import '../../util/component_usage_migrator_test.dart' show withOverReactImport; - -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('ClassComponentRequiredDefaultPropsMigrator', () { - late SuggestorTester testSuggestor; - - group('when sdkVersion is not set', () { - setUp(() { - testSuggestor = getSuggestorTester( - ClassComponentRequiredDefaultPropsMigrator(), - resolvedContext: resolvedContext, - ); - }); - - test('patches defaulted props in mixins', () async { - await testSuggestor( - expectedPatchCount: 7, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - String notDefaulted; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - String defaultedNullable; - num defaultedNonNullable; - var untypedDefaultedNonNullable; - var untypedDefaultedNullable; - var untypedNotDefaulted; - } - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - Function defaultedNonNullableFn; - List defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..untypedDefaultedNonNullable = 1 - ..untypedDefaultedNullable = null - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = [] - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - String notDefaulted; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; - /*late*/ dynamic/*!*/ untypedDefaultedNonNullable; - /*late*/ dynamic/*?*/ untypedDefaultedNullable; - var untypedNotDefaulted; - } - mixin SomeOtherPropsMixin on UiProps { - /*late*/ num/*!*/ anotherDefaultedNonNullable; - /*late*/ Function/*!*/ defaultedNonNullableFn; - /*late*/ List/*!*/ defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..untypedDefaultedNonNullable = 1 - ..untypedDefaultedNullable = null - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = [] - ); - - @override - render() => null; - } - '''), - ); - }); - - test( - 'patches defaulted props in mixins when defaults are in the props mixin', - () async { - await testSuggestor( - expectedPatchCount: 5, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - static final defaultProps = Foo() - ..alreadyPatched = 'foo' - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = []; - - String notDefaulted; - /*late*/ String/*!*/ alreadyPatched; - String defaultedNullable; - num defaultedNonNullable; - } - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - Function defaultedNonNullableFn; - List defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => FooPropsMixin.defaultProps; - - @override - render() => null; - } - - @Factory() - UiFactory FooLegacy = _$FooLegacy; // ignore: undefined_identifier - @Component() - class FooLegacyComponent extends UiComponent { - @override - getDefaultProps() => FooPropsMixin.defaultProps; - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - static final defaultProps = Foo() - ..alreadyPatched = 'foo' - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = []; - - String notDefaulted; - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; - } - mixin SomeOtherPropsMixin on UiProps { - /*late*/ num/*!*/ anotherDefaultedNonNullable; - /*late*/ Function/*!*/ defaultedNonNullableFn; - /*late*/ List/*!*/ defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => FooPropsMixin.defaultProps; - - @override - render() => null; - } - - @Factory() - UiFactory FooLegacy = _$FooLegacy; // ignore: undefined_identifier - @Component() - class FooLegacyComponent extends UiComponent { - @override - getDefaultProps() => FooPropsMixin.defaultProps; - - @override - render() => null; - } - '''), - ); - }); - - test('patches defaulted props in abstract classes', () async { - await testSuggestor( - expectedPatchCount: 7, - input: withOverReactImport(/*language=dart*/ r''' - mixin FooPropsMixin on UiProps { - String notDefaulted; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - String defaultedNullable; - num defaultedNonNullable; - var untypedDefaultedNonNullable; - var untypedDefaultedNullable; - var untypedNotDefaulted; - } - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - Function defaultedNonNullableFn; - List defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - abstract class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..untypedDefaultedNonNullable = 1 - ..untypedDefaultedNullable = null - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = [] - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - mixin FooPropsMixin on UiProps { - String notDefaulted; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; - /*late*/ dynamic/*!*/ untypedDefaultedNonNullable; - /*late*/ dynamic/*?*/ untypedDefaultedNullable; - var untypedNotDefaulted; - } - mixin SomeOtherPropsMixin on UiProps { - /*late*/ num/*!*/ anotherDefaultedNonNullable; - /*late*/ Function/*!*/ defaultedNonNullableFn; - /*late*/ List/*!*/ defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - abstract class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..untypedDefaultedNonNullable = 1 - ..untypedDefaultedNullable = null - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = [] - ); - - @override - render() => null; - } - '''), - ); - }); - - test('patches defaulted props in legacy classes', () async { - await testSuggestor( - expectedPatchCount: 3, - input: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @PropsMixin() - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - } - @Props() - class FooProps extends UiProps with SomeOtherPropsMixin { - String notDefaulted; - String defaultedNullable; - num defaultedNonNullable; - } - @Component() - class FooComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @PropsMixin() - mixin SomeOtherPropsMixin on UiProps { - /*late*/ num/*!*/ anotherDefaultedNonNullable; - } - @Props() - class FooProps extends UiProps with SomeOtherPropsMixin { - String notDefaulted; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; - } - @Component() - class FooComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - ); - }); - - test( - 'patches defaulted props in legacy classes using component1 boilerplate', - () async { - await testSuggestor( - expectedPatchCount: 2, - input: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class _$FooProps extends UiProps { - String notDefaulted; - String defaultedNullable; - num defaultedNonNullable; - } - @Component() - class FooComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ); - - @override - render() => null; - } - class FooProps extends _$FooProps - with - // ignore: mixin_of_non_class, undefined_class - _$FooPropsAccessorsMixin { - // ignore: const_initialized_with_non_constant_value, undefined_class, undefined_identifier - static const PropsMeta meta = _$metaForFooProps; - } - abstract class _$FooPropsAccessorsMixin implements _$FooProps { - set defaultedNullable(val) {} - get defaultedNullable => ''; - set defaultedNonNullable(val) {} - get defaultedNonNullable => 1; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class _$FooProps extends UiProps { - String notDefaulted; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; - } - @Component() - class FooComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ); - - @override - render() => null; - } - class FooProps extends _$FooProps - with - // ignore: mixin_of_non_class, undefined_class - _$FooPropsAccessorsMixin { - // ignore: const_initialized_with_non_constant_value, undefined_class, undefined_identifier - static const PropsMeta meta = _$metaForFooProps; - } - abstract class _$FooPropsAccessorsMixin implements _$FooProps { - set defaultedNullable(val) {} - get defaultedNullable => ''; - set defaultedNonNullable(val) {} - get defaultedNonNullable => 1; - } - '''), - ); - }); - }); - - group('when sdkVersion is set to 2.19.6', () { - setUp(() { - testSuggestor = getSuggestorTester( - ClassComponentRequiredDefaultPropsMigrator(Version.parse('2.19.6')), - resolvedContext: resolvedContext, - ); - }); - - test('patches defaulted props in mixins', () async { - await testSuggestor( - isExpectedError: (err) { - return err.message.contains(RegExp(r"Unexpected text 'late'")); - }, - expectedPatchCount: 3, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - /// This is a doc comment - late String alreadyPatched; - String notDefaulted; - String defaultedNullable; - num defaultedNonNullable; - } - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - - @Factory() - UiFactory FooLegacy = _$FooLegacy; // ignore: undefined_identifier - @Component() - class FooLegacyComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..alreadyPatched = 'foo' - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - /// This is a doc comment - late String alreadyPatched; - String notDefaulted; - late String? defaultedNullable; - late num defaultedNonNullable; - } - mixin SomeOtherPropsMixin on UiProps { - late num anotherDefaultedNonNullable; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - - @Factory() - UiFactory FooLegacy = _$FooLegacy; // ignore: undefined_identifier - @Component() - class FooLegacyComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..alreadyPatched = 'foo' - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - ); - }); - - test('patches defaulted props in legacy classes', () async { - await testSuggestor( - expectedPatchCount: 3, - input: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @PropsMixin() - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - } - @Props() - class FooProps extends UiProps with SomeOtherPropsMixin { - String notDefaulted; - String defaultedNullable; - num defaultedNonNullable; - } - @Component() - class FooComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @PropsMixin() - mixin SomeOtherPropsMixin on UiProps { - late num anotherDefaultedNonNullable; - } - @Props() - class FooProps extends UiProps with SomeOtherPropsMixin { - String notDefaulted; - late String? defaultedNullable; - late num defaultedNonNullable; - } - @Component() - class FooComponent extends UiComponent { - @override - getDefaultProps() => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - ); - }); - }); - - group('makes no update if file is already on a null safe Dart version', () { - final resolvedContext = SharedAnalysisContext.overReactNullSafe; - - // 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); - - late SuggestorTester nullSafeTestSuggestor; - - setUp(() { - nullSafeTestSuggestor = getSuggestorTester( - ClassComponentRequiredDefaultPropsMigrator(), - resolvedContext: resolvedContext, - ); - }); - - test('', () async { - await nullSafeTestSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - String? prop1; - late String prop2; - num? prop3; - } - mixin SomeOtherPropsMixin on UiProps { - num? prop4; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..prop2 = 'foo' - ..prop3 = 1 - ..prop4 = null - ); - - @override - render() => null; - } - '''), - ); - }); - - test('unless there is a lang version comment', () async { - await nullSafeTestSuggestor( - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - String notDefaulted; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - String defaultedNullable; - num defaultedNonNullable; - var untypedDefaultedNonNullable; - var untypedDefaultedNullable; - var untypedNotDefaulted; - } - mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; - Function defaultedNonNullableFn; - List defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..untypedDefaultedNonNullable = 1 - ..untypedDefaultedNullable = null - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = [] - ); - - @override - render() => null; - } - ''', filePrefix: '// @dart=2.11\n'), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooPropsMixin on UiProps { - String notDefaulted; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; - /*late*/ dynamic/*!*/ untypedDefaultedNonNullable; - /*late*/ dynamic/*?*/ untypedDefaultedNullable; - var untypedNotDefaulted; - } - mixin SomeOtherPropsMixin on UiProps { - /*late*/ num/*!*/ anotherDefaultedNonNullable; - /*late*/ Function/*!*/ defaultedNonNullableFn; - /*late*/ List/*!*/ defaultedNonNullableList; - } - class FooProps = UiProps with FooPropsMixin, SomeOtherPropsMixin; - class FooComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..alreadyPatched = 'foo' - ..untypedDefaultedNonNullable = 1 - ..untypedDefaultedNullable = null - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ..defaultedNonNullableFn = () {} - ..defaultedNonNullableList = [] - ); - - @override - render() => null; - } - ''', filePrefix: '// @dart=2.11\n'), - // Ignore error on language version comment. - isExpectedError: (error) => - error.errorCode.name.toLowerCase() == - 'illegal_language_version_override', - ); - }); - }); - }); -} diff --git a/test/dart3_suggestors/null_safety_prep/class_component_required_initial_state_test.dart b/test/dart3_suggestors/null_safety_prep/class_component_required_initial_state_test.dart deleted file mode 100644 index d1876cd8..00000000 --- a/test/dart3_suggestors/null_safety_prep/class_component_required_initial_state_test.dart +++ /dev/null @@ -1,392 +0,0 @@ -// 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/class_component_required_initial_state.dart'; -import 'package:test/test.dart'; - -import '../../resolved_file_context.dart'; -import '../../util.dart'; -import '../../util/component_usage_migrator_test.dart' show withOverReactImport; - -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('ClassComponentRequiredInitialStateMigrator', () { - late SuggestorTester testSuggestor; - - group('when sdkVersion is not set', () { - setUp(() { - testSuggestor = getSuggestorTester( - ClassComponentRequiredInitialStateMigrator(), - resolvedContext: resolvedContext, - ); - }); - - test('patches initialized state fields in mixins', () async { - await testSuggestor( - expectedPatchCount: 5, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps {} - mixin FooStateMixin on UiState { - String notInitialized; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - String initializedNullable; - num initializedNonNullable; - } - mixin SomeOtherStateMixin on UiState { - num anotherInitializedNonNullable; - Function initializedNonNullableFn; - List initializedNonNullableList; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState() - ..alreadyPatched = 'foo' - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ..anotherInitializedNonNullable = 1.1 - ..initializedNonNullableFn = () {} - ..initializedNonNullableList = [] - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps {} - mixin FooStateMixin on UiState { - String notInitialized; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - /*late*/ String/*?*/ initializedNullable; - /*late*/ num/*!*/ initializedNonNullable; - } - mixin SomeOtherStateMixin on UiState { - /*late*/ num/*!*/ anotherInitializedNonNullable; - /*late*/ Function/*!*/ initializedNonNullableFn; - /*late*/ List/*!*/ initializedNonNullableList; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState() - ..alreadyPatched = 'foo' - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ..anotherInitializedNonNullable = 1.1 - ..initializedNonNullableFn = () {} - ..initializedNonNullableList = [] - ); - - @override - render() => null; - } - '''), - ); - }); - - test('patches initialized state in legacy classes', () async { - await testSuggestor( - expectedPatchCount: 3, - input: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class FooProps extends UiProps {} - @StateMixin() - mixin SomeOtherStateMixin on UiState { - num anotherInitializedNonNullable; - } - @State() - class FooState extends UiState with SomeOtherStateMixin { - String notInitialized; - String initializedNullable; - num initializedNonNullable; - } - @Component() - class FooComponent extends UiStatefulComponent { - @override - getInitialState() => (newState() - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ..anotherInitializedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class FooProps extends UiProps {} - @StateMixin() - mixin SomeOtherStateMixin on UiState { - /*late*/ num/*!*/ anotherInitializedNonNullable; - } - @State() - class FooState extends UiState with SomeOtherStateMixin { - String notInitialized; - /*late*/ String/*?*/ initializedNullable; - /*late*/ num/*!*/ initializedNonNullable; - } - @Component() - class FooComponent extends UiStatefulComponent { - @override - getInitialState() => (newState() - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ..anotherInitializedNonNullable = 1.1 - ); - - @override - render() => null; - } - '''), - ); - }); - - test( - 'patches initialized state in legacy classes using component1 boilerplate', - () async { - await testSuggestor( - expectedPatchCount: 2, - input: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class _$FooProps extends UiProps {} - @State() - class _$FooState extends UiState { - String notInitialized; - String initializedNullable; - num initializedNonNullable; - } - @Component() - class FooComponent extends UiStatefulComponent { - @override - getInitialState() => (newState() - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ); - - @override - render() => null; - } - class FooProps extends _$FooProps - with - // ignore: mixin_of_non_class, undefined_class - _$FooPropsAccessorsMixin { - // ignore: const_initialized_with_non_constant_value, undefined_class, undefined_identifier - static const PropsMeta meta = _$metaForFooProps; - } - class FooState extends _$FooState - with - // ignore: mixin_of_non_class, undefined_class - _$FooStateAccessorsMixin { - // ignore: const_initialized_with_non_constant_value, undefined_class, undefined_identifier - static const StateMeta meta = _$metaForFooState; - } - abstract class _$FooStateAccessorsMixin implements _$FooState { - set initializedNullable(val) {} - get initializedNullable => ''; - set initializedNonNullable(val) {} - get initializedNonNullable => 1; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class _$FooProps extends UiProps {} - @State() - class _$FooState extends UiState { - String notInitialized; - /*late*/ String/*?*/ initializedNullable; - /*late*/ num/*!*/ initializedNonNullable; - } - @Component() - class FooComponent extends UiStatefulComponent { - @override - getInitialState() => (newState() - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ); - - @override - render() => null; - } - class FooProps extends _$FooProps - with - // ignore: mixin_of_non_class, undefined_class - _$FooPropsAccessorsMixin { - // ignore: const_initialized_with_non_constant_value, undefined_class, undefined_identifier - static const PropsMeta meta = _$metaForFooProps; - } - class FooState extends _$FooState - with - // ignore: mixin_of_non_class, undefined_class - _$FooStateAccessorsMixin { - // ignore: const_initialized_with_non_constant_value, undefined_class, undefined_identifier - static const StateMeta meta = _$metaForFooState; - } - abstract class _$FooStateAccessorsMixin implements _$FooState { - set initializedNullable(val) {} - get initializedNullable => ''; - set initializedNonNullable(val) {} - get initializedNonNullable => 1; - } - '''), - ); - }); - }); - - group('makes no update if file is already on a null safe Dart version', () { - final resolvedContext = SharedAnalysisContext.overReactNullSafe; - - // 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); - - late SuggestorTester nullSafeTestSuggestor; - - setUp(() { - nullSafeTestSuggestor = getSuggestorTester( - ClassComponentRequiredInitialStateMigrator(), - resolvedContext: resolvedContext, - ); - }); - - test('', () async { - await nullSafeTestSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps {} - mixin FooStateMixin on UiState { - String? state1; - /// This is a doc comment - String? state2; - } - mixin SomeOtherStateMixin on UiState { - late num? state3; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState() - ..state1 = 'foo' - ..state3 = null - ); - - @override - render() => null; - } - '''), - ); - }); - - test('unless there is a lang version comment', () async { - await nullSafeTestSuggestor( - expectedPatchCount: 5, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps {} - mixin FooStateMixin on UiState { - String notInitialized; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - String initializedNullable; - num initializedNonNullable; - } - mixin SomeOtherStateMixin on UiState { - num anotherInitializedNonNullable; - Function initializedNonNullableFn; - List initializedNonNullableList; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState() - ..alreadyPatched = 'foo' - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ..anotherInitializedNonNullable = 1.1 - ..initializedNonNullableFn = () {} - ..initializedNonNullableList = [] - ); - - @override - render() => null; - } - ''', filePrefix: '// @dart=2.11\n'), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps {} - mixin FooStateMixin on UiState { - String notInitialized; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*!*/ alreadyPatchedButNoDocComment; - /*late*/ String/*?*/ initializedNullable; - /*late*/ num/*!*/ initializedNonNullable; - } - mixin SomeOtherStateMixin on UiState { - /*late*/ num/*!*/ anotherInitializedNonNullable; - /*late*/ Function/*!*/ initializedNonNullableFn; - /*late*/ List/*!*/ initializedNonNullableList; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState() - ..alreadyPatched = 'foo' - ..initializedNullable = null - ..initializedNonNullable = 2.1 - ..anotherInitializedNonNullable = 1.1 - ..initializedNonNullableFn = () {} - ..initializedNonNullableList = [] - ); - - @override - render() => null; - } - ''', filePrefix: '// @dart=2.11\n'), - // Ignore error on language version comment. - isExpectedError: (error) => - error.errorCode.name.toLowerCase() == - 'illegal_language_version_override', - ); - }); - }); - }); -} diff --git a/test/dart3_suggestors/null_safety_prep/connect_required_props_test.dart b/test/dart3_suggestors/null_safety_prep/connect_required_props_test.dart deleted file mode 100644 index b0c39eb4..00000000 --- a/test/dart3_suggestors/null_safety_prep/connect_required_props_test.dart +++ /dev/null @@ -1,344 +0,0 @@ -// 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/connect_required_props.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( - 'ConnectRequiredProps - adds all connect props to disable required prop validation list', - () { - late SuggestorTester testSuggestor; - - String commonConnectFile(String source, {String filePrefix = ''}) { - return ''' - $filePrefix - $overReactImport - import 'package:over_react/over_react_redux.dart'; - - // ignore: uri_has_not_been_generated - part 'main.over_react.g.dart'; - - class FooState { - num count; - } - $source'''; - } - - setUp(() { - testSuggestor = getSuggestorTester( - ConnectRequiredProps(), - resolvedContext: resolvedContext, - ); - }); - - test('', () async { - final input = ''' - mixin FooProps on UiProps { - num setInMapStateToProps; - Function() setInMapDispatchToProps; - num setInBoth; - String notSetInConnect; - } - - UiFactory Foo = connect( - mapStateToProps: (state) => (Foo() - ..addTestId('abc') - ..setInMapStateToProps = state.count - ..setInBoth = 1 - ), - mapDispatchToProps: (dispatch) => Foo()..setInMapDispatchToProps = (() => null)..setInBoth = 1, - )(uiFunction((props) => (Foo()..notSetInConnect = '1')(), _\$Foo)); - '''; - - await testSuggestor( - input: commonConnectFile(input), - expectedOutput: commonConnectFile(''' - @Props(disableRequiredPropValidation: {'setInMapStateToProps', 'setInBoth', 'setInMapDispatchToProps'}) - $input - '''), - ); - }); - - test('for multiple mixins', () async { - final input = ''' - class FooProps = UiProps with FooPropsMixin, OtherPropsMixin; - - mixin FooPropsMixin on UiProps { - /*late*/ num prop1; - Function()/*?*/ prop2; - String notSetInConnect; - } - - mixin OtherPropsMixin on UiProps { - String otherProp; - String notSetInConnect2; - } - - UiFactory Foo = connect( - mapStateToProps: (state) => (Foo() - ..addTestId('abc') - ..prop1 = state.count - ..otherProp = '1' - ), - mapDispatchToProps: (dispatch) => Foo()..prop2 = (() => null), - )(uiFunction((props) => (Foo()..notSetInConnect = '1'..notSetInConnect2 = '2')(), _\$Foo)); - '''; - - await testSuggestor( - input: commonConnectFile(input), - expectedOutput: commonConnectFile(''' - @Props(disableRequiredPropValidation: {'prop1', 'otherProp', 'prop2'}) - $input - '''), - ); - }); - - group('adds to existing annotations', () { - Future testAnnotations( - {required String input, required String expectedOutput}) async { - final connectBoilerplate = ''' - mixin FooProps on UiProps { - num connectProp1; - Function() connectProp2; - String nonConnectProp; - } - - UiFactory Foo = connect( - mapStateToProps: (state) => (Foo()..connectProp1 = 1), - mapDispatchToProps: (dispatch) => Foo()..connectProp2 = (() => null), - )(uiFunction((props) => (Foo()..nonConnectProp = '1')(), _\$Foo)); - '''; - await testSuggestor( - input: commonConnectFile(''' - $input - $connectBoilerplate - '''), - expectedOutput: commonConnectFile(''' - $expectedOutput - $connectBoilerplate - '''), - ); - } - - test('', () async { - await testAnnotations( - input: '@Props()', - expectedOutput: - '@Props(disableRequiredPropValidation: {\'connectProp1\', \'connectProp2\'})', - ); - }); - - test('with other args', () async { - await testAnnotations( - input: '@Props(keyNamespace: \'\')', - expectedOutput: - '@Props(disableRequiredPropValidation: {\'connectProp1\', \'connectProp2\'}, keyNamespace: \'\')', - ); - }); - - test('with disableRequiredPropValidation', () async { - await testAnnotations( - input: '@Props(disableRequiredPropValidation: {\'connectProp1\'})', - expectedOutput: - '@Props(disableRequiredPropValidation: {\'connectProp2\', \'connectProp1\'})', - ); - }); - }); - - test('recognizes different arg formats', () async { - final input = ''' - mixin FooProps on UiProps { - /*late*/ num count; - Function()/*?*/ increment; - String abc; - } - - UiFactory Foo = connect( - mapStateToProps: (state) { - return (Foo() - ..count = state.count - ); - }, - mapDispatchToProps: (dispatch) { - final foo = (Foo() - ..increment = (() => null) - ); - return foo; - }, - )(_\$Foo); - '''; - await testSuggestor( - input: commonConnectFile(input), - expectedOutput: commonConnectFile(''' - @Props(disableRequiredPropValidation: {'count', 'increment'}) - $input - '''), - ); - }); - - test('only adds props used in specific connect args', () async { - final input = ''' - mixin FooProps on UiProps { - num propInMapStateToProps; - num propInMapStateToPropsWithOwnProps; - num propInMapDispatchToProps; - num propInMapDispatchToPropsWithOwnProps; - num propInMergeProps; - String notUsed; - } - - UiFactory Foo = connect( - mapStateToProps: (_) => (Foo()..propInMapStateToProps = 1), - mapStateToPropsWithOwnProps: (_, __) => (Foo()..propInMapStateToPropsWithOwnProps = 1), - mapDispatchToProps: (_) => (Foo()..propInMapDispatchToProps = 1), - mapDispatchToPropsWithOwnProps: (_, __) => (Foo()..propInMapDispatchToPropsWithOwnProps = 1), - mergeProps: (_, __, ___) => (Foo()..propInMergeProps = 1), - )(_\$Foo); - '''; - await testSuggestor( - input: commonConnectFile(input), - expectedOutput: commonConnectFile(''' - @Props(disableRequiredPropValidation: {'propInMapStateToProps', 'propInMapStateToPropsWithOwnProps', 'propInMapDispatchToProps', 'propInMapDispatchToPropsWithOwnProps'}) - $input - '''), - ); - }); - - test('does not cover certain unlikely edge cases', () async { - final input = ''' - mixin FooProps on UiProps { - num inTearOff; - Function() notReturned; - String notUsed; - } - - final _mapStateToProps = (state) { - return (Foo() - ..inTearOff = state.count - ); - }; - - UiFactory Foo = connect( - mapStateToProps: _mapStateToProps, - mapDispatchToProps: (dispatch) { - final foo = (Foo() - ..notReturned = (() => null) - ); - foo; - return Foo(); - }, - )(_\$Foo); - '''; - await testSuggestor( - input: commonConnectFile(input), - expectedOutput: commonConnectFile(''' - @Props(disableRequiredPropValidation: {'notReturned'}) - $input - '''), - ); - }); - - group('makes no update if file is already on a null safe Dart version', () { - final resolvedContext = SharedAnalysisContext.overReactNullSafe; - - // 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); - - late SuggestorTester nullSafeTestSuggestor; - - setUp(() { - nullSafeTestSuggestor = getSuggestorTester( - ConnectRequiredProps(), - resolvedContext: resolvedContext, - ); - }); - - test('', () async { - await nullSafeTestSuggestor( - expectedPatchCount: 0, - input: ''' - $overReactImport - import 'package:over_react/over_react_redux.dart'; - - // ignore: uri_has_not_been_generated - part 'main.over_react.g.dart'; - - class FooState { - num? count; - } - - mixin FooProps on UiProps { - num? setInMapStateToProps; - Function()? setInMapDispatchToProps; - late num setInBoth; - String? notSetInConnect; - } - - UiFactory Foo = connect( - mapStateToProps: (state) => (Foo() - ..addTestId('abc') - ..setInMapStateToProps = state.count - ..setInBoth = 1 - ), - mapDispatchToProps: (dispatch) => Foo()..setInMapDispatchToProps = (() => null)..setInBoth = 1, - )(uiFunction((props) => (Foo()..notSetInConnect = '1')(), _\$Foo));''', - ); - }); - - test('unless there is a lang version comment', () async { - final input = ''' - mixin FooProps on UiProps { - num setInMapStateToProps; - Function() setInMapDispatchToProps; - num setInBoth; - String notSetInConnect; - } - - UiFactory Foo = connect( - mapStateToProps: (state) => (Foo() - ..addTestId('abc') - ..setInMapStateToProps = state.count - ..setInBoth = 1 - ), - mapDispatchToProps: (dispatch) => Foo()..setInMapDispatchToProps = (() => null)..setInBoth = 1, - )(uiFunction((props) => (Foo()..notSetInConnect = '1')(), _\$Foo)); - '''; - - await nullSafeTestSuggestor( - input: commonConnectFile(input, filePrefix: '// @dart=2.11'), - expectedOutput: commonConnectFile(''' - @Props(disableRequiredPropValidation: {'setInMapStateToProps', 'setInBoth', 'setInMapDispatchToProps'}) - $input - ''', filePrefix: '// @dart=2.11'), - // Ignore error on language version comment. - isExpectedError: (error) => - error.errorCode.name.toLowerCase() == - 'illegal_language_version_override', - ); - }); - }); - }); -} diff --git a/test/dart3_suggestors/null_safety_prep/dom_callback_null_args_test.dart b/test/dart3_suggestors/null_safety_prep/dom_callback_null_args_test.dart deleted file mode 100644 index 57498df8..00000000 --- a/test/dart3_suggestors/null_safety_prep/dom_callback_null_args_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -// 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}()); - } - '''), - ); - }); - }); - }); - }); -} diff --git a/test/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor_test.dart b/test/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor_test.dart deleted file mode 100644 index d7f54777..00000000 --- a/test/dart3_suggestors/null_safety_prep/fn_prop_null_aware_call_suggestor_test.dart +++ /dev/null @@ -1,359 +0,0 @@ -// 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/fn_prop_null_aware_call_suggestor.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('FnPropNullAwareCallSuggestor', () { - late SuggestorTester testSuggestor; - - setUp(() { - testSuggestor = getSuggestorTester( - FnPropNullAwareCallSuggestor(), - resolvedContext: resolvedContext, - ); - }); - - group('handles block if conditions', () { - test('with a single condition', () async { - await testSuggestor( - expectedPatchCount: 1, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick != null) { - props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - expectedOutput: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - props.onClick?.call(e); - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - ''')); - }); - - test( - 'unless the single condition is not a null check of the function being called', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (1 > 0) { - props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless there is an else condition', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final bar = useState(0); - final handleClick = useCallback((e) { - if (props.onClick != null) { - props.onClick(e); - } else { - bar.set(1); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(bar.value); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless there is an else if condition', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final bar = useState(0); - final handleClick = useCallback((e) { - if (props.onMouseEnter != null) { - bar.set(1); - } else if (props.onClick != null) { - props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(bar.value); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test( - 'unless the single condition involves the function being called, but is not a null check', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick is Function) { - props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless the single condition does not involve props at all', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final bar = false; - final handleClick = useCallback((e) { - if (bar) { - props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless there are multiple conditions', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick != null && props.onMouseEnter != null) { - props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless the relevant prop fn is returned within the then statement', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick != null) { - return props.onClick(e); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test( - 'unless the relevant prop fn is not called within the then statement', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final bar = useState(0); - final handleClick = useCallback((e) { - if (props.onClick != null) { - bar.set(1); - } - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless there are multiple statements within the then statement', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick != null) { - props.onMouseEnter?.call(e); - props.onClick(e); - } - }, [props.onClick, props.onMouseEnter]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - }); - - group('handles inline if conditions', () { - test('with a single condition', () async { - await testSuggestor( - expectedPatchCount: 1, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick != null) props.onClick(e); - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - expectedOutput: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - props.onClick?.call(e); - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - ''')); - }); - - test( - 'unless the single condition is not a null check of the function being called', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (1 > 0) props.onClick(e); - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test( - 'unless the single condition involves the function being called, but is not a null check', - () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick is Function) props.onClick(e); - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - - test('unless there are multiple conditions', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(''' - final Foo = uiFunction( - (props) { - final handleClick = useCallback((e) { - if (props.onClick != null && props.onMouseEnter != null) props.onClick(e); - }, [props.onClick]); - - return (Dom.button()..onClick = handleClick)(); - }, - UiFactoryConfig(displayName: 'Foo'), - ); - '''), - ); - }); - }); - }); -} diff --git a/test/dart3_suggestors/null_safety_prep/required_flux_props_test.dart b/test/dart3_suggestors/null_safety_prep/required_flux_props_test.dart deleted file mode 100644 index c8e84f72..00000000 --- a/test/dart3_suggestors/null_safety_prep/required_flux_props_test.dart +++ /dev/null @@ -1,1656 +0,0 @@ -// Copyright 2023 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:meta/meta.dart'; -import 'package:over_react_codemod/src/dart3_suggestors/null_safety_prep/required_flux_props.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('RequiredFluxProps', () { - late SuggestorTester testSuggestor; - - setUp(() { - testSuggestor = getSuggestorTester( - RequiredFluxProps(), - resolvedContext: resolvedContext, - ); - }); - - test( - 'leaves builders alone if they don\'t use FluxUiPropsMixin, ' - 'even if they have props named store/actions', () async { - await testSuggestor( - isExpectedError: (err) => err.message - .contains(RegExp(r"'(theStore|theActions)' isn't used.")), - expectedPatchCount: 0, - input: withFluxComponentUsage(/*language=dart*/ r''' - main() { - final theStore = BazFooStore(); - final theActions = BazFooActions(); - - return (NotFoo() - ..id = '123' - )(); - } - '''), - ); - }); - - test('leaves defaultProps/getDefaultProps alone', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withFluxComponentUsage(/*language=dart*/ r''' - class FizComponent extends FluxUiComponent2 { - @override - getDefaultProps() => newProps()..id = '123'; - - @override - get defaultProps => newProps()..id = '123'; - - @override - render() => null; - } - '''), - ); - }); - - test('leaves PanelTitle/PanelTitleV2 alone', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withMockPanelTitleComponents(/*language=dart*/ r''' - main() { - final pt = (PanelTitle()..id = 'pt')(); - final pt2 = (PanelTitleV2()..id = 'pt2')(); - final pt3 = (_PanelTitle()..id = 'pt3')(); - return [pt, pt2, pt3]; - } - '''), - ); - }); - - test('leaves PanelToolbars alone', () async { - await testSuggestor( - expectedPatchCount: 0, - input: withMockPanelToolbarComponents(/*language=dart*/ r''' - main() { - final pt = (PanelToolbar()..id = 'pt')(); - final pt2 = (_PanelToolbar()..id = 'pt2')(); - return [pt, pt2]; - } - '''), - ); - }); - - @isTestGroup - void sharedTests({required bool invokeBuilder}) { - String maybeInvokeBuilder(String builderString) { - return (!invokeBuilder ? builderString : '($builderString)()') + ';'; - } - - String expectedTodo(String varName) { - return RequiredFluxProps.getTodoForPossiblyValidStoreVar(varName); - } - - group( - 'patches ${invokeBuilder ? 'invoked' : 'un-invoked'} builders that use FluxUiPropsMixin and', - () { - group('have no actions setter', () { - test('when no actions var is available in scope', () async { - await testSuggestor( - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - ${maybeInvokeBuilder('''Foo()..store = theStore''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - test('unless the in-scope var is dynamic', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'notTheActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - dynamic notTheActions = 123; - final theStore = FooStore(); - - ${maybeInvokeBuilder('''Foo()..store = theStore''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - dynamic notTheActions = 123; - final theStore = FooStore(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - test('unless the in-scope var is null', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'notTheActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final notTheActions = null; - final theStore = FooStore(); - - ${maybeInvokeBuilder('''Foo()..store = theStore''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final notTheActions = null; - final theStore = FooStore(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - test('unless TActions is dynamic', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'notTheActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - dynamic notTheActions = 123; - dynamic theStore = FooStore(); - - ${maybeInvokeBuilder('''DynamicFoo()..store = theStore''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - dynamic notTheActions = 123; - dynamic theStore = FooStore(); - - ${maybeInvokeBuilder(''' - DynamicFoo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - group('when a top-level actions var is available', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - final theActions = BazFooActions(); - main() { - final theStore = FooStore(); - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theActions = BazFooActions(); - main() { - final theStore = FooStore(); - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - final theActions = FooActions(); - main() { - final theStore = FooStore(); - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theActions = FooActions(); - main() { - final theStore = FooStore(); - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ..store = theStore - ''')} - } - '''), - ); - }); - }); - - group('when an actions var is available in block function scope', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = BazFooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = BazFooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ..store = theStore - ''')} - } - '''), - ); - }); - }); - - group('when an actions var is available as a function argument', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main(BazFooActions theActions) { - final theStore = FooStore(); - - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main(BazFooActions theActions) { - final theStore = FooStore(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main(FooActions theActions) { - final theStore = FooStore(); - - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main(FooActions theActions) { - final theStore = FooStore(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ..store = theStore - ''')} - } - '''), - ); - }); - }); - - group('when an actions var is available as a class field', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class TheBaz { - final BazFooActions theActions; - final FooStore theStore; - TheBaz(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - } - '''), - expectedOutput: withFluxComponentUsage(''' - class TheBaz { - final BazFooActions theActions; - final FooStore theStore; - TheBaz(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..actions = null - ..store = theStore - ''')} - } - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class TheFoo { - final FooActions theActions; - final FooStore theStore; - TheFoo(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..store = theStore - ''')} - } - } - '''), - expectedOutput: withFluxComponentUsage(''' - class TheFoo { - final FooActions theActions; - final FooStore theStore; - TheFoo(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ..store = theStore - ''')} - } - } - '''), - ); - }); - }); - - group('when an actions var is available in function component props', - () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => err.message.contains('someFunction'), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..store = FooStore() - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - expectedOutput: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..actions = null - ..store = FooStore() - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => err.message.contains('someFunction'), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..store = localProps.store - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - expectedOutput: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..actions = localProps.actions - ..store = localProps.store - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - ); - }); - }); - - group('when an actions var is available in class component props', - () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..store = FooStore() - ''')} - } - - @override - render() => null; - } - '''), - expectedOutput: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..actions = null - ..store = FooStore() - ''')} - } - - @override - render() => null; - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..store = props.store - ''')} - } - - @override - render() => null; - } - '''), - expectedOutput: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..actions = props.actions - ..store = props.store - ''')} - } - - @override - render() => null; - } - '''), - ); - }); - }); - }); - - group('have no store setter', () { - test('when no store var is available in scope', () async { - await testSuggestor( - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - - test('unless TStore is dynamic', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'notTheStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - dynamic notTheStore = 123; - dynamic theActions = FooActions(); - - ${maybeInvokeBuilder('''DynamicFoo()..actions = theActions''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - dynamic notTheStore = 123; - dynamic theActions = FooActions(); - - ${maybeInvokeBuilder(''' - DynamicFoo() - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - - group('when a top-level store var is available', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - final theStore = BazFooStore(); - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theStore = BazFooStore(); - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - final theStore = FooStore(); - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theStore = FooStore(); - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo()${expectedTodo('theStore')} - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - }); - - group('when a store var is available in block function scope', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final theStore = BazFooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = BazFooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo()${expectedTodo('theStore')} - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - }); - - group('when a store var is available as a function argument', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main(BazFooStore theStore) { - final theActions = FooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main(BazFooStore theStore) { - final theActions = FooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - main(FooStore theStore) { - final theActions = FooActions(); - - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main(FooStore theStore) { - final theActions = FooActions(); - - ${maybeInvokeBuilder(''' - Foo()${expectedTodo('theStore')} - ..store = null - ..actions = theActions - ''')} - } - '''), - ); - }); - }); - - group('when a store var is available as a class field', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class TheBaz { - final FooActions theActions; - final BazFooStore theStore; - TheBaz(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ''')} - } - } - '''), - expectedOutput: withFluxComponentUsage(''' - class TheBaz { - final FooActions theActions; - final BazFooStore theStore; - TheBaz(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..store = null - ..actions = theActions - ''')} - } - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class TheFoo { - final FooActions theActions; - final FooStore theStore; - TheFoo(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo() - ..actions = theActions - ''')} - } - } - '''), - expectedOutput: withFluxComponentUsage(''' - class TheFoo { - final FooActions theActions; - final FooStore theStore; - TheFoo(this.theActions, this.theStore); - - someMethod() { - ${maybeInvokeBuilder(''' - Foo()${expectedTodo('theStore')} - ..store = null - ..actions = theActions - ''')} - } - } - '''), - ); - }); - }); - - group('when a store var is available in function component props', - () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - isExpectedError: (err) => err.message.contains('someFunction'), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..actions = FooActions() - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - expectedOutput: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = FooActions() - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - isExpectedError: (err) => err.message.contains('someFunction'), - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..actions = localProps.actions - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - expectedOutput: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo()${expectedTodo('localProps.store')} - ..store = null - ..actions = localProps.actions - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - ); - }); - }); - - group('when a store var is available in class component props', () { - test('unless the type does not match (uses null instead)', - () async { - await testSuggestor( - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..actions = FooActions() - ''')} - } - - @override - render() => null; - } - '''), - expectedOutput: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = FooActions() - ''')} - } - - @override - render() => null; - } - '''), - ); - }); - - test('and the type matches', () async { - await testSuggestor( - expectedPatchCount: 1, - input: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..actions = props.actions - ''')} - } - - @override - render() => null; - } - '''), - expectedOutput: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo()${expectedTodo('props.store')} - ..store = null - ..actions = props.actions - ''')} - } - - @override - render() => null; - } - '''), - ); - }); - }); - }); - - group('have no store or actions setter', () { - test('when no store or actions var is available in scope', () async { - await testSuggestor( - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - main() { - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = null - ..id = '123' - ''')} - } - '''), - ); - }); - - group('when store and/or actions var(s) are available', () { - group('in top-level scope', () { - test('unless the type(s) do not match (uses null instead):', - () async { - await testSuggestor( - isExpectedError: (err) => err.message - .contains(RegExp(r"'(theStore|theActions)' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - final theStore = BazFooStore(); - final theActions = BazFooActions(); - main() { - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theStore = BazFooStore(); - final theActions = BazFooActions(); - main() { - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = null - ..id = '123' - ''')} - } - '''), - ); - }); - - group('and the type(s) match:', () { - test('store AND actions', () async { - await testSuggestor( - isExpectedError: (err) => err.message.contains( - RegExp(r"'(theStore|theActions)' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - final theStore = FooStore(); - final theActions = FooActions(); - main() { - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theStore = FooStore(); - final theActions = FooActions(); - main() { - ${maybeInvokeBuilder('''Foo()${expectedTodo('theStore')} - ..store = null - ..actions = theActions - ..id = '123' - ''')} - } - '''), - ); - }); - - test('store only', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - final theStore = FooStore(); - main() { - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theStore = FooStore(); - main() { - ${maybeInvokeBuilder('''Foo()${expectedTodo('theStore')} - ..store = null - ..actions = null - ..id = '123' - ''')} - } - '''), - ); - }); - - test('actions only', () async { - await testSuggestor( - isExpectedError: (err) => err.message - .contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - final theActions = FooActions(); - main() { - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - final theActions = FooActions(); - main() { - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = theActions - ..id = '123' - ''')} - } - '''), - ); - }); - }); - }); - - group('in block function scope', () { - test('unless the type(s) do not match (uses null instead):', - () async { - await testSuggestor( - isExpectedError: (err) => err.message - .contains(RegExp(r"'(theStore|theActions)' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - main() { - final theStore = BazFooStore(); - final theActions = BazFooActions(); - - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = BazFooStore(); - final theActions = BazFooActions(); - - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = null - ..id = '123' - ''')} - } - '''), - ); - }); - - group('and the type(s) match:', () { - test('store AND actions', () async { - await testSuggestor( - isExpectedError: (err) => err.message.contains( - RegExp(r"'(theStore|theActions)' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo()${expectedTodo('theStore')} - ..store = null - ..actions = theActions - ..id = '123' - ''')} - } - '''), - ); - }); - - test('store only', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains(RegExp(r"'theStore' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theStore = FooStore(); - - ${maybeInvokeBuilder('''Foo()${expectedTodo('theStore')} - ..store = null - ..actions = null - ..id = '123' - ''')} - } - '''), - ); - }); - - test('actions only', () async { - await testSuggestor( - isExpectedError: (err) => err.message - .contains(RegExp(r"'theActions' isn't used.")), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - '''), - expectedOutput: withFluxComponentUsage(''' - main() { - final theActions = FooActions(); - - ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = theActions - ..id = '123' - ''')} - } - '''), - ); - }); - }); - }); - - group('in function component props', () { - test('unless the types do not match (uses null instead):', - () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains('someFunction'), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - expectedOutput: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = null - ..id = '123' - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - ); - }); - - test('and the types match:', () async { - await testSuggestor( - isExpectedError: (err) => - err.message.contains('someFunction'), - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - expectedOutput: withFluxComponentUsage(''' - class FooConsumerProps = UiProps with FluxUiPropsMixin; - final FooConsumer = uiFunction( - (localProps) { - someFunction() { - return ${maybeInvokeBuilder('''Foo()${expectedTodo('localProps.store')} - ..store = null - ..actions = localProps.actions - ..id = '123' - ''')} - } - - return null; - }, - _\$FooConsumerConfig, // ignore: undefined_identifier - ); - '''), - ); - }); - }); - - group('in class component props', () { - test('unless the types do not match (uses null instead):', - () async { - await testSuggestor( - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - - @override - render() => null; - } - '''), - expectedOutput: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..store = null - ..actions = null - ..id = '123' - ''')} - } - - @override - render() => null; - } - '''), - ); - }); - - test('and the types match:', () async { - await testSuggestor( - expectedPatchCount: 2, - input: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo() - ..id = '123' - ''')} - } - - @override - render() => null; - } - '''), - expectedOutput: withFluxComponentUsage(''' - // ignore: undefined_identifier - UiFactory FooConsumer = castUiFactory(_\$FooConsumer); - class FooConsumerProps = UiProps with FluxUiPropsMixin; - class FooConsumerComponent extends FluxUiComponent2 { - someMethod() { - return ${maybeInvokeBuilder('''Foo()${expectedTodo('props.store')} - ..store = null - ..actions = props.actions - ..id = '123' - ''')} - } - - @override - render() => null; - } - '''), - ); - }); - }); - }); - }); - }); - } - - sharedTests(invokeBuilder: false); - sharedTests(invokeBuilder: true); - }); -} - -String withFluxComponentUsage(String source, - {String? actionsName = 'FooActions', String? storeName = 'FooStore'}) { - String getActionsClasses() => actionsName == null - ? '' - : ''' -class $actionsName { - $actionsName(); -} - -class Baz$actionsName { - Baz$actionsName(); -} -'''; - - String getStoreClasses() => storeName == null - ? '' - : ''' -class $storeName { - $storeName(); -} - -class Baz$storeName { - Baz$storeName(); -} -'''; - - return withOverReactImport('''$source - -${getActionsClasses()} - -${getStoreClasses()} - -UiFactory Foo = castUiFactory(_\$Foo); // ignore: undefined_identifier - -class FooProps = UiProps with FluxUiPropsMixin<${actionsName ?? 'Null'}, ${storeName ?? 'Null'}>; - -class FooComponent extends FluxUiComponent2 { - @override - render() => null; -} - -UiFactory DynamicFoo = castUiFactory(_\$DynamicFoo); // ignore: undefined_identifier - -class DynamicFooProps = UiProps with FluxUiPropsMixin; - -class DynamicFooComponent extends FluxUiComponent2 { - @override - render() => null; -} - -UiFactory NotFoo = castUiFactory(_\$NotFoo); // ignore: undefined_identifier - -mixin NotFooPropsMixin on UiProps { - Baz$storeName store; - Baz$actionsName actions; -} - -class NotFooProps = UiProps with NotFooPropsMixin; - -class NotFooComponent extends UiComponent2 { - @override - render() => null; -}'''); -} - -String withMockPanelTitleComponents(String source) { - return withOverReactImport('''$source - -UiFactory<_PanelTitleProps> _PanelTitle = castUiFactory(_\$_PanelTitle); // ignore: undefined_identifier -class _PanelTitleProps = UiProps with FluxUiPropsMixin; -class LegacyPanelTitleComponent extends FluxUiComponent2<_PanelTitleProps> { - @override - render() => null; -} - -UiFactory PanelTitle = castUiFactory(_\$PanelTitle); // ignore: undefined_identifier -class PanelTitleProps = UiProps with FluxUiPropsMixin; -class PanelTitleComponent extends FluxUiComponent2 { - @override - render() => null; -} - -UiFactory PanelTitleV2 = castUiFactory(_\$PanelTitleV2); // ignore: undefined_identifier -class PanelTitleV2Props = UiProps with FluxUiPropsMixin; -class PanelTitleV2Component extends FluxUiComponent2 { - @override - render() => null; -} - '''); -} - -String withMockPanelToolbarComponents(String source) { - return withOverReactImport('''$source -UiFactory<_PanelToolbarProps> _PanelToolbar = castUiFactory(_\$_PanelToolbar); // ignore: undefined_identifier -class _PanelToolbarProps = UiProps with FluxUiPropsMixin; -class LegacyPanelToolbarComponent extends FluxUiComponent2<_PanelToolbarProps> { - @override - render() => null; -} - -UiFactory PanelToolbar = castUiFactory(_\$PanelToolbar); // ignore: undefined_identifier -class PanelToolbarProps = UiProps with FluxUiPropsMixin; -class PanelToolbarComponent extends FluxUiComponent2 { - @override - render() => null; -} - '''); -} diff --git a/test/dart3_suggestors/null_safety_prep/state_mixin_suggestor_test.dart b/test/dart3_suggestors/null_safety_prep/state_mixin_suggestor_test.dart deleted file mode 100644 index f829f966..00000000 --- a/test/dart3_suggestors/null_safety_prep/state_mixin_suggestor_test.dart +++ /dev/null @@ -1,250 +0,0 @@ -// 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/state_mixin_suggestor.dart'; -import 'package:test/test.dart'; - -import '../../resolved_file_context.dart'; -import '../../util.dart'; -import '../../util/component_usage_migrator_test.dart' show withOverReactImport; - -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('StateMixinSuggestor', () { - late SuggestorTester testSuggestor; - - setUp(() { - testSuggestor = getSuggestorTester( - StateMixinSuggestor(), - resolvedContext: resolvedContext, - ); - }); - - test('patches state fields in mixins', () async { - await testSuggestor( - expectedPatchCount: 3, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps { - String prop1; - } - mixin FooStateMixin on UiState { - String state1; - num state2; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ alreadyPatchedButNoDocComment; - String/*?*/ alreadyPatchedOptional; - } - mixin SomeOtherStateMixin on UiState { - String state3; - String/*?*/ alreadyPatchedOptional2; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps { - String prop1; - } - mixin FooStateMixin on UiState { - String/*?*/ state1; - num/*?*/ state2; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ alreadyPatchedButNoDocComment; - String/*?*/ alreadyPatchedOptional; - } - mixin SomeOtherStateMixin on UiState { - String/*?*/ state3; - String/*?*/ alreadyPatchedOptional2; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - render() => null; - } - '''), - ); - }); - - test('patches state fields in legacy classes', () async { - await testSuggestor( - expectedPatchCount: 3, - input: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class FooProps extends UiProps { - String prop1; - } - @StateMixin() - mixin SomeOtherStateMixin on UiState { - num state1; - } - @State() - class FooState extends UiState with SomeOtherStateMixin { - String state2; - num state3; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ alreadyPatchedButNoDocComment; - String/*?*/ alreadyPatchedOptional; - } - @Component() - class FooComponent extends UiStatefulComponent { - @override - render() => null; - } - '''), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - @Factory() - UiFactory Foo = _$Foo; // ignore: undefined_identifier - @Props() - class FooProps extends UiProps { - String prop1; - } - @StateMixin() - mixin SomeOtherStateMixin on UiState { - num/*?*/ state1; - } - @State() - class FooState extends UiState with SomeOtherStateMixin { - String/*?*/ state2; - num/*?*/ state3; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ alreadyPatchedButNoDocComment; - String/*?*/ alreadyPatchedOptional; - } - @Component() - class FooComponent extends UiStatefulComponent { - @override - render() => null; - } - '''), - ); - }); - - group('makes no update if file is already on a null safe Dart version', () { - final resolvedContext = SharedAnalysisContext.overReactNullSafe; - - // 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); - - late SuggestorTester nullSafeTestSuggestor; - - setUp(() { - nullSafeTestSuggestor = getSuggestorTester( - StateMixinSuggestor(), - resolvedContext: resolvedContext, - ); - }); - - test('', () async { - await nullSafeTestSuggestor( - expectedPatchCount: 0, - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps { - String? prop1; - } - mixin FooStateMixin on UiState { - String? state1; - late num state2; - } - mixin SomeOtherStateMixin on UiState { - String? state3; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - render() => null; - } - '''), - ); - }); - - test('unless there is a lang version comment', () async { - await nullSafeTestSuggestor( - input: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps { - String prop1; - } - mixin FooStateMixin on UiState { - String state1; - num state2; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ alreadyPatchedButNoDocComment; - String/*?*/ alreadyPatchedOptional; - } - mixin SomeOtherStateMixin on UiState { - String state3; - String/*?*/ alreadyPatchedOptional2; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - render() => null; - } - ''', filePrefix: '// @dart=2.11\n'), - expectedOutput: withOverReactImport(/*language=dart*/ r''' - // ignore: undefined_identifier - UiFactory Foo = castUiFactory(_$Foo); - mixin FooProps on UiProps { - String prop1; - } - mixin FooStateMixin on UiState { - String/*?*/ state1; - num/*?*/ state2; - /// This is a doc comment - /*late*/ String/*!*/ alreadyPatched; - /*late*/ String/*?*/ alreadyPatchedButNoDocComment; - String/*?*/ alreadyPatchedOptional; - } - mixin SomeOtherStateMixin on UiState { - String/*?*/ state3; - String/*?*/ alreadyPatchedOptional2; - } - class FooState = UiState with FooStateMixin, SomeOtherStateMixin; - class FooComponent extends UiStatefulComponent2 { - @override - render() => null; - } - ''', filePrefix: '// @dart=2.11\n'), - // Ignore error on language version comment. - isExpectedError: (error) => - error.errorCode.name.toLowerCase() == - 'illegal_language_version_override', - ); - }); - }); - }); -} 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 deleted file mode 100644 index b4fa8552..00000000 --- a/test/dart3_suggestors/null_safety_prep/use_ref_init_migration_test.dart +++ /dev/null @@ -1,128 +0,0 @@ -// 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'), - ); - '''), - ); - }); - - test('removes unnecessary null arguments', () async { - await testSuggestor( - expectedPatchCount: 2, - input: withOverReactImport(''' - useTestHook() { - final foo = useRef(null); - final bar = useRef(null); - return [foo, bar]; - } - '''), - expectedOutput: withOverReactImport(''' - useTestHook() { - final foo = useRef(); - final bar = useRef(); - return [foo, bar]; - } - '''), - ); - }); - }); -} diff --git a/test/executables/null_safety_migrator_companion_test.dart b/test/executables/null_safety_migrator_companion_test.dart deleted file mode 100644 index 55f60fe0..00000000 --- a/test/executables/null_safety_migrator_companion_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -// 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/util/package_util.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:test_descriptor/test_descriptor.dart' as d; - -import 'required_props_collect_and_codemod_test.dart'; - -void main() { - group('null_safety_migrator_companion codemod, end-to-end behavior:', () { - final companionScript = p.join(findPackageRootFor(p.current), - 'bin/null_safety_migrator_companion.dart'); - - const name = 'test_package'; - late d.DirectoryDescriptor projectDir; - - setUp(() async { - projectDir = d.DirectoryDescriptor.fromFilesystem( - name, - p.join(findPackageRootFor(p.current), - 'test/test_fixtures/required_props/test_package')); - await projectDir.create(); - }); - - test('adds hints as expected in different cases', () async { - await testCodemod( - script: companionScript, - args: [ - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_state.dart', contains(''' -@Props(disableRequiredPropValidation: {\'prop1\'}) -mixin FooProps on UiProps { - int prop1; - int prop2; -} - -mixin FooState on UiState { - String/*?*/ state1; - /*late*/ int/*!*/ initializedState; - void Function()/*?*/ state2; -} - -class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState()..initializedState = 1); - - @override - render() { - ButtonElement/*?*/ _ref; - return (Dom.div()..ref = (ButtonElement/*?*/ r) => _ref = r)(); - } -}''')), - ]), - ]), - ]), - ); - }); - }, timeout: Timeout(Duration(minutes: 2))); -} diff --git a/test/executables/required_props_collect_and_codemod_test.dart b/test/executables/required_props_collect_and_codemod_test.dart deleted file mode 100644 index 188d9ad5..00000000 --- a/test/executables/required_props_collect_and_codemod_test.dart +++ /dev/null @@ -1,434 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:meta/meta.dart'; -import 'package:over_react_codemod/src/util/command.dart'; -import 'package:over_react_codemod/src/util/package_util.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:test_descriptor/test_descriptor.dart' as d; - -// Change this to `true` and all of the functional tests in this file will print -// the stdout/stderr of the codemod processes. -final _debug = false; - -void main() { - group( - 'null_safety_required_props collect and codemod command, end-to-end behavior:', - () { - final requiredPropsScript = p.join( - findPackageRootFor(p.current), 'bin/null_safety_required_props.dart'); - - const name = 'test_package'; - late d.DirectoryDescriptor projectDir; - late String dataFilePath; - - setUpAll(() async { - print('setUpAll: Collecting data...'); - final tmpDir = - Directory.systemTemp.createTempSync('required_props_codemod_test'); - dataFilePath = p.join(tmpDir.path, 'prop_requiredness.json'); - await runCommandAndThrowIfFailed('dart', [ - requiredPropsScript, - 'collect', - '--output', - dataFilePath, - p.join(findPackageRootFor(p.current), - 'test/test_fixtures/required_props/test_consuming_package'), - ]); - expect(File(dataFilePath).existsSync(), isTrue); - print('setUpAll: Done.'); - }); - - setUp(() async { - projectDir = d.DirectoryDescriptor.fromFilesystem( - name, - p.join(findPackageRootFor(p.current), - 'test/test_fixtures/required_props/test_package')); - await projectDir.create(); - }); - - const noDataTodoComment = - r"// TODO(orcm.required_props): No data for prop; either it's never set, all places it was set were on dynamic usages, or requiredness data was collected on a version before this prop was added."; - - test('adds hints as expected in different cases', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_private.dart', contains(''' -mixin TestPrivateProps on UiProps { - /*late*/ String set100percent; - String/*?*/ set80percent; - String/*?*/ set20percent; - $noDataTodoComment - String/*?*/ set0percent; -}''')), - d.file('test_class_component_defaults.dart', contains(''' -mixin TestPrivatePropsMixin on UiProps { - String/*?*/ notDefaultedOptional; - /*late*/ String notDefaultedAlwaysSet; - /*late*/ String/*?*/ defaultedNullable; - /*late*/ num/*!*/ defaultedNonNullable; -} - -mixin SomeOtherPropsMixin on UiProps { - /*late*/ num/*!*/ anotherDefaultedNonNullable; -}''')), - d.file('test_class_component_defaults.dart', contains(''' -mixin TestPublic2PropsMixin on UiProps { - String/*?*/ notDefaultedOptional; - /*late*/ String notDefaultedAlwaysSet; - String/*?*/ defaultedNullable; - num/*?*/ defaultedNonNullable; -}''')), - d.file('test_private_dynamic.dart', contains(''' -// TODO(orcm.required_props): This codemod couldn't reliably determine requiredness for these props -// because 75% of usages of components with these props (> max allowed 20% for private props) -// either contained forwarded props or were otherwise too dynamic to analyze. -// It may be possible to upgrade some from optional to required, with some manual inspection and testing. -mixin TestPrivateDynamicProps on UiProps { - String/*?*/ set100percent; -}''')), - d.file('test_private_existing_hints.dart', contains(''' -mixin TestPrivateExistingHintsProps on UiProps { - /*late*/ String set100percentWithoutHint; - /*late*/ String set100percent; - String/*?*/ set80percent; - String/*?*/ set0percent; -}''')), - ]), - ]), - ]), - ); - }); - - group('makes props with required over_react annotations late', () { - test('by default', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - // Note that there's no to-do comment on annotatedRequiredPropSet0Percent - // since we short-circuit the logic that inserts it when trusting the annotation. - d.file('test_required_annotations.dart', contains(''' -mixin TestRequiredAnnotationsProps on UiProps { - /*late*/ String annotatedRequiredProp; - /*late*/ String annotatedNullableRequiredProp; - - /*late*/ String annotatedRequiredPropSet50Percent; - /*late*/ String annotatedRequiredPropSet0Percent; - - /// Doc comment - /*late*/ String annotatedRequiredPropWithDocComment; -}''')), - ]), - ]), - ]), - ); - }); - - test('unless consumers pass --no-trust-required-annotation', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--no-trust-required-annotations', - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_required_annotations.dart', contains(''' -mixin TestRequiredAnnotationsProps on UiProps { - /*late*/ String annotatedRequiredProp; - /*late*/ String annotatedNullableRequiredProp; - - String/*?*/ annotatedRequiredPropSet50Percent; - $noDataTodoComment - String/*?*/ annotatedRequiredPropSet0Percent; - - /// Doc comment - /*late*/ String annotatedRequiredPropWithDocComment; -}''')), - ]), - ]), - ]), - ); - }); - }); - - test('allows customizing requiredness thresholds via command line options', - () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--private-requiredness-threshold=0.1', - '--public-requiredness-threshold=0.7', - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_private.dart', contains(''' -mixin TestPrivateProps on UiProps { - /*late*/ String set100percent; - /*late*/ String set80percent; - /*late*/ String set20percent; - $noDataTodoComment - String/*?*/ set0percent; -}''')), - d.file('test_public_multiple_components.dart', contains(''' -mixin TestPublicUsedByMultipleComponentsProps on UiProps { - /*late*/ String set100percent; - /*late*/ String set80percent; - String/*?*/ set20percent; - $noDataTodoComment - String/*?*/ set0percent; -}''')) - ]), - ]), - ]), - ); - }); - - group('allows customizing skip thresholds via command line options', () { - // Don't test both private and public above/below the threshold, - // so that tests ensure the private/public numbers don't get mixed up somewhere along the way. - test('private props below threshold, public above', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--private-max-allowed-skip-rate=0.12', - '--public-max-allowed-skip-rate=0.9', - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_private_dynamic.dart', contains(''' -// TODO(orcm.required_props): This codemod couldn't reliably determine requiredness for these props -// because 75% of usages of components with these props (> max allowed 12% for private props) -// either contained forwarded props or were otherwise too dynamic to analyze. -// It may be possible to upgrade some from optional to required, with some manual inspection and testing. -mixin TestPrivateDynamicProps on UiProps { - String/*?*/ set100percent; -}''')), - d.file('test_public_dynamic.dart', contains(''' -mixin TestPublicDynamicProps on UiProps { - /*late*/ String set100percent; -}''')) - ]), - ]), - ]), - ); - }); - - test('private props below threshold, public above', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--private-max-allowed-skip-rate=0.9', - '--public-max-allowed-skip-rate=0.34', - '--yes-to-all', - ], - input: projectDir, - expectedOutput: d.dir(projectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_private_dynamic.dart', contains(''' -mixin TestPrivateDynamicProps on UiProps { - /*late*/ String set100percent; -}''')), - d.file('test_public_dynamic.dart', contains(''' -// TODO(orcm.required_props): This codemod couldn't reliably determine requiredness for these props -// because 80% of usages of components with these props (> max allowed 34% for public props) -// either contained forwarded props or were otherwise too dynamic to analyze. -// It may be possible to upgrade some from optional to required, with some manual inspection and testing. -mixin TestPublicDynamicProps on UiProps { - String/*?*/ set100percent; -}''')) - ]), - ]), - ]), - ); - }); - }); - - group( - 'null_safety_required_props makes no update if file is already on a null safe Dart version', - () { - late d.DirectoryDescriptor nullSafeProjectDir; - - setUp(() async { - nullSafeProjectDir = d.DirectoryDescriptor.fromFilesystem( - name, - p.join(findPackageRootFor(p.current), - 'test/test_fixtures/over_react_null_safe_project')); - await nullSafeProjectDir.create(); - }); - - test('', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--yes-to-all', - ], - input: nullSafeProjectDir, - expectedOutput: d.dir(nullSafeProjectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_null_safe.dart', contains(''' -mixin TestPrivateProps on UiProps { - late String set100percent; - String? set80percent; - String? set20percent; - String? set0percent; -}''')), - ]), - ]), - ]), - ); - }); - - test('unless there is a lang version comment', () async { - await testCodemod( - script: requiredPropsScript, - args: [ - 'codemod', - '--prop-requiredness-data', - dataFilePath, - '--yes-to-all', - ], - input: nullSafeProjectDir, - expectedOutput: d.dir(nullSafeProjectDir.name, [ - d.dir('lib', [ - d.dir('src', [ - d.file('test_lang_version_comment.dart', contains(''' -mixin TestPrivateProps on UiProps { - $noDataTodoComment - String/*?*/ set100percent; - $noDataTodoComment - String/*?*/ set80percent; - $noDataTodoComment - String/*?*/ set20percent; - $noDataTodoComment - String/*?*/ set0percent; -}''')), - ]), - ]), - ]), - ); - }); - }); - }, timeout: Timeout(Duration(minutes: 2))); -} - -// Adapted from `testCodemod` in https://github.com/Workiva/dart_codemod/blob/c5d245308554b0e1e7a15a54fbd2c79a9231e2be/test/functional/run_interactive_codemod_test.dart#L39 -// Intentionally does not run `pub get` on the project. -@isTest -Future testCodemod({ - required String script, - required d.DirectoryDescriptor input, - d.DirectoryDescriptor? expectedOutput, - List? args, - void Function(String out, String err)? body, - int? expectedExitCode, - List? stdinLines, -}) async { - final projectDir = input; - - final processArgs = [ - script, - ...?args, - ]; - if (_debug) { - processArgs.add('--verbose'); - } - final process = await Process.start('dart', processArgs, - workingDirectory: projectDir.io.path); - - // If _debug, split these single-subscription streams into two - // so that we can display the output as it comes in. - final stdoutStreams = StreamSplitter.splitFrom( - process.stdout.transform(utf8.decoder), _debug ? 2 : 1); - final stderrStreams = StreamSplitter.splitFrom( - process.stderr.transform(utf8.decoder), _debug ? 2 : 1); - if (_debug) { - stdoutStreams[1] - .transform(LineSplitter()) - .forEach((line) => print('STDOUT: $line')); - stderrStreams[1] - .transform(LineSplitter()) - .forEach((line) => print('STDERR: $line')); - } - - stdinLines?.forEach(process.stdin.writeln); - final codemodExitCode = await process.exitCode; - expectedExitCode ??= 0; - - final codemodStdout = await stdoutStreams[0].join(); - final codemodStderr = await stderrStreams[0].join(); - - expect(codemodExitCode, expectedExitCode, - reason: 'Expected codemod to exit with code $expectedExitCode, but ' - 'it exited with $codemodExitCode.\n' - 'Process stderr:\n$codemodStderr'); - - if (expectedOutput != null) { - // Expect that the modified projet matches the gold files. - await expectedOutput.validate(); - } - - if (body != null) { - body(codemodStdout, codemodStderr); - } -} diff --git a/test/executables/required_props_collect_test.dart b/test/executables/required_props_collect_test.dart deleted file mode 100644 index 3387fb29..00000000 --- a/test/executables/required_props_collect_test.dart +++ /dev/null @@ -1,290 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'package:over_react_codemod/src/dart3_suggestors/required_props/collect/aggregated_data.sg.dart'; -import 'package:over_react_codemod/src/util/command.dart'; -import 'package:over_react_codemod/src/util/package_util.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -main() { - group('null_safety_required_props collect command', () { - late PropRequirednessResults aggregated; - - setUpAll(() async { - // Use this instead for local dev if you want to run collection manually - // as opposed to on every test run. - // final localDevAggregatedOutputFile = 'prop_requiredness.json'; - // aggregated = PropRequirednessResults.fromJson( - // jsonDecode(File(localDevAggregatedOutputFile).readAsStringSync())); - aggregated = await collectAndAggregateDataForTestPackage(); - }); - - group('collects expected data', () { - test('for visibility of props mixins', () { - const expectedVisibilities = { - 'TestPrivateProps': Visibility.private, - 'TestPublicProps': Visibility.public, - 'TestFactoryOnlyExportedProps': Visibility.indirectlyPublic, - }; - final actualVisibiilities = { - for (final name in expectedVisibilities.keys) - name: aggregated.mixinResultsByName(name).visibility - }; - expect(actualVisibiilities, expectedVisibilities); - }); - - group('for private props used within their own package:', () { - test('set rate', () { - final mixinResults = - aggregated.mixinResultsByName('TestPrivateProps'); - expect( - mixinResults.propResultsByName.mapValues((v) => v.samePackageRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 0.8), - containsPair('set20percent', 0.2), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.totalRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 0.8), - containsPair('set20percent', 0.2), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.otherPackageRate), - allOf( - containsPair('set100percent', null), - containsPair('set80percent', null), - containsPair('set20percent', null), - ), - reason: - 'props only used in the same package should not have otherPackageRate populated', - ); - - expect(mixinResults.usageSkipRate, 0); - }); - - test('set rate when used by multiple components', () { - final mixinResults = aggregated - .mixinResultsByName('TestPrivateUsedByMultipleComponentsProps'); - expect( - mixinResults.propResultsByName.mapValues((v) => v.samePackageRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 0.8), - containsPair('set20percent', 0.2), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.totalRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 0.8), - containsPair('set20percent', 0.2), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.otherPackageRate), - allOf( - containsPair('set100percent', null), - containsPair('set80percent', null), - containsPair('set20percent', null), - ), - reason: - 'props only used in the same package should not have otherPackageRate populated', - ); - - expect(mixinResults.usageSkipRate, 0); - }); - - group('skip rate:', () { - test('props that are never skipped', () { - final mixinResults = - aggregated.mixinResultsByName('TestPrivateProps'); - expect(mixinResults.usageSkipRate, 0); - expect(mixinResults.usageSkipCount, 0); - }); - - group('props that are skipped due to', () { - test('dynamic prop additions', () { - expect(aggregated.excludeOtherDynamicUsages, isTrue, - reason: 'test setup check'); - - final mixinResults = - aggregated.mixinResultsByName('TestPrivateDynamicProps'); - const expectedSkipCount = 3; - const expectedTotalUsages = 4; - expect(mixinResults.usageSkipCount, expectedSkipCount); - expect(mixinResults.usageSkipRate, - expectedSkipCount / expectedTotalUsages); - }); - - test('forwarded props', () { - expect(aggregated.excludeUsagesWithForwarded, isTrue, - reason: 'test setup check'); - - final mixinResults = - aggregated.mixinResultsByName('TestPrivateForwardedProps'); - const expectedSkipCount = 5; - const expectedTotalUsages = 6; - expect(mixinResults.usageSkipCount, expectedSkipCount); - expect(mixinResults.usageSkipRate, - expectedSkipCount / expectedTotalUsages); - }); - }); - }); - }); - - group('for public props used in multiple packages:', () { - test('set rate', () { - final mixinResults = aggregated.mixinResultsByName('TestPublicProps'); - expect( - mixinResults.propResultsByName.mapValues((v) => v.totalRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set20percent', 0.2), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.samePackageRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set20percent', anyOf(null, 0.0)), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.otherPackageRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set20percent', 1.0), - ), - ); - - expect(mixinResults.usageSkipRate, 0); - }); - - test('set rate when used by multiple components', () { - final mixinResults = aggregated - .mixinResultsByName('TestPublicUsedByMultipleComponentsProps'); - - expect( - mixinResults.propResultsByName.mapValues((v) => v.totalRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 0.8), - containsPair('set20percent', 0.2), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.samePackageRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 1.0), - containsPair('set20percent', 0.5), - ), - ); - expect( - mixinResults.propResultsByName.mapValues((v) => v.otherPackageRate), - allOf( - containsPair('set100percent', 1.0), - containsPair('set80percent', 2 / 3), - containsPair('set20percent', anyOf(null, 0.0)), - ), - ); - expect(mixinResults.usageSkipRate, 0); - }); - - group('skip rate:', () { - test('props that are never skipped', () { - final mixinResults = - aggregated.mixinResultsByName('TestPublicProps'); - expect(mixinResults.usageSkipRate, 0); - expect(mixinResults.usageSkipCount, 0); - }); - }); - }); - - test('does not aggregate data for non-factory usages', () { - final mixinResults = - aggregated.mixinResultsByName('TestPrivateNonFactoryUsagesProps'); - final propTotalRates = - mixinResults.propResultsByName.mapValues((v) => v.totalRate); - expect( - mixinResults.propResultsByName['set100percent'], - isA() - .having((r) => r.totalRate, 'totalRate', 1) - .having((r) => r.totalUsageCount, 'totalUsageCount', 1), - reason: - 'test setup check: should contain data for the single factory-based usage', - ); - expect(propTotalRates.keys.toList(), unorderedEquals(['set100percent']), - reason: - 'should not contain data for non-factory usages and props set on them, such as `onlySetOnNonFactoryUsages`'); - }); - }); - // Use a longer timeout since setupAll can be slow. - }, timeout: Timeout(Duration(seconds: 60))); -} - -Future collectAndAggregateDataForTestPackage() async { - print('Collecting data (this may take a while)...'); - final tmpFolder = - Directory.systemTemp.createTempSync('prop-requiredness-test'); - addTearDown(() => tmpFolder.delete(recursive: true)); - - final orcmRoot = findPackageRootFor(p.current); - final testPackagePath = p.join( - orcmRoot, 'test/test_fixtures/required_props/test_consuming_package'); - - final aggregateOutputFile = File(p.join(tmpFolder.path, 'aggregated.json')); - - await runCommandAndThrowIfFailedInheritIo('dart', [ - 'run', - p.join(orcmRoot, 'bin/null_safety_required_props.dart'), - 'collect', - ...['--output', aggregateOutputFile.path], - testPackagePath, - ]); - - return PropRequirednessResults.fromJson( - jsonDecode(aggregateOutputFile.readAsStringSync())); -} - -extension on PropRequirednessResults { - static const testPackageName = 'test_package'; - - String mixinIdForName(String mixinName) { - return mixinMetadata.mixinNamesById.entries - .singleWhere((entry) => entry.value == mixinName) - .key; - } - - MixinResult mixinResultsByName(String mixinName) { - final mixinId = mixinIdForName(mixinName); - return this.mixinResultsByIdByPackage[testPackageName]![mixinId]!; - } -} - -extension on Map { - /// Returns a new map with values transformed by [convertValue]. - Map mapValues(T convertValue(V value)) => - map((key, value) => MapEntry(key, convertValue(value))); -} diff --git a/test/resolved_file_context.dart b/test/resolved_file_context.dart index 2c2f97d7..daa47e64 100644 --- a/test/resolved_file_context.dart +++ b/test/resolved_file_context.dart @@ -48,8 +48,27 @@ class SharedAnalysisContext { /// that depends on the `over_react` package. /// /// Use this when possible over [wsd], since it resolves much faster. - static final overReact = SharedAnalysisContext(p.join( - findPackageRootFor(p.current), 'test/test_fixtures/over_react_project')); + /// + /// Since this fixture is now Dart 3, tests that write Dart 2-style source + /// (uninitialized fields, nullable function calls, etc.) into it will produce + /// null-safety analysis errors. Those are suppressed via [defaultIsExpectedError] + /// so suggestor tests can still exercise pre-null-safe code patterns. + static final overReact = SharedAnalysisContext( + p.join(findPackageRootFor(p.current), 'test/test_fixtures/over_react_project'), + defaultIsExpectedError: _isLegacyNullSafetyError); + + static bool _isLegacyNullSafetyError(AnalysisError error) { + const legacyCodes = { + 'not_initialized_non_nullable_instance_field', + 'not_initialized_non_nullable_variable', + 'unchecked_use_of_nullable_value', + 'argument_type_not_assignable', + 'invalid_override', + 'main_first_positional_parameter_type', + 'body_might_complete_normally', + }; + return legacyCodes.contains(error.errorCode.name.toLowerCase()); + } /// A context root located at `test/test_fixtures/over_react_null_safe_project` /// that depends on the `over_react` package and a null-safe Dart version. @@ -61,6 +80,7 @@ class SharedAnalysisContext { /// that depends on the internal `web_skin_dart` package (as well as `over_react`). static final wsd = SharedAnalysisContext( p.join(findPackageRootFor(p.current), 'test/test_fixtures/wsd_project'), + defaultIsExpectedError: _isLegacyNullSafetyError, customPubGetErrorMessage: 'If this fails to resolve in GitHub Actions, make sure your test or' ' test group is tagged with "wsd" so that it\'s only run in Skynet.'); @@ -68,7 +88,8 @@ class SharedAnalysisContext { /// A context root located at `test/test_fixtures/rmui_project` /// that depends on the `react_material_ui` package (as well as `over_react`). static final rmui = SharedAnalysisContext( - p.join(findPackageRootFor(p.current), 'test/test_fixtures/rmui_project')); + p.join(findPackageRootFor(p.current), 'test/test_fixtures/rmui_project'), + defaultIsExpectedError: _isLegacyNullSafetyError); /// The path to the package root in which test files will be created /// and resolved. @@ -81,6 +102,10 @@ class SharedAnalysisContext { /// A custom error message to display if `pub get` fails. final String? customPubGetErrorMessage; + /// An optional default error filter applied to every [resolvedFileContextForTest] + /// call. Merged with any caller-provided [isExpectedError]. + final IsExpectedError? defaultIsExpectedError; + // Namespace the test path using a UUID so that concurrent runs // don't try to output the same filename, making it so that we can // easily create new filenames by counting synchronously [nextFilename] @@ -91,7 +116,8 @@ class SharedAnalysisContext { // analysis results (meaning faster test runs). final _testFileSubpath = 'lib/dynamic_test_files/${Uuid().v4()}'; - SharedAnalysisContext(this._path, {this.customPubGetErrorMessage}) { + SharedAnalysisContext(this._path, + {this.customPubGetErrorMessage, this.defaultIsExpectedError}) { if (!p.isAbsolute(_path)) { throw ArgumentError.value(_path, 'projectRoot', 'must be absolute'); } @@ -235,7 +261,14 @@ class SharedAnalysisContext { final result = await _printAboutFirstFile( () => context.currentSession.getResolvedLibrary(path)); if (throwOnAnalysisErrors) { - checkResolvedResultForErrors(result, isExpectedError: isExpectedError); + final mergedIsExpectedError = + (defaultIsExpectedError == null && isExpectedError == null) + ? null + : (AnalysisError error) => + (defaultIsExpectedError?.call(error) ?? false) || + (isExpectedError?.call(error) ?? false); + checkResolvedResultForErrors(result, + isExpectedError: mergedIsExpectedError); } } diff --git a/test/test_fixtures/over_react_null_safe_project/pubspec.yaml b/test/test_fixtures/over_react_null_safe_project/pubspec.yaml index a1267be2..515da858 100644 --- a/test/test_fixtures/over_react_null_safe_project/pubspec.yaml +++ b/test/test_fixtures/over_react_null_safe_project/pubspec.yaml @@ -1,5 +1,5 @@ name: over_react_null_safe_project environment: - sdk: '>=2.19.0 <3.0.0' + sdk: '>=2.19.0 <4.0.0' dependencies: over_react: ^5.0.0 diff --git a/test/test_fixtures/over_react_project/pubspec.yaml b/test/test_fixtures/over_react_project/pubspec.yaml index 36042ef2..345b302a 100644 --- a/test/test_fixtures/over_react_project/pubspec.yaml +++ b/test/test_fixtures/over_react_project/pubspec.yaml @@ -1,5 +1,5 @@ name: over_react_project environment: - sdk: '>=2.11.0 <3.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: over_react: ^5.6.0 diff --git a/test/test_fixtures/required_props/test_consuming_package/lib/src/test_consume_public.dart b/test/test_fixtures/required_props/test_consuming_package/lib/src/test_consume_public.dart deleted file mode 100644 index 2a910b56..00000000 --- a/test/test_fixtures/required_props/test_consuming_package/lib/src/test_consume_public.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:test_package/entrypoint.dart'; - -usages() { - // 4 usages in source package, 1 in this package - (TestPublic() - ..set100percent = '' - ..set20percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_consuming_package/lib/src/test_consume_public_multiple_components.dart b/test/test_fixtures/required_props/test_consuming_package/lib/src/test_consume_public_multiple_components.dart deleted file mode 100644 index 86696072..00000000 --- a/test/test_fixtures/required_props/test_consuming_package/lib/src/test_consume_public_multiple_components.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:over_react/over_react.dart'; -import 'package:test_package/entrypoint.dart'; - -class TestMultiComponentsOtherPackageProps = UiProps - with TestPublicUsedByMultipleComponentsProps; - -UiFactory - TestMultiComponentsOtherPackage = uiFunction( - (props) {}, - _$TestMultiComponentsOtherPackageConfig, // ignore: undefined_identifier -); - -usages() { - // 2 usages of mixin in source package, 3 in this package - (TestMultiComponentsOtherPackage() - ..set100percent = '' - ..set80percent = '')(); - (TestMultiComponentsOtherPackage() - ..set100percent = '' - ..set80percent = '')(); - (TestMultiComponentsOtherPackage()..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_consuming_package/pubspec.yaml b/test/test_fixtures/required_props/test_consuming_package/pubspec.yaml deleted file mode 100644 index 4ab8af6d..00000000 --- a/test/test_fixtures/required_props/test_consuming_package/pubspec.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: test_consuming_package -environment: - sdk: '>=2.11.0 <3.0.0' -dependencies: - over_react: ^5.0.0 - test_package: - path: ../test_package diff --git a/test/test_fixtures/required_props/test_package/lib/entrypoint.dart b/test/test_fixtures/required_props/test_package/lib/entrypoint.dart deleted file mode 100644 index 2a2c1067..00000000 --- a/test/test_fixtures/required_props/test_package/lib/entrypoint.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'src/test_class_component_defaults.dart' show TestPublic2, TestPublic2PropsMixin; -export 'src/test_public.dart'; -export 'src/test_public_dynamic.dart'; -export 'src/test_public_multiple_components.dart'; -export 'src/test_factory_only_exported.dart' show TestFactoryOnlyExported; diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_class_component_defaults.dart b/test/test_fixtures/required_props/test_package/lib/src/test_class_component_defaults.dart deleted file mode 100644 index 0cef4e6f..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_class_component_defaults.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_class_component_defaults.over_react.g.dart'; - -mixin TestPrivatePropsMixin on UiProps { - String notDefaultedOptional; - String notDefaultedAlwaysSet; - String defaultedNullable; - num defaultedNonNullable; -} - -mixin SomeOtherPropsMixin on UiProps { - num anotherDefaultedNonNullable; -} - -class TestPrivateProps = UiProps - with TestPrivatePropsMixin, SomeOtherPropsMixin; - -UiFactory TestPrivate = - castUiFactory(_$TestPrivate); // ignore: undefined_identifier - -class TestPrivateComponent extends UiComponent2 { - @override - get defaultProps => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() {} -} - -mixin TestPublic2PropsMixin on UiProps { - String notDefaultedOptional; - String notDefaultedAlwaysSet; - String defaultedNullable; - num defaultedNonNullable; -} - -class TestPublic2Props = UiProps - with TestPublic2PropsMixin, SomeOtherPropsMixin; - -UiFactory TestPublic2 = - castUiFactory(_$TestPublic2); // ignore: undefined_identifier - -class TestPublic2Component extends UiComponent2 { - @override - get defaultProps => (newProps() - ..defaultedNullable = null - ..defaultedNonNullable = 2.1 - ..anotherDefaultedNonNullable = 1.1 - ); - - @override - render() {} -} - -usages() { - (TestPrivate()..notDefaultedAlwaysSet = 'abc')(); - (TestPrivate() - ..notDefaultedOptional = 'abc' - ..notDefaultedAlwaysSet = 'abc' - ..defaultedNullable = 'abc' - ..defaultedNonNullable = 1 - ..anotherDefaultedNonNullable = 2 - )(); - (TestPublic2()..notDefaultedAlwaysSet = 'abc')(); - (TestPublic2() - ..notDefaultedAlwaysSet = 'abc' - ..notDefaultedOptional = 'abc' - ..defaultedNullable = 'abc' - ..defaultedNonNullable = 1 - ..anotherDefaultedNonNullable = 2 - )(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_factory_only_exported.dart b/test/test_fixtures/required_props/test_package/lib/src/test_factory_only_exported.dart deleted file mode 100644 index c9a7e6f5..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_factory_only_exported.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:over_react/over_react.dart'; - -part 'test_factory_only_exported.over_react.g.dart'; - -@internal -mixin TestFactoryOnlyExportedProps on UiProps { - String set100percent; -} - -UiFactory TestFactoryOnlyExported = uiFunction( - (props) {}, - _$TestFactoryOnlyExportedConfig, // ignore: undefined_identifier -); - -usages() { - (TestFactoryOnlyExported()..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_private.dart b/test/test_fixtures/required_props/test_package/lib/src/test_private.dart deleted file mode 100644 index 5a0dbd3e..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_private.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_private.over_react.g.dart'; - -mixin TestPrivateProps on UiProps { - String set100percent; - String set80percent; - String set20percent; - String set0percent; -} - -UiFactory TestPrivate = uiFunction( - (props) {}, - _$TestPrivateConfig, // ignore: undefined_identifier -); - -usages() { - (TestPrivate() - ..set100percent = '' - ..set80percent = '' - ..set20percent = '')(); - (TestPrivate() - ..set100percent = '' - ..set80percent = '')(); - (TestPrivate() - ..set100percent = '' - ..set80percent = '')(); - (TestPrivate() - ..set100percent = '' - ..set80percent = '')(); - (TestPrivate()..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_private_dynamic.dart b/test/test_fixtures/required_props/test_package/lib/src/test_private_dynamic.dart deleted file mode 100644 index 15bc7489..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_private_dynamic.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_private_dynamic.over_react.g.dart'; - -mixin TestPrivateDynamicProps on UiProps { - String set100percent; -} - -UiFactory TestPrivateDynamic = uiFunction( - (props) {}, - _$TestPrivateDynamicConfig, // ignore: undefined_identifier -); - -void dynamicUsages(Map props, void Function(Map) propsModifier) { - // Test all dynamic usage cases. - // 75% of usages are dynamic. - - // One non-dynamic usage to help assert we're collecting data properly. - (TestPrivateDynamic()..set100percent = '')(); - - (TestPrivateDynamic() - ..addProps(props) - ..set100percent = '')(); - (TestPrivateDynamic() - ..addAll(props) - ..set100percent = '')(); - (TestPrivateDynamic() - ..modifyProps(propsModifier) - ..set100percent = '')(); -} - -mixin TestPrivateForwardedProps on UiProps { - String set100percent; -} - -UiFactory TestPrivateForwarded = uiFunction( - (props) {}, - _$TestPrivateForwardedConfig, // ignore: undefined_identifier -); - -abstract class ForwardedUsagesComponent extends UiComponent2 { - void forwardedUsages() { - // Test all forwarded usage cases. - - // One non-dynamic usage to help assert we're collecting data properly. - (TestPrivateForwarded()..set100percent = '')(); - - (TestPrivateForwarded() - ..set100percent = '' - ..addProps(copyUnconsumedProps()))(); - (TestPrivateForwarded() - ..modifyProps(addUnconsumedProps) - ..set100percent = '')(); - - (TestPrivateForwarded() - ..set100percent = '' - ..addProps(props.getPropsToForward()))(); - (TestPrivateForwarded() - ..set100percent = '' - ..modifyProps(props.addPropsToForward()))(); - - (TestPrivateForwarded() - ..addUnconsumedProps(props, []) - ..set100percent = '')(); - } -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_private_existing_hints.dart b/test/test_fixtures/required_props/test_package/lib/src/test_private_existing_hints.dart deleted file mode 100644 index cb0ed95b..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_private_existing_hints.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_private.over_react.g.dart'; - -mixin TestPrivateExistingHintsProps on UiProps { - String set100percentWithoutHint; - /*late*/ String set100percent; - String/*?*/ set80percent; - String/*?*/ set0percent; -} - -UiFactory TestPrivateExistingHints = uiFunction( - (props) {}, - _$TestPrivateExistingHintsConfig, // ignore: undefined_identifier -); - -usages() { - (TestPrivateExistingHints() - ..set100percentWithoutHint = '' - ..set100percent = '' - ..set80percent = '')(); - (TestPrivateExistingHints() - ..set100percentWithoutHint = '' - ..set100percent = '' - ..set80percent = '')(); - (TestPrivateExistingHints() - ..set100percentWithoutHint = '' - ..set100percent = '' - ..set80percent = '')(); - (TestPrivateExistingHints() - ..set100percentWithoutHint = '' - ..set100percent = '' - ..set80percent = '')(); - (TestPrivateExistingHints() - ..set100percentWithoutHint = '' - ..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_private_multiple_components.dart b/test/test_fixtures/required_props/test_package/lib/src/test_private_multiple_components.dart deleted file mode 100644 index a921d111..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_private_multiple_components.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:over_react/over_react.dart'; - -mixin TestPrivateUsedByMultipleComponentsProps on UiProps { - String set100percent; - String set80percent; - String set20percent; - String set0percent; -} - -class TestMultiComponents1Props = UiProps - with TestPrivateUsedByMultipleComponentsProps; - -UiFactory TestMultiComponents1 = uiFunction( - (props) {}, - _$TestMultiComponents1Config, // ignore: undefined_identifier -); - -class TestMultiComponents2Props = UiProps - with TestPrivateUsedByMultipleComponentsProps; - -UiFactory TestMultiComponents2 = uiFunction( - (props) {}, - _$TestMultiComponents2Config, // ignore: undefined_identifier -); - -usages() { - (TestMultiComponents1() - ..set100percent = '' - ..set80percent = '' - ..set20percent = '')(); - (TestMultiComponents1() - ..set100percent = '' - ..set80percent = '')(); - (TestMultiComponents1() - ..set100percent = '' - ..set80percent = '')(); - (TestMultiComponents1() - ..set100percent = '' - ..set80percent = '')(); - - (TestMultiComponents2()..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_private_non_factory_usages.dart b/test/test_fixtures/required_props/test_package/lib/src/test_private_non_factory_usages.dart deleted file mode 100644 index 1f9e85af..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_private_non_factory_usages.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_private_non_factory_usages.over_react.g.dart'; - -mixin TestPrivateNonFactoryUsagesProps on UiProps { - String set100percent; - String onlySetOnNonFactoryUsages; -} - -UiFactory TestPrivateNonFactoryUsages = - uiFunction( - (props) {}, - _$TestPrivateNonFactoryUsagesConfig, // ignore: undefined_identifier -); - -class SomeObject { - UiFactory factoryProperty; -} - -usages(SomeObject object) { - // A single usage to make sure we're collecting data for these props. - (TestPrivateNonFactoryUsages()..set100percent = '')(); - { - final factoryLocalVariable = TestPrivateNonFactoryUsages; - (factoryLocalVariable() - ..set100percent = '' - ..onlySetOnNonFactoryUsages = '')(); - } - { - final builderLocalVariable = TestPrivateNonFactoryUsages(); - (builderLocalVariable - ..set100percent = '' - ..onlySetOnNonFactoryUsages = '')(); - } - { - (object.factoryProperty() - ..set100percent = '' - ..onlySetOnNonFactoryUsages = '')(); - } -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_public.dart b/test/test_fixtures/required_props/test_package/lib/src/test_public.dart deleted file mode 100644 index 140a6d68..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_public.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_public.g.dart'; - -mixin TestPublicProps on UiProps { - String set100percent; - String set20percent; -} - -UiFactory TestPublic = uiFunction( - (props) {}, - _$TestPublicConfig, // ignore: undefined_identifier -); - -usages() { - // 4 usages in this package, 1 in consuming package - (TestPublic()..set100percent = '')(); - (TestPublic()..set100percent = '')(); - (TestPublic()..set100percent = '')(); - (TestPublic()..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_public_dynamic.dart b/test/test_fixtures/required_props/test_package/lib/src/test_public_dynamic.dart deleted file mode 100644 index 90ff87d9..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_public_dynamic.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_public_dynamic.over_react.g.dart'; - -mixin TestPublicDynamicProps on UiProps { - String set100percent; -} - -UiFactory TestPublicDynamic = uiFunction( - (props) {}, - _$TestPublicDynamicConfig, // ignore: undefined_identifier -); - -void dynamicUsages(Map props) { - // 80% of usages are dynamic. - - // One non-dynamic usage to help assert we're collecting data properly. - (TestPublicDynamic()..set100percent = '')(); - (TestPublicDynamic() - ..addProps(props) - ..set100percent = '')(); - (TestPublicDynamic() - ..addProps(props) - ..set100percent = '')(); - (TestPublicDynamic() - ..addProps(props) - ..set100percent = '')(); - (TestPublicDynamic() - ..addProps(props) - ..set100percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_public_multiple_components.dart b/test/test_fixtures/required_props/test_package/lib/src/test_public_multiple_components.dart deleted file mode 100644 index 3faad993..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_public_multiple_components.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:over_react/over_react.dart'; - -mixin TestPublicUsedByMultipleComponentsProps on UiProps { - String set100percent; - String set80percent; - String set20percent; - String set0percent; -} - -class _TestMultiComponentsSamePackageProps = UiProps - with TestPublicUsedByMultipleComponentsProps; - -UiFactory<_TestMultiComponentsSamePackageProps> - _TestMultiComponentsSamePackage = uiFunction( - (props) {}, - _$_TestMultiComponentsSamePackageConfig, // ignore: undefined_identifier -); - -usages() { - // 2 usages of mixin in this package, 3 in consuming package - (_TestMultiComponentsSamePackage() - ..set100percent = '' - ..set80percent = '' - ..set20percent = '')(); - (_TestMultiComponentsSamePackage() - ..set100percent = '' - ..set80percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_required_annotations.dart b/test/test_fixtures/required_props/test_package/lib/src/test_required_annotations.dart deleted file mode 100644 index 2732408d..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_required_annotations.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:over_react/over_react.dart'; - -part 'test_required_annotations.over_react.g.dart'; - -mixin TestRequiredAnnotationsProps on UiProps { - @requiredProp - String annotatedRequiredProp; - @nullableRequiredProp - String annotatedNullableRequiredProp; - - @requiredProp - String annotatedRequiredPropSet50Percent; - @requiredProp - String annotatedRequiredPropSet0Percent; - - /// Doc comment - @requiredProp - String annotatedRequiredPropWithDocComment; -} - -UiFactory TestRequiredAnnotations = uiFunction( - (props) {}, - _$TestRequiredAnnotationsConfig, // ignore: undefined_identifier -); - -usages() { - (TestRequiredAnnotations() - ..annotatedRequiredProp = '' - ..annotatedNullableRequiredProp = '' - ..annotatedRequiredPropWithDocComment = '')(); - (TestRequiredAnnotations() - ..annotatedRequiredProp = '' - ..annotatedNullableRequiredProp = '' - ..annotatedRequiredPropWithDocComment = '' - ..annotatedRequiredPropSet50Percent = '')(); -} diff --git a/test/test_fixtures/required_props/test_package/lib/src/test_state.dart b/test/test_fixtures/required_props/test_package/lib/src/test_state.dart deleted file mode 100644 index e2262318..00000000 --- a/test/test_fixtures/required_props/test_package/lib/src/test_state.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:html'; - -import 'package:over_react/over_react.dart'; -import 'package:over_react/over_react_redux.dart'; - -// ignore: uri_has_not_been_generated -part 'test_state.over_react.g.dart'; - -UiFactory Foo = connect( - mapStateToPropsWithOwnProps: (state, props) => Foo()..prop1 = 1, -)(castUiFactory(_$Foo)); // ignore: undefined_identifier - -mixin FooProps on UiProps { - int prop1; - int prop2; -} - -mixin FooState on UiState { - String state1; - int initializedState; - void Function() state2; -} - -class FooComponent extends UiStatefulComponent2 { - @override - get initialState => (newState()..initializedState = 1); - - @override - render() { - ButtonElement _ref; - return (Dom.div()..ref = (ButtonElement r) => _ref = r)(); - } -} diff --git a/test/test_fixtures/required_props/test_package/pubspec.yaml b/test/test_fixtures/required_props/test_package/pubspec.yaml deleted file mode 100644 index fda07fef..00000000 --- a/test/test_fixtures/required_props/test_package/pubspec.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: test_package -environment: - sdk: '>=2.11.0 <3.0.0' -dependencies: - meta: ^1.16.0 - over_react: ^5.0.0 diff --git a/test/test_fixtures/rmui_project/pubspec.yaml b/test/test_fixtures/rmui_project/pubspec.yaml index ab7118a2..d7036ac5 100644 --- a/test/test_fixtures/rmui_project/pubspec.yaml +++ b/test/test_fixtures/rmui_project/pubspec.yaml @@ -1,6 +1,6 @@ name: rmui_project environment: - sdk: '>=2.11.0 <3.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: over_react: ^5.0.0 react_material_ui: diff --git a/test/test_fixtures/wsd_project/pubspec.yaml b/test/test_fixtures/wsd_project/pubspec.yaml index 3befe900..a8c8e48d 100644 --- a/test/test_fixtures/wsd_project/pubspec.yaml +++ b/test/test_fixtures/wsd_project/pubspec.yaml @@ -1,6 +1,6 @@ name: wsd_project environment: - sdk: '>=2.11.0 <3.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: over_react: ^5.0.0 web_skin_dart: