Skip to content
Draft
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
12 changes: 5 additions & 7 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ command:
categories:
flutter_projects:
- packages/mix
- packages/mix_schema
- packages/mix_tailwinds
- packages/mix/example
- packages/mix_lint_test
- packages/annotations
Expand All @@ -36,11 +38,9 @@ categories:
scripts:
# ANALYSIS
analyze:
run: melos run analyze:dart && melos run analyze:dcm
run: melos run analyze:dart --no-select && melos run analyze:dcm --no-select
description: Run all static analysis checks.
exec:
failFast: true


analyze:dart:
run: melos exec -c 4 -- dart analyze .
description: Run Dart static analysis checks.
Expand Down Expand Up @@ -126,10 +126,8 @@ scripts:
dirExists: test

ci:
run: melos run test:flutter && melos run test:dart
run: melos run test:flutter --no-select && melos run test:dart --no-select
description: Run flutter and dart tests
packageFilters:
dirExists: test

brb:
run: melos run gen:build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import '../checkers.dart';
import '../curated/styler_surface_metadata.dart';
import '../curated/type_metadata.dart';
import '../errors.dart';
import '../helpers/type_hierarchy.dart';
import '../helpers/widget_call_planner.dart';
import '../models/annotation_config.dart';
import '../models/field_model.dart';
Expand Down Expand Up @@ -499,45 +498,14 @@ class SpecStylerClassBuilder {
);
}

