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
}