replace asserts for semantic labels with lints (#3483)

malik1004x committed Aug 4, 2026 at 14:09 UTC 0b93bafe0b20e2443d383e286815da2338ac70c9
7 files changed +222 -12
cw_custom_lints/lib/main.dart
+4
@@ -1,6 +1,8 @@
1 import "package:analysis_server_plugin/plugin.dart";
2 import "package:analysis_server_plugin/registry.dart";
3 import "package:cw_custom_lints/http_force_proxy/http_force_proxy_rule.dart";
4 +import "package:cw_custom_lints/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart";
5 +import "package:cw_custom_lints/modern_button_semantics/modern_button_semantics_rule.dart";
6 import "package:cw_custom_lints/print_verbose/print_verbose_fix.dart";
7 import "package:cw_custom_lints/print_verbose/print_verbose_rule.dart";
8 import "package:cw_custom_lints/restricted_imports/restricted_imports_rule.dart";
@@ -16,6 +18,8 @@ class CwCustomLintsPlugin extends Plugin {
18 registry.registerWarningRule(PrintVerboseRule());
19 registry.registerWarningRule(RestrictedImportsRule());
20 registry.registerWarningRule(HttpForceProxyRule());
21 + registry.registerWarningRule(ModernButtonSemanticsRule());
22 + registry.registerWarningRule(ModalTopBarSemanticsRule());
23
24 registry.registerFixForRule(PrintVerboseRule.code, ReplaceWithPrintV.new);
25 }
cw_custom_lints/lib/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart new
+69
@@ -0,0 +1,69 @@
1 +import "package:analyzer/analysis_rule/analysis_rule.dart";
2 +import "package:analyzer/analysis_rule/rule_context.dart";
3 +import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
4 +import "package:analyzer/dart/ast/ast.dart";
5 +import "package:analyzer/dart/ast/visitor.dart";
6 +import "package:analyzer/error/error.dart";
7 +import "package:cw_custom_lints/utils/widget_arguments.dart";
8 +
9 +class ModalTopBarSemanticsRule extends AnalysisRule {
10 + ModalTopBarSemanticsRule()
11 + : super(
12 + name: "require_modal_top_bar_semantics",
13 + description:
14 + "ModalTopBar icons need a semantic label, because the icon alone does not say whether it closes the modal, goes back or does something else.",
15 + );
16 +
17 + static const LintCode code = LintCode(
18 + "require_modal_top_bar_semantics",
19 + "ModalTopBar was given a {0} without a {1}, so screen readers have nothing to announce.",
20 + correctionMessage: "Pass a localized {1} next to the {0}.",
21 + severity: DiagnosticSeverity.WARNING,
22 + );
23 +
24 + static const _iconSemanticLabels = {
25 + "leadingIcon": "leadingSemanticLabel",
26 + "trailingIcon": "trailingSemanticLabel",
27 + };
28 +
29 + @override
30 + LintCode get diagnosticCode => code;
31 +
32 + @override
33 + void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) =>
34 + registry.addInstanceCreationExpression(this, _Visitor(this));
35 +}
36 +
37 +class _Visitor extends SimpleAstVisitor<void> {
38 + _Visitor(this.rule);
39 +
40 + final AnalysisRule rule;
41 +
42 + @override
43 + void visitInstanceCreationExpression(InstanceCreationExpression node) {
44 + if (!isCakeWalletWidget(node.constructorName, "ModalTopBar")) {
45 + return;
46 + }
47 +
48 + for (final slot in ModalTopBarSemanticsRule._iconSemanticLabels.entries) {
49 + _checkSlot(node.argumentList, iconName: slot.key, semanticLabelName: slot.value);
50 + }
51 + }
52 +
53 + void _checkSlot(
54 + ArgumentList argumentList, {
55 + required String iconName,
56 + required String semanticLabelName,
57 + }) {
58 + final icon = namedArgument(argumentList, iconName);
59 + if (icon == null || icon.argumentExpression is NullLiteral) {
60 + return;
61 + }
62 +
63 + if (!isMissingOrEmptyText(namedArgumentExpression(argumentList, semanticLabelName))) {
64 + return;
65 + }
66 +
67 + rule.reportAtToken(icon.name, arguments: [iconName, semanticLabelName]);
68 + }
69 +}
cw_custom_lints/lib/modern_button_semantics/modern_button_semantics_rule.dart new
+56
@@ -0,0 +1,56 @@
1 +import "package:analyzer/analysis_rule/analysis_rule.dart";
2 +import "package:analyzer/analysis_rule/rule_context.dart";
3 +import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
4 +import "package:analyzer/dart/ast/ast.dart";
5 +import "package:analyzer/dart/ast/visitor.dart";
6 +import "package:analyzer/error/error.dart";
7 +import "package:cw_custom_lints/utils/widget_arguments.dart";
8 +
9 +class ModernButtonSemanticsRule extends AnalysisRule {
10 + ModernButtonSemanticsRule()
11 + : super(
12 + name: "require_modern_button_semantics",
13 + description:
14 + "ModernButton needs a semanticLabel when it has no visible label, otherwise screen readers announce an unnamed button.",
15 + );
16 +
17 + static const LintCode code = LintCode(
18 + "require_modern_button_semantics",
19 + "ModernButton has no visible label, so screen readers have nothing to announce.",
20 + correctionMessage: "Pass a localized semanticLabel, or a non-empty label.",
21 + severity: DiagnosticSeverity.WARNING,
22 + );
23 +
24 + @override
25 + LintCode get diagnosticCode => code;
26 +
27 + @override
28 + void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) =>
29 + registry.addInstanceCreationExpression(this, _Visitor(this));
30 +}
31 +
32 +class _Visitor extends SimpleAstVisitor<void> {
33 + _Visitor(this.rule);
34 +
35 + final AnalysisRule rule;
36 +
37 + @override
38 + void visitInstanceCreationExpression(InstanceCreationExpression node) {
39 + final constructorName = node.constructorName;
40 + if (!isCakeWalletWidget(constructorName, "ModernButton")) {
41 + return;
42 + }
43 +
44 + final argumentList = node.argumentList;
45 + final hasLabel = !isMissingOrEmptyText(namedArgumentExpression(argumentList, "label"));
46 + final hasSemanticLabel = !isMissingOrEmptyText(
47 + namedArgumentExpression(argumentList, "semanticLabel"),
48 + );
49 +
50 + if (hasLabel || hasSemanticLabel) {
51 + return;
52 + }
53 +
54 + rule.reportAtNode(constructorName);
55 + }
56 +}
cw_custom_lints/lib/no_bare_catch/no_bare_catch_rule.dart new
+51
@@ -0,0 +1,51 @@
1 +import "package:analyzer/analysis_rule/analysis_rule.dart";
2 +import "package:analyzer/analysis_rule/rule_context.dart";
3 +import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
4 +import "package:analyzer/dart/ast/ast.dart";
5 +import "package:analyzer/dart/ast/visitor.dart";
6 +import "package:analyzer/error/error.dart";
7 +
8 +class NoBareCatchRule extends AnalysisRule {
9 + NoBareCatchRule()
10 + : super(
11 + name: "no_bare_catch",
12 + description: "please specify the exception class to catch (on SomeException catch (e))",
13 + );
14 +
15 + static const LintCode code = LintCode(
16 + "no_bare_catch",
17 + "please specify the exception class to catch (on SomeException catch (e))",
18 + severity: DiagnosticSeverity.WARNING,
19 + );
20 +
21 + @override
22 + LintCode get diagnosticCode => code;
23 +
24 + @override
25 + void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
26 + registry.addCatchClause(this, _Visitor(this));
27 + }
28 +}
29 +
30 +class _Visitor extends SimpleAstVisitor<void> {
31 + _Visitor(this.rule);
32 +
33 + final AnalysisRule rule;
34 +
35 + @override
36 + void visitCatchClause(CatchClause node) {
37 + if (node.exceptionType != null) {
38 + return;
39 + }
40 +
41 + final tryStatement = node.parent;
42 + if (tryStatement is TryStatement) {
43 + final precedingClauses = tryStatement.catchClauses.takeWhile((clause) => clause != node);
44 + if (precedingClauses.any((clause) => clause.exceptionType != null)) {
45 + return;
46 + }
47 + }
48 +
49 + rule.reportAtNode(node);
50 + }
51 +}
cw_custom_lints/lib/utils/widget_arguments.dart new
+39
@@ -0,0 +1,39 @@
1 +import "package:analyzer/dart/ast/ast.dart";
2 +
3 +bool isCakeWalletWidget(ConstructorName constructorName, String className) {
4 + final element = constructorName.type.element;
5 + if (element == null || element.name != className) {
6 + return false;
7 + }
8 +
9 + final libraryUri = element.library?.uri;
10 + if (libraryUri == null || libraryUri.scheme != "package") {
11 + return false;
12 + }
13 +
14 + final segments = libraryUri.pathSegments;
15 + return segments.isNotEmpty && segments.first == "cake_wallet";
16 +}
17 +
18 +NamedArgument? namedArgument(ArgumentList argumentList, String name) {
19 + for (final argument in argumentList.arguments) {
20 + if (argument is NamedArgument && argument.name.lexeme == name) {
21 + return argument;
22 + }
23 + }
24 +
25 + return null;
26 +}
27 +
28 +Expression? namedArgumentExpression(ArgumentList argumentList, String name) =>
29 + namedArgument(argumentList, name)?.argumentExpression;
30 +
31 +// passed null or ""
32 +// does NOT work if a runtime expression evaluates to an empty string or null
33 +bool isMissingOrEmptyText(Expression? expression) {
34 + if (expression == null || expression is NullLiteral) {
35 + return true;
36 + }
37 +
38 + return expression is StringLiteral && (expression.stringValue?.isEmpty ?? false);
39 +}
lib/new-ui/widgets/modern_button.dart
+2 -6
@@ -28,9 +28,7 @@ class ModernButton extends StatelessWidget {
28 this.backgroundColor,
29 this.label,
30 this.semanticLabel})
31 - : svgPath = null,
32 - assert(semanticLabel != null || (label != null && label != ""),
33 - "ModernButton needs a semanticLabel when it has no visible label");
31 + : svgPath = null;
32
33 const ModernButton.svg(
34 {super.key,
@@ -42,9 +40,7 @@ class ModernButton extends StatelessWidget {
40 this.backgroundColor,
41 this.label,
42 this.semanticLabel})
45 - : icon = null,
46 - assert(semanticLabel != null || (label != null && label != ""),
47 - "ModernButton needs a semanticLabel when it has no visible label");
43 + : icon = null;
44
45 @override
46 Widget build(BuildContext context) {
lib/new-ui/widgets/receive_page/receive_top_bar.dart
+1 -6
@@ -16,12 +16,7 @@ class ModalTopBar extends StatelessWidget {
16 this.leadingWidget,
17 this.trailingWidget,
18 this.leadingSemanticLabel,
19 - this.trailingSemanticLabel})
20 - : assert(leadingIcon == null || (leadingSemanticLabel != null && leadingSemanticLabel != ""),
21 - "leadingIcon requires a non-empty leadingSemanticLabel"),
22 - assert(
23 - trailingIcon == null || (trailingSemanticLabel != null && trailingSemanticLabel != ""),
24 - "trailingIcon requires a non-empty trailingSemanticLabel") {
19 + this.trailingSemanticLabel}) {
20 if (leadingIcon != null && leadingWidget != null) {
21 throw Exception("Cannot have both leadingIcon and leadingWidget");
22 }