Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1e3f876
Stub in RequiredFluxProps suggestor / test files
aaronlademann-wf Dec 1, 2023
10de57d
Begin work on visitor for required flux props
aaronlademann-wf Dec 1, 2023
c60beec
Progress
aaronlademann-wf Dec 5, 2023
4bbc2ae
All but one test passing!
aaronlademann-wf Dec 8, 2023
61ef7a9
All tests passing
aaronlademann-wf Dec 8, 2023
c38224a
Oops
aaronlademann-wf Dec 13, 2023
e5e98b8
Add case for class / fn component props
aaronlademann-wf Dec 13, 2023
af15a8f
Cleanup
aaronlademann-wf Dec 13, 2023
5234055
Create shared tests for both invoked / uninvoked
aaronlademann-wf Dec 13, 2023
d69dbbe
Bin wiring
aaronlademann-wf Dec 13, 2023
6ab4088
Format
aaronlademann-wf Dec 13, 2023
647457b
Fix lints
aaronlademann-wf Dec 13, 2023
927991d
Make large method a top-level fn
aaronlademann-wf Dec 13, 2023
44b1e38
Store in separate vars to improve readability
aaronlademann-wf Dec 13, 2023
b7703be
Improve test file readability
aaronlademann-wf Dec 13, 2023
e60a936
Format
aaronlademann-wf Dec 13, 2023
b2597ca
Improve types
aaronlademann-wf Dec 19, 2023
a57eec6
Match on actual type instead of type name
aaronlademann-wf Dec 19, 2023
580b82c
Match on actual type instead of type name
aaronlademann-wf Dec 19, 2023
619910d
Remove unnecessary base class
aaronlademann-wf Dec 19, 2023
aea20f0
Improve approximation of in-scope vars, add tests
aaronlademann-wf Dec 19, 2023
7ab4757
Remove FIXME that has already been addressed
aaronlademann-wf Dec 19, 2023
6a3aafc
Improve isExpectedError to avoid bad setups
aaronlademann-wf Dec 19, 2023
b08151b
Don’t patch defaultProps
aaronlademann-wf Dec 19, 2023
5f81eb9
Fix edge case with single cascade
aaronlademann-wf Dec 19, 2023
942b8e2
Don’t consider dynamic vars as possible values
aaronlademann-wf Dec 19, 2023
d344edc
Fix test description
aaronlademann-wf Dec 19, 2023
2745fc9
Don’t attempt to assign a var when store/actions are dynamic
aaronlademann-wf Dec 19, 2023
9ec4ec8
Add executable
aaronlademann-wf Dec 19, 2023
1bf39c4
Add pub get
aaronlademann-wf Dec 19, 2023
d36a656
Address CR feedback
aaronlademann-wf Dec 19, 2023
1d1a288
Remove unused import
aaronlademann-wf Jan 4, 2024
e8300cb
Avoid unintentional behavior change
aaronlademann-wf Jan 4, 2024
a603afe
Don’t assign vars with null values
aaronlademann-wf Jan 4, 2024
0f3a754
Ignore PanelTitle/PanelTitleV2
aaronlademann-wf Jan 4, 2024
7798be7
Skip PanelToolbar as well
aaronlademann-wf Jan 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions bin/required_flux_props.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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';
230 changes: 230 additions & 0 deletions lib/src/dart3_suggestors/null_safety_prep/required_flux_props.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// 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:over_react_codemod/src/util/class_suggestor.dart';
import 'package:over_react_codemod/src/util/offset_util.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';