final widgetClass = fn.enclosingElement;
final widgetName = requireName(
widgetClass,
orFailWith: '@MixableSpec(target:) widget class must have a name.',
);
final styleWidgetSupertype = findSupertypeMatching(
widgetClass.thisType,
styleWidgetChecker,
final widgetName = mixableSpecTargetWidgetName(fn);
validateMixableSpecTargetConstructor(
constructor: fn,
widgetName: widgetName,
specElement: specElement,
specName: specName,
anchor: specElement,
);
if (styleWidgetSupertype == null) {
fail(
specElement,
'Widget $widgetName must extend StyleWidget<$specName> '
'to be used as @MixableSpec(target:).',
);
}

final widgetSpecArg = styleWidgetSupertype.typeArguments.first;
if (widgetSpecArg is! InterfaceType ||
widgetSpecArg.element != specElement) {
fail(
specElement,
'Spec generic mismatch: $specName annotated, but '
'$widgetName extends StyleWidget<${widgetSpecArg.getDisplayString()}>.',
);
}

final optionalPositional = optionalPositionalNames(fn.formalParameters);
if (optionalPositional.isNotEmpty) {
fail(
specElement,
'@MixableSpec(target:) does not support optional positional target '
'constructor parameters on $widgetName: '
'[${optionalPositional.join(', ')}].',
todo: 'Convert these parameters to required positional or named.',
);
}

_requireStyleParameter(fn, widgetName);

final result = extractCallParams(
fn,
Expand All @@ -556,22 +524,6 @@ class SpecStylerClassBuilder {
);
}

void _requireStyleParameter(
ConstructorElement constructor,
String widgetName,
) {
for (final parameter in constructor.formalParameters) {
if (parameter.name == 'style' && parameter.isNamed) return;
}

fail(
specElement,
'@MixableSpec(target:) requires $widgetName to expose a named '
'`style` constructor parameter so the generated call() can pass '
'`style: this`.',
);
}

_CompoundConfig? _compoundConfig(List<FieldModel> fields) {
final surface = compoundStylerSurfaceFor(stylerName);
if (surface == null) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
library;

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';

import '../checkers.dart';
import '../errors.dart';
import '../models/mix_widget_model.dart';
import 'type_hierarchy.dart';
import 'library_scope.dart';

const reservedParamNames = {
Expand All @@ -28,6 +30,66 @@ List<String> optionalPositionalNames(
.toList();
}

String mixableSpecTargetWidgetName(ConstructorElement constructor) {
return requireName(
constructor.enclosingElement,
orFailWith: '@MixableSpec(target:) widget class must have a name.',
);
}

void validateMixableSpecTargetConstructor({
required ConstructorElement constructor,
required String widgetName,
required InterfaceElement specElement,
required String specName,
required Element anchor,
}) {
final styleWidgetSupertype = findSupertypeMatching(
constructor.enclosingElement.thisType,
styleWidgetChecker,
);
if (styleWidgetSupertype == null) {
fail(
anchor,
'Widget $widgetName must extend StyleWidget<$specName> '
'to be used as @MixableSpec(target:).',
);
}

final widgetSpecArg = styleWidgetSupertype.typeArguments.first;
if (widgetSpecArg is! InterfaceType || widgetSpecArg.element != specElement) {
fail(
anchor,
'Spec generic mismatch: $specName annotated, but '
'$widgetName extends StyleWidget<${widgetSpecArg.getDisplayString()}>.',
);
}

final optionalPositional = optionalPositionalNames(
constructor.formalParameters,
);
if (optionalPositional.isNotEmpty) {
fail(
anchor,
'@MixableSpec(target:) does not support optional positional target '
'constructor parameters on $widgetName: '
'[${optionalPositional.join(', ')}].',
todo: 'Convert these parameters to required positional or named.',
);
}

for (final parameter in constructor.formalParameters) {
if (parameter.name == 'style' && parameter.isNamed) return;
}

fail(
anchor,
'@MixableSpec(target:) requires $widgetName to expose a named '
'`style` constructor parameter so the generated call() can pass '
'`style: this`.',
);
}

({List<WidgetCallParam> params, bool forwardsKey}) extractCallParams(
ExecutableElement executable, {
required Element anchor,
Expand Down
61 changes: 7 additions & 54 deletions packages/mix_generator/lib/src/styler_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,7 @@ class StylerGenerator extends GeneratorForAnnotation<MixableStyler> {
}

final widgetClass = fn.enclosingElement;
final widgetName = requireName(
widgetClass,
orFailWith: '@MixableSpec(target:) widget class must have a name.',
);
final widgetName = mixableSpecTargetWidgetName(fn);
final hiddenWidgetType = firstInvisibleTypeName(
widgetClass.thisType,
stylerElement.library,
Expand All @@ -113,40 +110,13 @@ class StylerGenerator extends GeneratorForAnnotation<MixableStyler> {
);
}

final styleWidgetSupertype = findSupertypeMatching(
widgetClass.thisType,
styleWidgetChecker,
validateMixableSpecTargetConstructor(
constructor: fn,
widgetName: widgetName,
specElement: specElement,
specName: specName,
anchor: specElement,
);
if (styleWidgetSupertype == null) {
fail(
specElement,
'Widget $widgetName must extend StyleWidget<$specName> '
'to be used as @MixableSpec(target:).',
);
}

final widgetSpecArg = styleWidgetSupertype.typeArguments.first;
if (widgetSpecArg is! InterfaceType ||
widgetSpecArg.element != specElement) {
fail(
specElement,
'Spec generic mismatch: $specName annotated, but '
'$widgetName extends StyleWidget<${widgetSpecArg.getDisplayString()}>.',
);
}

final optionalPositional = optionalPositionalNames(fn.formalParameters);
if (optionalPositional.isNotEmpty) {
fail(
specElement,
'@MixableSpec(target:) does not support optional positional target '
'constructor parameters on $widgetName: '
'[${optionalPositional.join(', ')}].',
todo: 'Convert these parameters to required positional or named.',
);
}

_requireStyleParameter(fn, widgetName, specElement);

final result = extractCallParams(
fn,
Expand All @@ -166,23 +136,6 @@ class StylerGenerator extends GeneratorForAnnotation<MixableStyler> {
);
}

void _requireStyleParameter(
ConstructorElement constructor,
String widgetName,
Element anchor,
) {
for (final parameter in constructor.formalParameters) {
if (parameter.name == 'style' && parameter.isNamed) return;
}

fail(
anchor,
'@MixableSpec(target:) requires $widgetName to expose a named '
'`style` constructor parameter so the generated call() can pass '
'`style: this`.',
);
}

@override
String generateForAnnotatedElement(
Element element,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

import '../utils/ast_helpers.dart';
import '../utils/type_helpers.dart';

class MixAvoidDefiningTokensWithinScope extends AnalysisRule {
Expand Down Expand Up @@ -46,19 +47,13 @@ class _Visitor extends SimpleAstVisitor<void> {
void visitInstanceCreationExpression(InstanceCreationExpression node) {
if (!isMixTokenType(node.staticType)) return;

// Walk up the AST to find a MixScope ancestor.
// Stop at statement/declaration boundaries.
AstNode? current = node.parent;
while (current != null &&
current is! Statement &&
current is! Declaration) {
if (current is InstanceCreationExpression &&
isMixScopeType(current.staticType)) {
for (final ancestor in ancestorsBeforeStatementOrDeclaration(node)) {
if (ancestor is InstanceCreationExpression &&
isMixScopeType(ancestor.staticType)) {
rule.reportAtNode(node);

return;
}
current = current.parent;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

import '../utils/ast_helpers.dart';
import '../utils/type_helpers.dart';

class MixAvoidDefiningTokensWithinStyle extends AnalysisRule {
Expand Down Expand Up @@ -45,24 +46,19 @@ class _Visitor extends SimpleAstVisitor<void> {
void visitInstanceCreationExpression(InstanceCreationExpression node) {
if (!isMixTokenType(node.staticType)) return;

// Walk up the AST to see if this token is inside a Styler method chain.
// Stop at statement/declaration boundaries.
AstNode? current = node.parent;
while (current != null &&
current is! Statement &&
current is! Declaration) {
if (current is MethodInvocation && isMixStylerType(current.staticType)) {
for (final ancestor in ancestorsBeforeStatementOrDeclaration(node)) {
if (ancestor is MethodInvocation &&
isMixStylerType(ancestor.staticType)) {
rule.reportAtNode(node);

return;
}
if (current is InstanceCreationExpression &&
isMixStylerType(current.staticType)) {
if (ancestor is InstanceCreationExpression &&
isMixStylerType(ancestor.staticType)) {
rule.reportAtNode(node);

return;
}
current = current.parent;
}
}
}
22 changes: 2 additions & 20 deletions packages/mix_lint/lib/src/rules/mix_avoid_empty_variants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

import '../utils/ast_helpers.dart';
import '../utils/type_helpers.dart';

class MixAvoidEmptyVariants extends AnalysisRule {
Expand Down Expand Up @@ -41,30 +42,11 @@ class _Visitor extends SimpleAstVisitor<void> {

const _Visitor(this.rule);

/// Collects the linear chain of [MethodInvocation]s directly following [ice].
/// Stops when the chain branches into non-target contexts (e.g. argument lists).
List<MethodInvocation> _collectChain(InstanceCreationExpression ice) {
final chain = <MethodInvocation>[];
AstNode? current = ice;

while (true) {
final parent = current!.parent;
if (parent is MethodInvocation && parent.target == current) {
chain.add(parent);
current = parent;
} else {
break;
}
}

return chain;
}

@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
if (!isMixStylerType(node.staticType)) return;

final chain = _collectChain(node);
final chain = collectDirectMethodChain(node);
if (chain.isEmpty) return;

final allVariants = chain.every(
Expand Down
Loading
Loading