Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPLAT-7393 Add logic to handle components with mixin(s) #52

Merged
merged 2 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import 'package:codemod/codemod.dart';
import 'package:over_react_codemod/src/react16_suggestors/react16_utilities.dart';

import '../constants.dart';
import 'component2_constants.dart';
import 'component2_utilities.dart';

/// Suggestor that replaces `UiComponent` with `UiComponent2` in extends clauses
Expand Down Expand Up @@ -139,10 +138,7 @@ class ClassNameAndAnnotationMigrator extends GeneralizingAstVisitor
wasUpdated = true;
});

if (extendsName.name == 'UiComponent' ||
extendsName.name == 'UiStatefulComponent' ||
extendsName.name == 'FluxUiComponent' ||
extendsName.name == 'FluxUiStatefulComponent') {
if (upgradableV1ComponentClassNames.contains(extendsName.name)) {
// Update `UiComponent` or `UiStatefulComponent` extends clause.
yieldPatch(
extendsName.end,
Expand All @@ -154,15 +150,55 @@ class ClassNameAndAnnotationMigrator extends GeneralizingAstVisitor
}
}

// Add comment for abstract components that are updated
if (wasUpdated &&
canBeExtendedFrom(node) &&
!hasComment(node, sourceFile, abstractClassMessage)) {
yieldPatch(
node.offset,
node.offset,
'$abstractClassMessage\n',
);
if (wasUpdated) {
_addFixMeCommentPatches(node, extendsName);
}
}

void _addFixMeCommentPatches(ClassDeclaration node, Identifier extendsName) {
final extendsNameStr = extendsName.toString().replaceAll('2', '');
final classToUpgradeTo =
upgradableV1ComponentClassNames.contains(extendsNameStr)
? '`${extendsNameStr}2`'
: 'a class that now extends from `UiComponent2`';

[
_FixMeCommentPatch(hasOneOrMoreMixins, () => node,
'FIXME: Before upgrading this component to $classToUpgradeTo, verify that none of the mixin(s) contain implementations of any React lifecycle methods that are not supported in $classToUpgradeTo.'),
_FixMeCommentPatch(
(node) => shouldUpgradeAbstractComponents && canBeExtendedFrom(node),
() => node,
'FIXME: Abstract class has been updated to $classToUpgradeTo. This is a breaking change if this class is exported.'),
].forEach((patch) {
if (!patch.shouldYieldPatch(node) ||
hasMultilineDocComment(node, sourceFile, patch.commentSrc)) {
return;
}

final patchOffset = node.metadata?.beginToken?.offset ??
node.firstTokenAfterCommentAndMetadata.offset;
yieldPatch(patchOffset, patchOffset, patch.commentSrc);
});
}
}

class _FixMeCommentPatch {
final bool Function(ClassDeclaration node) shouldYieldPatch;
final ClassDeclaration Function() getNode;

_FixMeCommentPatch(this.shouldYieldPatch, this.getNode, String commentSrc) {
final node = getNode();
final indentationLevel = node.beginToken.charOffset;
final fixmeCommentLineStart =
indentationLevel == 0 ? '///' : (' ' * indentationLevel) + '///';
final fixmeCommentStart = node.documentationComment != null
? '$fixmeCommentLineStart\n$fixmeCommentLineStart'
: fixmeCommentLineStart;
final fixmeCommentEnd = '\n';

_commentSrc = '$fixmeCommentStart $commentSrc$fixmeCommentEnd';
}

String _commentSrc;
String get commentSrc => _commentSrc;
}
3 changes: 0 additions & 3 deletions lib/src/component2_suggestors/component2_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,3 @@ String getDeperecationMessage(String methodName) {
///
$revertInstructions''';
}

const abstractClassMessage =
'/// FIXME: Abstract class has been updated to Component2. This is a breaking change if this class is exported.';
9 changes: 9 additions & 0 deletions lib/src/component2_suggestors/component2_utilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ bool extendsComponent2(ClassDeclaration classNode) {
}
}

/// Returns whether or not [classNode] has one or more mixins.
bool hasOneOrMoreMixins(ClassDeclaration classNode) =>
classNode?.withClause != null;

/// Returns whether or not [classNode] can be fully upgraded to Component2.
///
/// In order for a component to be fully upgradable, the component must:
///
/// * not have a `with` clause
/// * extend directly from `UiComponent`, `UiStatefulComponent`,
/// `FluxUiComponent`, `FluxUiStatefulComponent`, or `react.Component`
/// * contain only the lifecycle methods that the codemod updates:
Expand All @@ -66,6 +71,10 @@ bool fullyUpgradableToComponent2(ClassDeclaration classNode) {
return false;
}

if (hasOneOrMoreMixins(classNode)) {
return false;
}

// Check that class extends directly from Component classes.
String reactImportName =
getImportNamespace(classNode, 'package:react/react.dart');
Expand Down
8 changes: 8 additions & 0 deletions lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ const List<String> overReactMixinAnnotationNames = [
'StateMixin',
];

/// A list of the names of the core component classes that can be upgraded to a "v2" version.
const List<String> upgradableV1ComponentClassNames = [
'UiComponent',
'UiStatefulComponent',
'FluxUiComponent',
'FluxUiStatefulComponent',
];

/// Dart type for the static meta field on props classes.
const String propsMetaType = 'PropsMeta';

Expand Down
42 changes: 41 additions & 1 deletion lib/src/react16_suggestors/react16_utilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,53 @@ bool hasComment(AstNode node, SourceFile sourceFile, String comment) {
return commentText?.contains(comment) ?? false;
}

/// Whether the [node] has a documentation comment that has
/// any lines that match lines found within the provided [comment].
bool hasMultilineDocComment(
AnnotatedNode node, SourceFile sourceFile, String comment) {
final nodeComments = nodeCommentSpan(node, sourceFile)
.text
.replaceAll('///', '')
.split('\n')
.map((line) => line.replaceAll('\n', '').trim())
.toList()
..removeWhere((line) => line.isEmpty);
final commentLines = comment
.replaceAll('///', '')
.trimLeft()
.split('\n')
.map((line) => line.replaceAll('\n', '').trim())
.toList()
..removeWhere((line) => line.isEmpty);

bool match = false;

for (var i = 0; i < commentLines.length; i++) {
final potentialMatch = commentLines[i];
if (nodeComments.any((line) => line == potentialMatch)) {
match = true;
break;
}
}

return match;
}

/// Returns the `SourceSpan` value of any comments on the provided [node] within the [sourceFile].
SourceSpan nodeCommentSpan(AnnotatedNode node, SourceFile sourceFile) {
return sourceFile.span(
node.beginToken.offset,
node.metadata?.beginToken?.offset ??
node.firstTokenAfterCommentAndMetadata.offset);
}

/// Returns an iterable of all the comments from [beginToken] to the end of the
/// file.
///
/// Comments are part of the normal stream, and need to be accessed via
/// [Token.precedingComments], so it's difficult to iterate over them without
/// this method.
Iterable allComments(Token beginToken) sync* {
Iterable<Token> allComments(Token beginToken) sync* {
var currentToken = beginToken;
while (!currentToken.isEof) {
var currentComment = currentToken.precedingComments;
Expand Down
Loading