@override
visitCascadeExpression(CascadeExpression node) {
var writesToFluxUiProps = false;
var actionsAssigned = false;
var storeAssigned = false;

DartType? fluxActionsType;
DartType? fluxStoreType;
final cascadeWriteEl = node.staticType?.element;
if (cascadeWriteEl is ClassElement) {
final maybeFluxUiPropsMixin = cascadeWriteEl.mixins
.singleWhereOrNull((e) => e.element.name == fluxPropsMixinName);
writesToFluxUiProps = maybeFluxUiPropsMixin != null;
fluxActionsType = maybeFluxUiPropsMixin?.typeArguments[0];
fluxStoreType = maybeFluxUiPropsMixin?.typeArguments[1];
}

final cascadingAssignments =
node.cascadeSections.whereType<AssignmentExpression>();
storeAssigned = cascadingAssignments.any((cascade) {
final lhs = cascade.leftHandSide;
return lhs is PropertyAccess && lhs.propertyName.name == 'store';
});
actionsAssigned = cascadingAssignments.any((cascade) {
final lhs = cascade.leftHandSide;
return lhs is PropertyAccess && lhs.propertyName.name == 'actions';
});

if (writesToFluxUiProps && !storeAssigned) {
storeAssigned = true;
final storeValue =
_getNameOfVarOrFieldInScopeWithType(node, fluxStoreType) ?? 'null';
yieldNewCascadeSection(node, '..store = $storeValue');
}

if (writesToFluxUiProps && !actionsAssigned) {
actionsAssigned = true;
final actionsValue =
_getNameOfVarOrFieldInScopeWithType(node, fluxActionsType) ?? 'null';
yieldNewCascadeSection(node, '..actions = $actionsValue');
}
}

void yieldNewCascadeSection(CascadeExpression node, String newSection) {
final offset = context.sourceFile.getOffsetOfLineAfter(node.target.offset);
yieldPatch(newSection, offset, offset);
}

@override
Future<void> generatePatches() async {
_result = await context.getResolvedUnit();
if (_result == null) {
throw Exception(
'Could not get resolved result for "${context.relativePath}"');
}
_result!.unit.accept(this);
}
}

