diff --git a/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md b/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md index a8a3995..509600a 100644 --- a/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md @@ -11,114 +11,116 @@ Use this skill when you need to add a new validation rule to the `dart_skills_li ## ๐Ÿ› ๏ธ Step-by-Step Implementation -### 1. Define the Rule in `lib/src/rules.dart` - -To create a new rule, add a top-level `CheckType` instance. +### 1. Create the Rule Class +Create a new file in `lib/src/rules/` extending `SkillRule`. ```dart -// lib/src/rules.dart - -/// Template instance for checking if the description has a trailing period. -final descriptionTrailingPeriodCheck = CheckType( - name: 'description-trailing-period', - defaultSeverity: AnalysisSeverity.error, // or warning/disabled -); -``` +// lib/src/rules/my_new_rule.dart -### 2. Expose the CLI Flag in `lib/src/entry_point.dart` +import '../models/analysis_severity.dart'; +import '../models/skill_context.dart'; +import '../models/skill_rule.dart'; +import '../models/validation_error.dart'; -Modify `runApp` to include the toggleable flag for your new rule. - -#### ๐Ÿ“‹ Register Parser Option -Add `.addFlag` in `runApp` (around line 60-90): - -```dart -// lib/src/entry_point.dart +class MyNewRule extends SkillRule { + MyNewRule({super.severity}); -parser - ..addFlag(descriptionTrailingPeriodCheck.name, - negatable: true, - defaultsTo: true, - help: 'Check if the description ends with a period.'); -``` - -#### ๐ŸŽš๏ธ Resolve Flag Logic or Override Severities -Find where severities are evaluated (around line 140-170) and bind your flag state: - -```dart -if (results.wasParsed(descriptionTrailingPeriodCheck.name)) { - descriptionTrailingPeriodCheck.severity = (results[descriptionTrailingPeriodCheck.name] as bool) - ? descriptionTrailingPeriodCheck.defaultSeverity - : AnalysisSeverity.disabled; + @override + Future> validate(SkillContext context) async { + final errors = []; + // Add validation logic here using context.rawContent or context.directory + return errors; + } } ``` -Add your rule to the `checkTypes` set if you want it loaded by default configuration overrides. +### 2. Register the Rule in `lib/src/rule_registry.dart` -### 3. Implement Validation in `lib/src/validator.dart` - -Write the specific logic inside `Validator` checking the schema. +Add a new `CheckType` instance to `RuleRegistry.allChecks` list. This automatically exposes it as a CLI flag. ```dart -// lib/src/validator.dart - -void _validateDescriptionPeriod(String description, List validationErrors) { - if (description.isNotEmpty && !description.endsWith('.')) { - validationErrors.add(ValidationError( - ruleId: _getRule(descriptionTrailingPeriodCheck).name, - file: _skillFileName, - message: 'Description must end with a period.', - severity: _getRule(descriptionTrailingPeriodCheck).severity, - )); - } -} +// lib/src/rule_registry.dart in allChecks list + + const CheckType( + name: MyNewRule.ruleName, + defaultSeverity: MyNewRule.defaultSeverity, + help: 'Description of what the rule does for CLI help.', + ), ``` -Invoke your sub-routine inside `_parseMetadataFields`: +Then, add a case to `RuleRegistry.createRule` to instantiate your rule: ```dart -final description = yaml[_descriptionField]?.toString() ?? ''; -if (description.isNotEmpty) { - _validateDescriptionPeriod(description, validationErrors); -} +// lib/src/rule_registry.dart in createRule method + + static SkillRule? createRule(String name, AnalysisSeverity severity) { + switch (name) { + // ... other rules + case MyNewRule.ruleName: + return MyNewRule(severity: severity); + default: + return null; + } + } ``` +### 3. Handle Disabled by Default Rules (If applicable) +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`). + --- ## ๐Ÿงช Testing the New Rule You must write automated tests verifying your rule triggers when it should and skips when it shouldn't. -### Creating a Test Suite Case -Add matching suite files in `test/` (e.g., `test/field_constraints_test.dart` or `test/metadata_validation_test.dart`). +### Preferred Approach: In-Memory Unit Tests +Instead of writing files to disk, test the rule directly using a mock `SkillContext`. This is faster and avoids I/O dependencies. ```dart -// test/field_constraints_test.dart - -test('triggers error when description does not end with period', () async { - final tempDir = createTempSkillDir( - name: 'test-skill', - description: 'This description does not end with a period', - ); - - final validator = Validator(rules: {descriptionTrailingPeriodCheck}); - final result = await validator.validate(tempDir); - - expect(result.validationErrors.any((e) => e.ruleId == 'description-trailing-period'), isTrue); -}); -test('skips error when description ends with period', () async { - final tempDir = createTempSkillDir( - name: 'test-skill', - description: 'This description ends with a period.', - ); - - final validator = Validator(rules: {descriptionTrailingPeriodCheck}); - final result = await validator.validate(tempDir); - - expect(result.validationErrors.any((e) => e.ruleId == 'description-trailing-period'), isFalse); -}); +// test/my_new_rule_test.dart + +import 'dart:io'; +import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/skill_context.dart'; +import 'package:dart_skills_lint/src/models/validation_error.dart'; +import 'package:dart_skills_lint/src/rules/my_new_rule.dart'; +import 'package:test/test.dart'; + +void main() { + group('MyNewRule', () { + test('flags invalid content', () async { + final rule = MyNewRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: 'Invalid content', + ); + + final List errors = await rule.validate(context); + + expect(errors, isNotEmpty); + expect(errors.first.message, contains('Expected error message')); + }); + + test('passes valid content', () async { + final rule = MyNewRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: 'Valid content', + ); + + final List errors = await rule.validate(context); + + expect(errors, isEmpty); + }); + }); +} ``` +### Integration Tests +If the rule interacts with CLI flags or configuration files, add a test in `test/cli_integration_test.dart` using `TestProcess`. +> [!IMPORTANT] +> When writing integration tests that use config files and `TestProcess`, ensure that paths in the config file and paths passed to the CLI match in style (both relative or both absolute) to avoid issues with path matching in `entry_point.dart`. + --- ## ๐Ÿ“š Documentation Updates @@ -127,20 +129,18 @@ When a new rule is introduced, verify that you synchronize sibling markdown file 1. **`README.md`:** * Add your flag under the **Usage** and **Flags** sections so users know it exists. - * Add descriptive lines under **Specification Validation**. 2. **`documentation/knowledge/SPECIFICATION.md`:** - * If the rule implements standard specifications traits, add constraints parameters under Section 5.1 (Validation parameters). + * Document the formal constraint in the specification if it defines a standard for skill files. --- ## ๐Ÿšฆ Checklist Before Submitting PR -- [ ] Rule defined in `rules.dart`. -- [ ] Flag registered in `entry_point.dart`. -- [ ] Logic implemented in `validator.dart`. -- [ ] Toggle logic tests added in `test/`. +- [ ] Rule class created in `lib/src/rules/`. +- [ ] Rule registered in `lib/src/rule_registry.dart`. +- [ ] Unit tests added in `test/` using in-memory `SkillContext`. - [ ] Usage listed in `README.md`. -- [ ] Schema documented in `documentation/knowledge/SPECIFICATION.md`. -- [ ] Run `dart analyze` to ensure no issues. -- [ ] Run `dart test` to ensure tests passing. +- [ ] Schema documented in `documentation/knowledge/SPECIFICATION.md` (if applicable). - [ ] Run `dart format .` to format code. +- [ ] Run `dart analyze --fatal-infos` to ensure no issues. +- [ ] Run `dart test` to ensure tests passing. diff --git a/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md b/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md index 2bed63e..cfe2c41 100644 --- a/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md @@ -47,6 +47,8 @@ or identifiers that cannot be broken. ## Related Skills -- **[`dart-modern-features`](../dart-modern-features/SKILL.md)**: For idiomatic +- **[dart-modern-features]**: For idiomatic usage of modern Dart features like Pattern Matching (useful for deep JSON extraction), Records, and Switch Expressions. + +[dart-modern-features]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-modern-features/SKILL.md diff --git a/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md b/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md index 6dad82a..2251733 100644 --- a/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md @@ -93,6 +93,15 @@ Future main(List arguments) async { } ``` +### Cross-Platform Compatibility (Windows Support) +When writing CLI applications and tests, ensure compatibility with Windows: +- **Paths**: Never hardcode path separators like `/`. Use `package:path`'s + `p.join` or `p.normalize` to construct paths portably. +- **File Permissions**: When testing file permission errors, remember that + `chmod` is not available on Windows. Use `icacls` on Windows or appropriate + mock libraries. Never skip tests on Windows simply because of permission + commands if a Windows equivalent exists. + ## 3. Recommended Packages Use these community-standard packages owned by the [Dart team](https://dart.dev) diff --git a/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md b/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md index b0a9147..d434338 100644 --- a/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md @@ -16,7 +16,7 @@ Use this skill when: ## Core Matchers -### 1. Collections (`hasLength`, `contains`, `isEmpty`) +### 1. Collections (`hasLength`, `contains`, `isEmpty`, `unorderedEquals`, `containsPair`) - **`hasLength(n)`**: - Prefer `expect(list, hasLength(n))` over `expect(list.length, n)`. @@ -28,9 +28,16 @@ Use this skill when: - **`contains(item)`**: - Verify existence without manual iteration. + - Prefer over `expect(list.contains(item), true)`. - **`unorderedEquals(items)`**: - Verify contents regardless of order. + - Prefer over `expect(list, containsAll(items))`. + +- **`containsPair(key, value)`**: + - Verify a map contains a specific key-value pair. + - Prefer over checking `expect(map[key], value)` or + `expect(map.containsKey(key), true)`. ### 2. Type Checks (`isA` and `TypeMatcher`) @@ -99,8 +106,11 @@ expect(sideEffectState, equals('done')); // Race condition! ## Related Skills -- **[`dart-test-fundamentals`](../dart-test-fundamentals/SKILL.md)**: Core +- **[dart-test-fundamentals]**: Core concepts for structuring tests, lifecycles, and configuration. -- **[`dart-checks-migration`](../dart-checks-migration/SKILL.md)**: Use this +- **[dart-checks-migration]**: Use this skill if you are migrating tests from `package:matcher` to modern `package:checks`. + +[dart-test-fundamentals]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-test-fundamentals/SKILL.md +[dart-checks-migration]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-checks-migration/SKILL.md diff --git a/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md b/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md index 9328219..1a1a68f 100644 --- a/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md @@ -236,6 +236,8 @@ LogLevel currentLevel = .info; ## Related Skills -- **[`dart-best-practices`](../dart-best-practices/SKILL.md)**: General code +- **[dart-best-practices]**: General code style and foundational Dart idioms that predate or complement the modern syntax features. + +[dart-best-practices]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-best-practices/SKILL.md diff --git a/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md b/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md index e4a99b7..51a9381 100644 --- a/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md @@ -117,8 +117,11 @@ timeouts: `dart-test-fundamentals` is the core skill for structuring and configuring tests. For writing assertions within those tests, refer to: -- **[`dart-matcher-best-practices`](../dart-matcher-best-practices/SKILL.md)**: +- **[dart-matcher-best-practices]**: Use this if the project sticks with the traditional `package:matcher` (`expect` calls). -- **[`dart-checks-migration`](../dart-checks-migration/SKILL.md)**: Use this +- **[dart-checks-migration]**: Use this if the project is migrating to the modern `package:checks` (`check` calls). + +[dart-matcher-best-practices]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-matcher-best-practices/SKILL.md +[dart-checks-migration]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-checks-migration/SKILL.md diff --git a/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json b/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json index e1114b9..1ab118f 100644 --- a/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json +++ b/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json @@ -1,3 +1,3 @@ { "skills": {} -} +} \ No newline at end of file diff --git a/tool/dart_skills_lint/.agents/skills/ignore.json b/tool/dart_skills_lint/.agents/skills/ignore.json index bf86e50..557baba 100644 --- a/tool/dart_skills_lint/.agents/skills/ignore.json +++ b/tool/dart_skills_lint/.agents/skills/ignore.json @@ -1,14 +1,14 @@ { "skills": { - "dart-matcher-best-practices": [ + "dart-modern-features": [ { - "rule_id": "check-relative-paths", + "rule_id": "check-trailing-whitespace", "file_name": "SKILL.md" } ], - "dart-test-fundamentals": [ + "dart-matcher-best-practices": [ { - "rule_id": "check-relative-paths", + "rule_id": "check-trailing-whitespace", "file_name": "SKILL.md" } ] diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index d55406d..1d27af8 100644 --- a/tool/dart_skills_lint/README.md +++ b/tool/dart_skills_lint/README.md @@ -73,6 +73,7 @@ If no directory is specified, it automatically checks `.claude/skills` and `.age - `-w`, `--print-warnings`: Enable printing of warning messages. - `--fast-fail`: Halt execution immediately on the error. - `--ignore-config`: Ignore the YAML configuration file entirely. +- `--[no-]check-trailing-whitespace`: Enable/disable checking for trailing whitespace. (Disabled by default). ### 2. As a Command Line Tool with a YAML Configuration File You can configure the linter using a configuration file (defaulting to `dart_skills_lint.yaml` in the current directory). @@ -190,6 +191,9 @@ The linter checks against the criteria defined in `documentation/knowledge/SPECI - **Description (`description`)**: Max 1024 characters. - **Compatibility (`compatibility`)**: Max 500 characters. +### 4. Content Constraints +- **Trailing Whitespace**: Lines in `SKILL.md` should not have trailing whitespace. Exactly 2 spaces at the end of a line are allowed to support Markdown hard line breaks, per the [CommonMark Spec](https://spec.commonmark.org/0.30/#hard-line-breaks). + ## Contributing Contributions are welcome! Please ensure that any PRs pass the linter themselves and align with the `documentation/knowledge/SPECIFICATION.md`. diff --git a/tool/dart_skills_lint/dart_skills_lint.yaml b/tool/dart_skills_lint/dart_skills_lint.yaml index d4f6939..bd85dd8 100644 --- a/tool/dart_skills_lint/dart_skills_lint.yaml +++ b/tool/dart_skills_lint/dart_skills_lint.yaml @@ -4,6 +4,7 @@ dart_skills_lint: check-absolute-paths: error directories: - path: ".agents/skills" + check-trailing-whitespace: error ignore_file: ".agents/skills/ignore.json" - path: "../../skills" ignore_file: ".agents/skills/flutter_skills_ignore.json" diff --git a/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md b/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md index 1221337..189c774 100644 --- a/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md +++ b/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md @@ -61,6 +61,9 @@ Validation ensures that a skill directory and its `SKILL.md` file adhere to the - **Compatibility (`compatibility`)**: - Length: Maximum 500 characters. +#### 4.1.4 Content Constraints +- **Trailing Whitespace**: Lines in `SKILL.md` should not have trailing whitespace. Exactly 2 spaces at the end of a line are allowed to support Markdown hard line breaks, per the [CommonMark Spec](https://spec.commonmark.org/0.30/#hard-line-breaks). + ### 5.2 Scripts & Tools - Scripts in the `scripts/` directory should be self-documenting and provide clear error messages. diff --git a/tool/dart_skills_lint/lib/src/rule_registry.dart b/tool/dart_skills_lint/lib/src/rule_registry.dart index a194e9d..5e874a8 100644 --- a/tool/dart_skills_lint/lib/src/rule_registry.dart +++ b/tool/dart_skills_lint/lib/src/rule_registry.dart @@ -10,6 +10,7 @@ import 'rules/description_length_rule.dart'; import 'rules/disallowed_field_rule.dart'; import 'rules/name_format_rule.dart'; import 'rules/relative_paths_rule.dart'; +import 'rules/trailing_whitespace_rule.dart'; import 'rules/valid_yaml_metadata_rule.dart'; /// Registry of all built-in rules. @@ -42,6 +43,11 @@ class RuleRegistry { defaultSeverity: RelativePathsRule.defaultSeverity, help: 'Check if relative paths exist.', ), + const CheckType( + name: TrailingWhitespaceRule.ruleName, + defaultSeverity: TrailingWhitespaceRule.defaultSeverity, + help: 'Check for trailing whitespace (allows exactly 2 spaces for line breaks).', + ), const CheckType( name: ValidYamlMetadataRule.ruleName, defaultSeverity: ValidYamlMetadataRule.defaultSeverity, @@ -62,6 +68,8 @@ class RuleRegistry { return NameFormatRule(severity: severity); case RelativePathsRule.ruleName: return RelativePathsRule(severity: severity); + case TrailingWhitespaceRule.ruleName: + return TrailingWhitespaceRule(severity: severity); case ValidYamlMetadataRule.ruleName: return ValidYamlMetadataRule(severity: severity); default: diff --git a/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart b/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart new file mode 100644 index 0000000..d5d2883 --- /dev/null +++ b/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart @@ -0,0 +1,64 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../models/analysis_severity.dart'; +import '../models/skill_context.dart'; +import '../models/skill_rule.dart'; +import '../models/validation_error.dart'; + +/// Enforces that lines in SKILL.md do not have trailing whitespace, +/// except for exactly two spaces which indicate a hard line break. +class TrailingWhitespaceRule extends SkillRule { + TrailingWhitespaceRule({this.severity = defaultSeverity}); + + static const String ruleName = 'check-trailing-whitespace'; + static const AnalysisSeverity defaultSeverity = AnalysisSeverity.disabled; + static final RegExp _whitespaceRegExp = RegExp(r'([ \t]+)$'); + + @override + String get name => ruleName; + + @override + final AnalysisSeverity severity; + + @override + Future> validate(SkillContext context) async { + final errors = []; + final List lines = context.rawContent.split('\n'); + + for (var i = 0; i < lines.length; i++) { + final String line = lines[i]; + + // Remove carriage return if present (Windows line endings) + final String trimmedLine = line.endsWith('\r') ? line.substring(0, line.length - 1) : line; + + final RegExpMatch? match = _whitespaceRegExp.firstMatch(trimmedLine); + if (match != null) { + final String whitespace = match.group(1)!; + String? message; + + if (whitespace.contains('\t')) { + message = 'Line ${i + 1} has trailing whitespace containing tabs.'; + } else { + final int spacesCount = whitespace.length; + if (spacesCount == 1 || spacesCount >= 3) { + message = + 'Line ${i + 1} has $spacesCount trailing space(s). Only exactly 2 spaces are allowed for line breaks.'; + } + } + + if (message != null) { + errors.add(ValidationError( + ruleId: name, + severity: severity, + file: 'SKILL.md', + message: message, + )); + } + } + } + + return errors; + } +} diff --git a/tool/dart_skills_lint/skills-lock.json b/tool/dart_skills_lint/skills-lock.json index f380f6d..9dd6649 100644 --- a/tool/dart_skills_lint/skills-lock.json +++ b/tool/dart_skills_lint/skills-lock.json @@ -4,12 +4,12 @@ "dart-best-practices": { "source": "kevmoo/dash_skills", "sourceType": "github", - "computedHash": "c1105a6b1d0a41c9e70eeeb6b1e857b81344ad224a339198dae0749e3b07988c" + "computedHash": "1eb4a38aa94060d3d420f1b03b661ad784a1cd732ffb7ff4d8369d085024d4f5" }, "dart-cli-app-best-practices": { "source": "kevmoo/dash_skills", "sourceType": "github", - "computedHash": "94fc01ceca36f67a468222041651b536ee87762df05452ce630484969bb63006" + "computedHash": "6bd4fd96c0ac4dc59699332948ff9a8a724e9d3093cf13d28b542c7e70c92809" }, "dart-doc-validation": { "source": "kevmoo/dash_skills", @@ -19,12 +19,12 @@ "dart-matcher-best-practices": { "source": "kevmoo/dash_skills", "sourceType": "github", - "computedHash": "9a105e76e414500def935665023347e22778d19ef4ad3c543a205b12eb5b0cee" + "computedHash": "11b0b422a571cab62fb7f945ba548947200cd22a5a8f7b5627af636d002c4ad9" }, "dart-modern-features": { "source": "kevmoo/dash_skills", "sourceType": "github", - "computedHash": "9b56ee1527bd3c233c74016872bd48bfaec0ec1c9e83f1f2ac590e110cdc3539" + "computedHash": "cb4cda90e1451aaa5a9a7d4d81358dc563ec07499ce1c18d282882d9c1bdae4e" }, "dart-package-maintenance": { "source": "kevmoo/dash_skills", diff --git a/tool/dart_skills_lint/test/cli_integration_test.dart b/tool/dart_skills_lint/test/cli_integration_test.dart index 4b1147c..8ee089d 100644 --- a/tool/dart_skills_lint/test/cli_integration_test.dart +++ b/tool/dart_skills_lint/test/cli_integration_test.dart @@ -462,5 +462,31 @@ dart_skills_lint: expect(stdoutStr, contains('--- Validating skill: valid-skill ---')); expect(stdoutStr, contains('--- Validating skill: contributing ---')); }); + + test('CLI reports trailing whitespace as error when enabled via config', () async { + final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); + await File('${skillDir.path}/SKILL.md') + .writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); + + final configFile = File('${tempDir.path}/dart_skills_lint.yaml'); + await configFile.writeAsString(''' +dart_skills_lint: + directories: + - path: "test-skill" + rules: + check-trailing-whitespace: error +'''); + + final TestProcess process = await TestProcess.start( + 'dart', + [p.normalize(p.absolute('bin/cli.dart')), '-s', 'test-skill'], + workingDirectory: tempDir.path, + ); + + final List stderr = await process.stderr.rest.toList(); + final String stderrStr = stderr.join('\n'); + expect(stderrStr, contains('has 1 trailing space(s)')); + await process.shouldExit(1); + }); }); } diff --git a/tool/dart_skills_lint/test/trailing_whitespace_test.dart b/tool/dart_skills_lint/test/trailing_whitespace_test.dart new file mode 100644 index 0000000..41ece41 --- /dev/null +++ b/tool/dart_skills_lint/test/trailing_whitespace_test.dart @@ -0,0 +1,127 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/skill_context.dart'; +import 'package:dart_skills_lint/src/models/validation_error.dart'; +import 'package:dart_skills_lint/src/rules/trailing_whitespace_rule.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + group('Trailing Whitespace Validation', () { + test('passes for line with no trailing whitespace', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line without trailing whitespace\n', + ); + + final List errors = await rule.validate(context); + + expect(errors, isEmpty); + }); + + test('passes for line with exactly 2 trailing spaces (hard line break)', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 2 spaces \nNext line\n', + ); + + final List errors = await rule.validate(context); + + expect(errors, isEmpty); + }); + + test('flags line with 1 trailing space as warning', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 1 space \n', + ); + + final List errors = await rule.validate(context); + + expect(errors.any((e) => e.message.contains('has 1 trailing space(s)')), isTrue); + }); + + test('flags line with 3 trailing spaces as warning', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 3 spaces \n', + ); + + final List errors = await rule.validate(context); + + expect(errors.any((e) => e.message.contains('has 3 trailing space(s)')), isTrue); + }); + + test('flags line with trailing tabs as warning', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line with tab\t\n', + ); + + final List errors = await rule.validate(context); + + expect(errors.any((e) => e.message.contains('trailing whitespace containing tabs')), isTrue); + }); + + test('respects severity setting', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.error); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 1 space \n', + ); + + final List errors = await rule.validate(context); + + expect(errors.length, 1); + expect(errors.first.severity, AnalysisSeverity.error); + }); + + test(r'flags line with 1 trailing space before Windows line ending (\r\n) as warning', + () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 1 space \r\n', + ); + + final List errors = await rule.validate(context); + + expect(errors.any((e) => e.message.contains('has 1 trailing space(s)')), isTrue); + }); + + test('flags line containing only whitespace (3 spaces) as warning', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')} \n', + ); + + final List errors = await rule.validate(context); + + expect(errors.any((e) => e.message.contains('has 3 trailing space(s)')), isTrue); + }); + + test('passes for line containing only 2 spaces', () async { + final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); + final context = SkillContext( + directory: Directory('dummy'), + rawContent: '${buildFrontmatter(name: 'test-skill')} \n', + ); + + final List errors = await rule.validate(context); + + expect(errors, isEmpty); + }); + }); +}