diff --git a/over_react_analyzer_plugin/lib/src/diagnostic/bool_prop_name_readability.dart b/over_react_analyzer_plugin/lib/src/diagnostic/bool_prop_name_readability.dart index efe20e733..374bd87f2 100644 --- a/over_react_analyzer_plugin/lib/src/diagnostic/bool_prop_name_readability.dart +++ b/over_react_analyzer_plugin/lib/src/diagnostic/bool_prop_name_readability.dart @@ -1,6 +1,5 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:analyzer/dart/element/element.dart'; import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; class BoolPropNameReadabilityDiagnostic extends DiagnosticContributor { @@ -18,11 +17,11 @@ class BoolPropNameReadabilityDiagnostic extends DiagnosticContributor { result.unit.accept(visitor); - final returnClasses = visitor.returnClasses; + final returnMixins = visitor.returnMixins; - for (final propsClass in returnClasses) { - final classFields = propsClass.declaredElement.fields; - for (final field in classFields) { + for (final propsClass in returnMixins) { + final mixinFields = propsClass.declaredElement.fields; + for (final field in mixinFields) { final propName = field.name; if (field.type != typeProvider.boolType) continue; if (propName == null) continue; // just in case @@ -70,10 +69,16 @@ bool hasBooleanContain(String propName) { return propName.toLowerCase().contains(RegExp('(${allowedContainsForBoolProp.join("|")})')); } -bool isPropsClass(ClassDeclaration c) => c.declaredElement.allSupertypes.any((m) => m.name == 'UiProps'); +bool isPropsClass(ClassDeclaration classDecl) { + return classDecl.declaredElement.allSupertypes.any((m) => m.getDisplayString() == 'UiProps'); +} + +bool isPropsMixin(MixinDeclaration mixinDecl) { + return mixinDecl.onClause.superclassConstraints.any((m) => m.name.name == 'UiProps'); +} class PropsVisitor extends SimpleAstVisitor { - List returnClasses = []; + List returnMixins = []; @override void visitCompilationUnit(CompilationUnit node) { node.visitChildren(this); @@ -82,7 +87,14 @@ class PropsVisitor extends SimpleAstVisitor { @override void visitClassDeclaration(ClassDeclaration node) { if (isPropsClass(node)) { - returnClasses.add(node); + returnMixins.add(node); + } + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + if (isPropsMixin(node)) { + returnMixins.add(node); } } } diff --git a/over_react_analyzer_plugin/lib/src/diagnostic/dom_prop_types.dart b/over_react_analyzer_plugin/lib/src/diagnostic/dom_prop_types.dart index 062f1b376..93294efe2 100644 --- a/over_react_analyzer_plugin/lib/src/diagnostic/dom_prop_types.dart +++ b/over_react_analyzer_plugin/lib/src/diagnostic/dom_prop_types.dart @@ -1,10 +1,12 @@ import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; import 'package:over_react_analyzer_plugin/src/fluent_interface_util.dart'; +/// A diagnostic that warns when an HTML attribute set on an OverReact `Dom` component builder is invalid +/// based on the `: []` schema found within [allowedHtmlElementsForAttribute]. class InvalidDomAttributeDiagnostic extends ComponentUsageDiagnosticContributor { static const code = DiagnosticCode( 'over_react_invalid_dom_attribute', - "{}' isn't a valid HTML attribute prop for '{}'. It may only be used on: {}", + "'{0}' isn't a valid HTML attribute prop for '{1}'. It may only be used on: {2}", AnalysisErrorSeverity.WARNING, AnalysisErrorType.STATIC_WARNING, ); @@ -29,7 +31,7 @@ class InvalidDomAttributeDiagnostic extends ComponentUsageDiagnosticContributor if (!allowedElements.contains(nodeName)) { collector.addError(code, result.locationFor(lhs.propertyName), - errorMessageArgs: [propName, nodeName, allowedElements.join(',')]); + errorMessageArgs: [propName, 'Dom.$nodeName()', allowedElements.map((name) => 'Dom.$name()').join(',')]); } }); } @@ -50,6 +52,9 @@ String _camelToSpinalCase(String camel) { .toLowerCase(); } +/// A map keyed with HTML attributes and iterable values of the HTML element names they are allowed on. +/// +/// > See: [InvalidDomAttributeDiagnostic] const allowedHtmlElementsForAttribute = { 'accept': ['form', 'input'], 'accept-charset': ['form'], diff --git a/over_react_analyzer_plugin/lib/src/diagnostic/pseudo_static_lifecycle.dart b/over_react_analyzer_plugin/lib/src/diagnostic/pseudo_static_lifecycle.dart index f6cb2ab2c..692e05860 100644 --- a/over_react_analyzer_plugin/lib/src/diagnostic/pseudo_static_lifecycle.dart +++ b/over_react_analyzer_plugin/lib/src/diagnostic/pseudo_static_lifecycle.dart @@ -3,7 +3,7 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; -const staticMethodNames = ['getDefaultProps', 'getDerivedStateFromProps']; +const staticMethodNames = ['getDefaultProps', 'defaultProps', 'getDerivedStateFromProps']; const instanceMemberWhitelist = [ 'newProps', 'newState', diff --git a/playground/web/dom_prop_types.dart b/playground/web/dom_prop_types.dart new file mode 100644 index 000000000..8603d6136 --- /dev/null +++ b/playground/web/dom_prop_types.dart @@ -0,0 +1,62 @@ +import 'package:over_react/over_react.dart'; + +part 'dom_prop_types.over_react.g.dart'; + +main() { + final content = Fragment()( + (Dom.a() + // This should have a lint. + ..size = 1 + // These should have no lint + ..href = null + ..hrefLang = null + ..download = null + ..rel = null + ..target = null + )(), + (Dom.abbr() + // This should have a lint. + ..size = 1 + // These should have no lint + ..title = 'foo' + )(), + (Dom.address() + // This should have a lint. + ..size = 1 + // These should have no lint + ..title = 'foo' + )(), + (Dom.area() + // This should have a lint. + ..size = 1 + // These should have no lint + ..coords = 1 + ..download = null + ..href = null + ..hrefLang = null + ..rel = null + ..shape = null + ..target = null + )(), + (Dom.article() + // This should have a lint. + ..size = 1 + // These should have no lint + ..title = 'foo' + )(), + (Dom.aside() + // This should have a lint. + ..size = 1 + // These should have no lint + ..title = 'foo' + )(), + (Dom.audio() + // This should have a lint. + ..size = 1 + // These should have no lint + ..autoPlay = true + ..controls = true + ..muted = true + )(), + ); +} diff --git a/playground/web/pseudo_static_lifecycle.dart b/playground/web/pseudo_static_lifecycle.dart new file mode 100644 index 000000000..b5cf734ce --- /dev/null +++ b/playground/web/pseudo_static_lifecycle.dart @@ -0,0 +1,40 @@ +import 'package:over_react/over_react.dart'; + +part 'dom_prop_types.over_react.g.dart'; + +UiFactory HammerTime = + // ignore: undefined_identifier + _$HammerTime; + +mixin HammerTimeProps on UiProps { + String somethingThatCanBeTouched; +} + +mixin HammerTimeState on UiState {} + +class HammerTimeComponent extends UiStatefulComponent2 { + final mcHammer = 'cant touch this'; + + @override + get defaultProps { + return newProps() + ..somethingThatCanBeTouched = mcHammer; + } + + @override + getDerivedStateFromProps(Map nextProps, Map prevState) { + final tNextProps = typedPropsFactory(nextProps); + final tNextPropsJs = typedPropsFactoryJs(nextProps); + final tPrevState = typedStateFactory(prevState); + final tPrevStateJs = typedStateFactoryJs(prevState); + + if (props.somethingThatCanBeTouched == mcHammer) { + return newState(); + } + + return null; + } + + @override + render() {} +}