String? _getNameOfVarOrFieldInScopeWithType(AstNode node, DartType? type) {
final inScopeVariableDetector = _InScopeVarDetector();
// Find top level vars
node
.thisOrAncestorOfType<CompilationUnit>()
?.accept(inScopeVariableDetector);
// Find vars declared in top-level fns (like `main()`)
node
.thisOrAncestorOfType<BlockFunctionBody>()
?.visitChildren(inScopeVariableDetector);

final inScopeVarName = inScopeVariableDetector.found
.firstWhereOrNull((v) {
final maybeMatchingType = v.declaredElement?.type;
return maybeMatchingType?.element?.name == type?.element?.name;
})
?.declaredElement
?.name;

final componentScopePropDetector = _ComponentScopeFluxPropsDetector();
// Find actions/store in props of class components
node
.thisOrAncestorOfType<ClassDeclaration>()
?.accept(componentScopePropDetector);
// Find actions/store in props of fn components
node
.thisOrAncestorOfType<MethodInvocation>()
?.accept(componentScopePropDetector);

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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should account for both uiFunction and uiForwardRef


/// A visitor to detect in-scope store/actions variables (top-level and block function scopes)
class _InScopeVarDetector extends RecursiveAstVisitor<void> {
final List<VariableDeclaration> found;

_InScopeVarDetector() : found = [];

@override
visitVariableDeclaration(VariableDeclaration node) {
// Don't visit function component declarations here since we visit them using the _ComponentScopeFluxPropsDetector
if (_isFnComponentDeclaration(node.initializer)) return;

if (node.declaredElement != null) {
found.add(node);
}
}
}

/// A visitor to detect store/actions values in a props class (supports both class and fn components)
class _ComponentScopeFluxPropsDetector extends RecursiveAstVisitor<void> {
final Map<PropertyAccessorElement, DartType> _foundWithMappedTypes;
List<PropertyAccessorElement> 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
@override
void visitMethodInvocation(MethodInvocation node) {
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
@override
void visitClassDeclaration(ClassDeclaration node) {
final elWithProps =
node.declaredElement?.supertype?.typeArguments.singleOrNull?.element;
_lookForFluxStoreAndActionsInPropsClass(elWithProps);
}
}
44 changes: 44 additions & 0 deletions lib/src/executables/required_flux_props.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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: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';

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
""";

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

final parsedArgs = parser.parse(args);

exitCode = await runInteractiveCodemod(
allDartPathsExceptHidden(),
aggregate([
RequiredFluxProps(),
].map((s) => ignoreable(s))),
defaultYes: true,
args: parsedArgs.rest,
additionalHelpOutput: parser.usage,
changesRequiredOutput: _changesRequiredOutput,
);
}
4 changes: 2 additions & 2 deletions lib/src/intl_suggestors/intl_migrator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class ConstantStringMigrator extends GeneralizingAstVisitor
if (node.isConst &&
node.initializer != null &&
node.initializer is SimpleStringLiteral) {
SimpleStringLiteral literal = node.initializer as SimpleStringLiteral;
SimpleStringLiteral literal = node.initializer! as SimpleStringLiteral;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were lints causing CI failures in a few misc. files that are unrelated to the scope of this PR.

var string = literal.stringValue;
// I don't see how the parent could possibly be null, but if it's true, bail out.
if (node.parent == null || string == null || string.length <= 1) return;
Expand Down Expand Up @@ -147,7 +147,7 @@ class ConstantStringMigrator extends GeneralizingAstVisitor
} else {
// Use a content-based name.
var contentBasedName =
toVariableName(stringContent(node.initializer as StringLiteral)!);
toVariableName(stringContent(node.initializer! as StringLiteral)!);
return contentBasedName;
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/intl_suggestors/message_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class MessageParser {
String withCorrectedNameParameter(MethodDeclaration declaration) {
var invocation = intlMethodInvocation(declaration);
var nameParameter = nameParameterFrom(invocation);
var className = (declaration.parent as ClassDeclaration).name.lexeme;
var className = (declaration.parent! as ClassDeclaration).name.lexeme;
var expected = "'${className}_${declaration.name.lexeme}'";
var actual = nameParameter?.expression.toSource();
var basicString = '$declaration';
Expand Down
2 changes: 1 addition & 1 deletion lib/src/react16_suggestors/react_style_maps_updater.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class ReactStyleMapsUpdater extends GeneralizingAstVisitor
// Handle `toRem(1).toString()`
if (invocation.methodName.name == 'toString' &&
invocation.target is MethodInvocation) {
invocation = invocation.target as MethodInvocation;
invocation = invocation.target! as MethodInvocation;
}

if (!const ['toPx', 'toRem'].contains(invocation.methodName.name)) {
Expand Down
8 changes: 1 addition & 7 deletions lib/src/util/component_usage_migrator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import 'package:meta/meta.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/util/ignore_info.dart';
import 'package:source_span/source_span.dart';

import 'class_suggestor.dart';
import 'offset_util.dart';

export 'class_suggestor.dart' show ClassSuggestor;
export 'wsd_util.dart';
Expand Down Expand Up @@ -729,12 +729,6 @@ void handleCascadedPropsByName(
}
}

extension on SourceFile {
/// Return the offset of the first character on the line following the line
/// containing the given [offset].
int getOffsetOfLineAfter(int offset) => getOffset(getLine(offset) + 1);
}

bool _isDataAttributePropKey(Expression expression) {
final keyValue = _getStringConstantValue(expression);
return keyValue != null && keyValue.startsWith('data-');
Expand Down
7 changes: 7 additions & 0 deletions lib/src/util/offset_util.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:source_span/source_span.dart';

extension SourceFileOffsetUtils on SourceFile {
/// Return the offset of the first character on the line following the line
/// containing the given [offset].
int getOffsetOfLineAfter(int offset) => getOffset(getLine(offset) + 1);
}
6 changes: 3 additions & 3 deletions test/dart2_9_suggestors/dart2_9_utilities_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ void main() {
.variables
.variables
.first
.initializer as MethodInvocation)
.initializer! as MethodInvocation)
.argumentList;

expect(getGeneratedFactoryConfigArg(argList), isNull);
Expand Down Expand Up @@ -74,7 +74,7 @@ void main() {
.variables
.variables
.first
.initializer as MethodInvocation)
.initializer! as MethodInvocation)
.argumentList;

final returnValue = getGeneratedFactoryConfigArg(argList)!;
Expand Down Expand Up @@ -156,7 +156,7 @@ void main() {
.variables
.variables
.first
.initializer as MethodInvocation)
.initializer! as MethodInvocation)
.argumentList;
final uiFunctionArgList = memoArgList.arguments
.whereType<MethodInvocation>()
Expand Down
Loading