$npx -y skills add flutter/agent-plugins --skill add-dart-lint-validation-ruleInstructions for adding a new validation rule and CLI flag to dart_skills_lint. Use this skill when asked to create a new rule that validates aspects of skills (like frontmatter metadata).
| 1 | # Add a New Validation Rule and Flag |
| 2 | |
| 3 | Use this skill when you need to add a new validation rule to the `dart_skills_lint` package, expose it as a toggleable CLI flag, and verify its behavior. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## 🛠️ Step-by-Step Implementation |
| 8 | |
| 9 | ### 1. Create the Rule Class |
| 10 | Create a new file in `lib/src/rules/` extending `SkillRule`. |
| 11 | |
| 12 | > [!TIP] |
| 13 | > If your rule expects a specific structure in the skill's YAML frontmatter (e.g., inside `metadata`), document this structure clearly in the class Dart docstring. |
| 14 | |
| 15 | ```dart |
| 16 | // lib/src/rules/my_new_rule.dart |
| 17 | |
| 18 | import '../models/analysis_severity.dart'; |
| 19 | import '../models/skill_context.dart'; |
| 20 | import '../models/skill_rule.dart'; |
| 21 | import '../models/validation_error.dart'; |
| 22 | |
| 23 | class MyNewRule extends SkillRule { |
| 24 | MyNewRule({super.severity}); |
| 25 | |
| 26 | @override |
| 27 | Future<List<ValidationError>> validate(SkillContext context) async { |
| 28 | final errors = <ValidationError>[]; |
| 29 | // Add validation logic here using context.rawContent or context.directory |
| 30 | return errors; |
| 31 | } |
| 32 | } |
| 33 | ``` |
| 34 | |
| 35 | #### Accessing YAML Frontmatter |
| 36 | If your rule needs configuration from the skill's YAML frontmatter, you can access it via `context.parsedYaml`. |
| 37 | |
| 38 | ```dart |
| 39 | @override |
| 40 | Future<List<ValidationError>> validate(SkillContext context) async { |
| 41 | final errors = <ValidationError>[]; |
| 42 | final yaml = context.parsedYaml; |
| 43 | if (yaml != null) { |
| 44 | final metadata = yaml['metadata']; |
| 45 | if (metadata is Map) { |
| 46 | // Read your custom config here |
| 47 | } |
| 48 | } |
| 49 | return errors; |
| 50 | } |
| 51 | ``` |
| 52 | |
| 53 | ### 2. Register the Rule in `lib/src/rule_registry.dart` |
| 54 | |
| 55 | Add a new `CheckType` instance to `RuleRegistry.allChecks` list. This automatically exposes it as a CLI flag. |
| 56 | |
| 57 | ```dart |
| 58 | // lib/src/rule_registry.dart in allChecks list |
| 59 | |
| 60 | const CheckType( |
| 61 | name: MyNewRule.ruleName, |
| 62 | defaultSeverity: MyNewRule.defaultSeverity, |
| 63 | help: 'Description of what the rule does for CLI help.', |
| 64 | ), |
| 65 | ``` |
| 66 | |
| 67 | Then, add a case to `RuleRegistry.createRule` to instantiate your rule: |
| 68 | |
| 69 | ```dart |
| 70 | // lib/src/rule_registry.dart in createRule method |
| 71 | |
| 72 | static SkillRule? createRule(String name, AnalysisSeverity severity) { |
| 73 | switch (name) { |
| 74 | // ... other rules |
| 75 | case MyNewRule.ruleName: |
| 76 | return MyNewRule(severity: severity); |
| 77 | default: |
| 78 | return null; |
| 79 | } |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | ### 3. Handle Disabled by Default Rules (If applicable) |
| 84 | If the rule is disabled by default (`defaultSeverity: AnalysisSeverity.disabled`), passing the flag `--check-my-new-rule` will automatically enable it with `AnalysisSeverity.error` severity (handled in `entry_point.dart`). |
| 85 | |
| 86 | --- |
| 87 | |
| 88 | ## 🧪 Testing the New Rule |
| 89 | |
| 90 | You must write automated tests verifying your rule triggers when it should and skips when it shouldn't. |
| 91 | |
| 92 | ### Preferred Approach: In-Memory Unit Tests |
| 93 | Instead of writing files to disk, test the rule directly using a mock `SkillContext`. This is faster and avoids I/O dependencies. |
| 94 | |
| 95 | ```dart |
| 96 | // test/my_new_rule_test.dart |
| 97 | |
| 98 | import 'dart:io'; |
| 99 | import 'package:dart_skills_lint/src/models/analysis_severity.dart'; |
| 100 | import 'package:dart_skills_lint/src/models/skill_context.dart'; |
| 101 | import 'package:dart_skills_lint/src/models/validation_error.dart'; |
| 102 | import 'package:dart_skills_lint/src/rules/my_new_rule.dart'; |
| 103 | import 'package:test/test.dart'; |
| 104 | |
| 105 | void main() { |
| 106 | group('MyNewRule', () { |
| 107 | test('flags invalid content', () async { |
| 108 | final rule = MyNewRule(severity: AnalysisSeverity.warning); |
| 109 | final context = SkillContext( |
| 110 | directory: Directory('dummy'), |
| 111 | rawContent: 'Invalid content', |
| 112 | ); |
| 113 | |
| 114 | final List<ValidationError> errors = await rule.validate(context); |
| 115 | |
| 116 | expect(errors, isNotEmpty); |
| 117 | expect(errors.first.message, contains('Expected error message')); |
| 118 | }); |
| 119 | |
| 120 | test('passes valid content', () async { |
| 121 | final rule = MyNewRule(severity: AnalysisSeverity.warning); |
| 122 | final context = SkillContext( |
| 123 | directory: Directory('dummy'), |
| 124 | rawContent: 'Valid content', |
| 125 | ); |
| 126 | |
| 127 | final List<ValidationError> errors = await rule.validate(context); |
| 128 | |
| 129 | expect(errors, isEmpty); |
| 130 | }); |
| 131 | }); |
| 132 | } |
| 133 | ``` |
| 134 | |
| 135 | ### Alternative Approach: File System Interaction |
| 136 | If the rule interacts with the file system or wraps an external CLI tool (like `popmark`), you should use a temporary directory for testing instead of in-memory mocks. |
| 137 | |
| 138 | ```dart |
| 139 | late Directory tempDir; |
| 140 | |
| 141 | setUp(() async { |
| 142 | tempDir = await Directory.systemTemp.createTemp('my_rule_test.'); |
| 143 | }); |
| 144 | |
| 145 | tearDown(() async { |
| 146 | if (tempDir.existsSync()) { |
| 147 | await tempDir.delete(recursive: true); |
| 148 | } |
| 149 | }); |
| 150 | |
| 151 | test('flags invalid file content', () async { |
| 152 | final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); |
| 153 | await File('${skillDir.path}/SKILL.md').writeAsString('In |