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

FED-2112 Required props lint: respect @Props(ignoreRequiredProps: ...) #873

Merged
merged 17 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
939993f
Merge remote-tracking branch 'origin/FED-2015-opt-out-required-props-…
greglittlefield-wf Jan 22, 2024
e0c0f45
Merge branch 'FED-2015-opt-out-required-props-2' into analyzer-plugin…
greglittlefield-wf Jan 23, 2024
4846adf
Merge remote-tracking branch 'origin/FED-2015-opt-out-required-props-…
greglittlefield-wf Jan 23, 2024
b2a1191
Respect ignoreRequiredProps in analyzer plugin, add tests
greglittlefield-wf Jan 22, 2024
c859acb
Ignore invalid_use_of_visible_for_overriding_member in generated code…
greglittlefield-wf Jan 22, 2024
77e3ec5
Add invalid_use_of_visible_for_overriding_member in generated code
greglittlefield-wf Jan 22, 2024
008939c
Remove invalid_use_of_visible_for_overriding_member workaround
greglittlefield-wf Jan 22, 2024
92a2e82
Add some missing test cases
greglittlefield-wf Jan 22, 2024
ef7d8d2
Add integration test for ignored props
greglittlefield-wf Jan 22, 2024
b97d9ad
Update playground
greglittlefield-wf Jan 22, 2024
8a2e187
Add regression test when typing boilerplate annotations
greglittlefield-wf Jan 23, 2024
56d5c97
Severe-log instead of throwing when failing to instantiate meta in bo…
greglittlefield-wf Jan 23, 2024
a8e401a
Check in generated code changes (added ignore_for_file)
greglittlefield-wf Jan 23, 2024
fb68796
Update gold files with added ignore_for_file
greglittlefield-wf Jan 23, 2024
6b1e1c7
Update tests after making metaNode non-nullable
greglittlefield-wf Jan 23, 2024
14d6c32
Update subtypeOf error test
greglittlefield-wf Jan 23, 2024
3dc54cf
Merge remote-tracking branch 'origin/v5_wip' into analyzer-plugin-opt…
greglittlefield-wf Jan 25, 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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions example/builder/src/functional_consumed_props.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ mixin ParentOnlyPropsMixin on UiProps {
}

mixin SharedPropsMixin on UiProps {
late String requiredProp;
String? aPropToBePassed;
}

@Props(ignoreRequiredProps: {'requiredProp'})
class SomeParentProps = UiProps with ParentOnlyPropsMixin, SharedPropsMixin;

