Skip to content
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
8 changes: 4 additions & 4 deletions lib/src/builder/parsing/declarations_from_members.dart
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ Iterable<BoilerplateDeclaration> getBoilerplateDeclarations(

const errorStateOnly =
'Could not find matching factory, props class, and component class in this file;'
' these are required to use UiState';
' these are required to use UiState.';

const errorFactoryOnly = 'Could not find matching props class in this file;'
' this is required to declare a props map view or function component,'
Expand All @@ -376,10 +376,10 @@ const errorComponentClassOnly = 'Could not find matching factory and props class
' these are required to declare a class-based component.';

const errorNoFactory = 'Could not find a matching factory in this file;'
' this is required to declare a component or props map view';
' this is required to declare a component or props map view.';

const errorNoProps = 'Could not find a matching props class in this file;'
' this is required to declare a component or props map view';
' this is required to declare a component or props map view.';

const errorNoComponent = 'Could not find a matching component class in this file;'
' this is required to declare a class-based component';
' this is required to declare a class-based component.';
37 changes: 31 additions & 6 deletions lib/src/builder/parsing/members_from_ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,9 @@ class _BoilerplateMemberDetector {
// Thus, it's non-legacy boilerplate.

VersionConfidences getConfidence() {
final overridesIsClassGenerated = classish.members
.whereType<MethodDeclaration>()
.any((member) => member.isGetter && member.name.name == r'$isClassGenerated');
// Handle classes that look like props but are really just used as interfaces, and aren't extended from or directly used as a component's props.
// Watch out for empty mixins, though; those are valid props/state mixins.
if (overridesIsClassGenerated ||
if (_overridesIsClassGenerated(classish) ||
(node is! MixinDeclaration && onlyImplementsThings(classish))) {
return VersionConfidences.none();
} else if (classish.members.whereType<ConstructorDeclaration>().isNotEmpty) {
Expand Down Expand Up @@ -429,26 +426,54 @@ class _BoilerplateMemberDetector {
return false;
}

/// UiComponent, UiComponent2, UiStatefulComponent, FluxUiComponent, CustomUiComponent, ...
static final _componentBaseClassPattern = RegExp(r'Ui\w*Component');

bool _detectPotentialComponent(ClassishDeclaration classish) {
// Don't detect react-dart components as boilerplate components, since they cause issues with grouping
// if they're in the same file as an OverReact component with non-matching names.
if (!const {'Component', 'Component2'}.contains(classish.superclass?.nameWithoutPrefix)) {
if (classish.name.name.endsWith('Component') ||
classish.allSuperTypes
.map((t) => t.typeNameWithoutPrefix)
.any(const {'UiComponent', 'UiComponent2'}.contains) ||
.any(_componentBaseClassPattern.hasMatch) ||
(classish.superclass?.typeArguments?.arguments
?.map((t) => t.typeNameWithoutPrefix)
?.any(propsOrMixinNamePattern.hasMatch) ??
false)) {
onComponent(BoilerplateComponent(classish, VersionConfidences.all(Confidence.neutral)));
const mixinBoilerplateBaseClasses = {
'UiComponent2',
'UiStatefulComponent2',
'FluxUiComponent2',
'FluxUiStatefulComponent2'
};
final confidences = VersionConfidences(
// If the component extends from a base class known to be supported by the new boilerplate,
// has no annotation, is not abstract, and does not have $isClassGenerated, then it's
// most likely intended to be part of a new boilerplate class component declaration.
//
// Make this `likely` so that components that don't get associated with factory/props
// due to naming issues aren't silently ignored.
v4_mixinBased: !classish.hasAbstractKeyword &&
!_overridesIsClassGenerated(classish) &&
mixinBoilerplateBaseClasses.contains(classish.superclass?.nameWithoutPrefix)
? Confidence.likely
: Confidence.neutral,
v2_legacyBackwardsCompat: Confidence.neutral,
v3_legacyDart2Only: Confidence.neutral,
Comment on lines +450 to +463
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really cool

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I'm super happy that the confidence approach made this kind of change easy

);
onComponent(BoilerplateComponent(classish, confidences));

return true;
}
}

return false;
}

static bool _overridesIsClassGenerated(ClassishDeclaration classish) => classish.members
.whereType<MethodDeclaration>()
.any((member) => member.isGetter && member.name.name == r'$isClassGenerated');
}

class _BoilerplateMemberDetectorVisitor extends SimpleAstVisitor<void> {
Expand Down
1 change: 1 addition & 0 deletions lib/src/component_declaration/component_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ abstract class UiProps extends MapBase
'1. Something went wrong when initializing the `\$${runtimeType}Factory` variable in the generated code.\n'
' It\'s possible React swallowed errors thrown during that initialization, so try pausing on caught exceptions to see it.\n'
'2. This is a props map view factory (declared as just a factory and props), and should not be invoked.\n'
' This can also happen if your component didn\'t get grouped with your factory/props (e.g., if its name doesn\'t match).'
'3. This is a function component factory that was set up improperly, not wrapping the generated function in `uiFunction`.\n'
'4. componentFactory was erroneously assigned to null on this UiProps instance, potentially in an HOC function.');
}
Expand Down
66 changes: 66 additions & 0 deletions test/vm_tests/builder/declaration_parsing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2045,6 +2045,72 @@ main() {
)));
});
});

group('(new boilerplate)', () {
// Tests the same codepath as "a component is declared without matching factory/props",
// but using a different real-world scenario to ensure it behaves the same.
test('a component doesn\'t get grouped with a factory/props due to mismatched name', () {
setUpAndParse(r'''
UiFactory<FooProps> Foo = _$Foo;
mixin FooProps on UiProps {}
class BarComponent<FooProps> extends UiComponent2 {
render() {}
}
''');

verify(logger.severe(contains(errorComponentClassOnly)));
});

group('a component is declared without matching factory/props', () {
group('with a base class of', () {
@isTest
void sharedTest(String baseClassName) {
test(baseClassName, () {
setUpAndParse('''
class FooComponent extends $baseClassName<FooProps> {
render() {}
}
''');

verify(logger.severe(contains(errorComponentClassOnly)));
});
}

sharedTest('UiComponent2');
sharedTest('UiStatefulComponent2');
sharedTest('FluxUiComponent2');
sharedTest('FluxUiStatefulComponent2');
});

group('unless', () {
test('it has a nonstandard base class', () {
setUpAndParse(r'''
class FooComponent extends SomeNonstandardBaseUiComponent<FooProps> {
render() {}
}
''');
});

test('it is abstract', () {
setUpAndParse(r'''
abstract class FooComponent extends UiComponent2<FooProps> {
render() {}
}
''');
});

test('it overrides \$isClassGenerated', () {
setUpAndParse(r'''
class FooComponent extends UiComponent2<FooProps> {
render() {}

bool get $isClassGenerated => true;
}
''');
});
});
});
});
});

group('and logs a warning when', () {
Expand Down