UiFactory<SomeParentProps> SomeParent = uiFunction((props) {
Expand All @@ -40,6 +42,7 @@ UiFactory<SomeParentProps> SomeParent = uiFunction((props) {
_$SomeParentConfig, // ignore: undefined_identifier
);

@Props(ignoreRequiredProps: {'requiredProp'})
class SomeChildProps = UiProps with SharedPropsMixin;

UiFactory<SomeChildProps> SomeChild = uiFunction((props) {
Expand Down
1 change: 1 addition & 0 deletions example/builder/src/props_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ part 'props_mixin.over_react.g.dart';

mixin ExamplePropsMixin on UiProps {
String? propMixin1;
late String requiredProp;
}

mixin RequiresOtherMixinPropsMixin<T extends Iterable, U>
Expand Down
13 changes: 12 additions & 1 deletion lib/src/builder/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,17 @@ class OverReactBuilder extends Builder {
return null;
}

static const _ignoreForFileCodes = {
// Needed when the component itself or some of its props are deprecated,
// or if deprecated types are referenced within the boilerplate.
'deprecated_member_use_from_same_package',
// These two are needed for the `?? null` workaround in prop accessors.
'unnecessary_null_in_if_null_operators',
'prefer_null_aware_operators',
// Needed to ignore the `requiredPropNamesToSkipValidation` checks.
'invalid_use_of_visible_for_overriding_member',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this ignore to fix my test setup, which validated there were no warnings in generated code

};

static FutureOr<void> _writePart(BuildStep buildStep, AssetId outputId, Iterable<String> outputs,
{required String nullSafetyCommentText, String? languageVersionComment}) async {
final partOf = "'${p.basename(buildStep.inputId.uri.toString())}'";
Expand All @@ -264,7 +275,7 @@ class OverReactBuilder extends Builder {
buffer
..writeln('// GENERATED CODE - DO NOT MODIFY BY HAND')
..writeln()
..writeln('// ignore_for_file: deprecated_member_use_from_same_package, unnecessary_null_in_if_null_operators, prefer_null_aware_operators')
..writeln('// ignore_for_file: ${_ignoreForFileCodes.join(', ')}')
..writeln('part of $partOf;')
..writeln()
..writeln(_headerLine)
Expand Down
4 changes: 3 additions & 1 deletion lib/src/builder/codegen/accessors_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ abstract class TypedMapAccessorsGenerator extends BoilerplateDeclarationGenerato
isPotentiallyNullable = true;

if (type.isProps && disableRequiredPropValidation == null) {
requiredPropChecks.add(' if(!props.containsKey($keyValue)) {\n'
requiredPropChecks.add(
' if(!props.containsKey($keyValue) && !requiredPropNamesToSkipValidation.contains(\'$accessorName\')) {\n'
' throw MissingRequiredPropsError(${stringLiteral('Required prop `$accessorName` is missing.')});\n'
'}\n');
}
Expand Down Expand Up @@ -518,6 +519,7 @@ class TypedMapType {
final bool isProps;
final bool isAbstract;
final bool isMixin;

// ignore: avoid_positional_boolean_parameters
const TypedMapType(this.isProps, this.isAbstract, this.isMixin);

Expand Down
21 changes: 19 additions & 2 deletions lib/src/builder/codegen/typed_map_impl_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../parsing.dart';
import '../util.dart';
import 'names.dart';
import 'util.dart';
import 'package:over_react/src/component_declaration/annotations.dart' as annotations;

/// Base class for generating concrete factory and props/state class implementations.
abstract class TypedMapImplGenerator extends BoilerplateDeclarationGenerator {
Expand Down Expand Up @@ -134,6 +135,7 @@ abstract class TypedMapImplGenerator extends BoilerplateDeclarationGenerator {
String? componentFactoryName,
String? propKeyNamespace,
List<String>? allPropsMixins,
required Set<String>? requiredPropNamesToSkipValidation,
}) {
if (isProps) {
if (componentFactoryName == null || propKeyNamespace == null) {
Expand Down Expand Up @@ -243,6 +245,13 @@ abstract class TypedMapImplGenerator extends BoilerplateDeclarationGenerator {
..writeln(
' String \$getPropKey(void Function(Map m) accessMap) => $topLevelGetPropKeyAliasName(accessMap, (map) => ${names.implName}(map));');
}
if (requiredPropNamesToSkipValidation != null) {
buffer
..writeln()
..writeln(' @override')
..writeln(
' Set<String> get requiredPropNamesToSkipValidation => const {${requiredPropNamesToSkipValidation.map(stringLiteral).join(', ')}};');
}

// End of class body
buffer.writeln('}');
Expand Down Expand Up @@ -332,12 +341,16 @@ class _LegacyTypedMapImplGenerator extends TypedMapImplGenerator {
outputContentsBuffer.write(_generateConcretePropsOrStateImpl(
componentFactoryName: ComponentNames(declaration.component.name.name).componentFactoryName,
propKeyNamespace: getAccessorKeyNamespace(names, member.meta),
requiredPropNamesToSkipValidation:
member.meta.tryCast<annotations.Props>()?.ignoreRequiredProps,
));
}

@override
void _generateStateImpl() {
outputContentsBuffer.write(_generateConcretePropsOrStateImpl());
outputContentsBuffer.write(_generateConcretePropsOrStateImpl(
requiredPropNamesToSkipValidation: null,
));
}

@override
Expand Down Expand Up @@ -451,12 +464,16 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator {
// This doesn't really apply to the new boilerplate
propKeyNamespace: '',
allPropsMixins: allPropsMixins,
requiredPropNamesToSkipValidation:
member.meta.tryCast<annotations.Props>()?.ignoreRequiredProps,
));
}

@override
void _generateStateImpl() {
outputContentsBuffer.write(_generateConcretePropsOrStateImpl());
outputContentsBuffer.write(_generateConcretePropsOrStateImpl(
requiredPropNamesToSkipValidation: null,
));
}

@override
Expand Down
1 change: 1 addition & 0 deletions lib/src/builder/parsing/members.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:build/build.dart' show log;
import 'package:collection/collection.dart' show IterableExtension;
import 'package:meta/meta.dart';
import 'package:over_react/src/builder/codegen/names.dart';
Expand Down
57 changes: 46 additions & 11 deletions lib/src/builder/parsing/members/props_and_state_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,52 @@ class _PropsStateStringHelpersImpl extends Object with PropsStateStringHelpers {

/// Uses [InstantiatedMeta] to analyze [node] and determine the proper annotation.
annotations.TypedMap getPropsOrStateAnnotation(bool isProps, AnnotatedNode node) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whitespace-agnostic diff here is helpful

final meta = isProps
? (InstantiatedMeta.fromNode<annotations.Props>(node) ??
InstantiatedMeta.fromNode<annotations.AbstractProps>(node) ??
// ignore: deprecated_member_use_from_same_package
InstantiatedMeta.fromNode<annotations.PropsMixin>(node))
: (InstantiatedMeta.fromNode<annotations.State>(node) ??
InstantiatedMeta.fromNode<annotations.AbstractState>(node) ??
// ignore: deprecated_member_use_from_same_package
InstantiatedMeta.fromNode<annotations.StateMixin>(node));

return meta?.value ?? (isProps ? annotations.Props() : annotations.State());
late final defaultValue = isProps ? annotations.Props() : annotations.State();

InstantiatedMeta<annotations.TypedMap>? meta;
try {
meta = isProps
? (InstantiatedMeta.fromNode<annotations.Props>(node) ??
InstantiatedMeta.fromNode<annotations.AbstractProps>(node) ??
// ignore: deprecated_member_use_from_same_package
InstantiatedMeta.fromNode<annotations.PropsMixin>(node))
: (InstantiatedMeta.fromNode<annotations.State>(node) ??
InstantiatedMeta.fromNode<annotations.AbstractState>(node) ??
// ignore: deprecated_member_use_from_same_package
InstantiatedMeta.fromNode<annotations.StateMixin>(node));

if (meta == null) return defaultValue;

if (meta.potentiallyIncompleteValue is annotations.Props) {
if (meta.unsupportedArguments.length == 1) {
final arg = meta.unsupportedArguments[0];
if (arg is NamedExpression && arg.name.label.name == 'ignoreRequiredProps') {
// Attempt to parse the value, and fall through if something goes wrong,
// and let `meta?.value` below throw.
final expression = arg.expression;
if (expression is SetOrMapLiteral) {
final simpleStringElements =
expression.elements.whereType<SimpleStringLiteral>().toList();
if (simpleStringElements.length == expression.elements.length) {
return annotations.Props(
keyNamespace: meta.potentiallyIncompleteValue.keyNamespace,
ignoreRequiredProps: simpleStringElements.map((e) => e.value).toSet(),
);
}
}
}
}
}

return meta.value;
} catch (e, st) {
// Log a severe error instead of throwing, so that the error doesn't propagate when we're doing parsing within
// the analyzer plugin.
// This severe error will fail the build and be presented to the consumer.
log.severe(
'Error reading annotation${meta == null ? '' : ': ${meta.metaNode.toSource()}'}', e, st);
return defaultValue;
}
}

/// If a [ClassMember] exists in [node] with the name `meta`, this will
Expand Down
54 changes: 32 additions & 22 deletions lib/src/builder/parsing/meta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import 'dart:mirrors' as mirrors;

import 'package:analyzer/dart/ast/ast.dart';
import 'package:build/build.dart' show log;
import 'package:collection/collection.dart' show IterableExtension;
import 'package:transformer_utils/transformer_utils.dart';

Expand Down Expand Up @@ -44,7 +45,7 @@ Annotation? _getMatchingAnnotation(AnnotatedNode member, Type annotationType) {
/// Based off of [NodeWithMeta].
class InstantiatedMeta<TMeta> {
/// The node of the [TMeta] annotation, if it exists.
final Annotation? metaNode;
final Annotation metaNode;

/// A reflectively-instantiated version of [metaNode], if it exists.
final TMeta _value;
Expand All @@ -63,6 +64,8 @@ class InstantiatedMeta<TMeta> {
/// The instantiated annotation will be available via [value].
static InstantiatedMeta<T>? fromNode<T>(AnnotatedNode node) {
final metaNode = _getMatchingAnnotation(node, T);
if (metaNode == null) return null;

final unsupportedArguments = <Expression>[];
final value =
instantiateAnnotationTyped<T>(node, onUnsupportedArgument: unsupportedArguments.add);
Expand Down Expand Up @@ -102,31 +105,38 @@ class InstantiatedComponentMeta<TMeta> extends InstantiatedMeta<TMeta> {
final Identifier? subtypeOfValue;

InstantiatedComponentMeta._(
Annotation? metaNode, TMeta meta, List<Expression> unsupportedArguments, this.subtypeOfValue)
Annotation metaNode, TMeta meta, List<Expression> unsupportedArguments, this.subtypeOfValue)
: super._(metaNode, meta, unsupportedArguments);

static InstantiatedComponentMeta<T>? fromNode<T>(AnnotatedNode node) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whitespace-agnostic diff here is helpful

final instantiated = InstantiatedMeta.fromNode<T>(node);

if (instantiated == null) return null;

Identifier? subtypeOfValue;

NamedExpression? subtypeOfParam = instantiated.unsupportedArguments
.whereType<NamedExpression>()
.firstWhereOrNull((expression) => expression.name.label.name == _subtypeOfParamName);

if (subtypeOfParam != null) {
final expression = subtypeOfParam.expression;
if (expression is Identifier) {
subtypeOfValue = expression;
instantiated.unsupportedArguments.remove(subtypeOfParam);
} else {
throw Exception('`$_subtypeOfParamName` must be an identifier: $subtypeOfParam');
try {
final instantiated = InstantiatedMeta.fromNode<T>(node);
if (instantiated == null) return null;

Identifier? subtypeOfValue;

NamedExpression? subtypeOfParam = instantiated.unsupportedArguments
.whereType<NamedExpression>()
.firstWhereOrNull((expression) => expression.name.label.name == _subtypeOfParamName);

if (subtypeOfParam != null) {
final expression = subtypeOfParam.expression;
if (expression is Identifier) {
subtypeOfValue = expression;
instantiated.unsupportedArguments.remove(subtypeOfParam);
} else {
throw Exception('`$_subtypeOfParamName` must be an identifier: $subtypeOfParam');
}
}
}

return InstantiatedComponentMeta._(instantiated.metaNode, instantiated.value,
instantiated.unsupportedArguments, subtypeOfValue);
return InstantiatedComponentMeta._(instantiated.metaNode, instantiated.value,
instantiated.unsupportedArguments, subtypeOfValue);
} catch (e, st) {
// Log a severe error instead of throwing, so that the error doesn't propagate when we're doing parsing within
// the analyzer plugin.
// This severe error will fail the build and be presented to the consumer.
log.severe('Error reading component annotation', e, st);
return null;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/src/component/abstract_transition.over_react.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/src/component/aria_mixin.over_react.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/src/component/dummy_component2.over_react.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading