Skip to content
Merged
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
9 changes: 7 additions & 2 deletions tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,28 @@ class RelativePathsRule extends SkillRule {
match != null ? context.rawContent.substring(match.end) : context.rawContent;

for (final RegExpMatch linkMatch in _markdownLinkRegex.allMatches(markdownContent)) {
final String path = linkMatch.group(1)!;
final String fullPath = linkMatch.group(1)!;
// Markdown links can have a title after the URL, separated by spaces.
// e.g. [text](url "title")
final String path = fullPath.trim().split(RegExp(r'\s+')).first;

// Skip absolute paths (handled by AbsolutePathsRule)
if (isAbsolute(path) || windows.isAbsolute(path)) {
continue;
}

var effectivePath = path;
try {
final Uri uri = Uri.parse(path);
if (uri.hasScheme || path.startsWith('#')) {
continue; // Ignore web URLs, email links, anchors, etc.
}
effectivePath = uri.path;
} catch (_) {
// If Uri parsing fails, treat it as a potential filepath.
}

final linkedFile = File(join(context.directory.path, path));
final linkedFile = File(join(context.directory.path, effectivePath));
if (!linkedFile.existsSync()) {
errors.add(ValidationError(
ruleId: name,
Expand Down
66 changes: 66 additions & 0 deletions tool/dart_skills_lint/test/relative_paths_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,71 @@ void main() {
expect(result.errors, isEmpty);
expect(result.warnings, isEmpty); // None of these should trigger local file checks
});

test('passes with valid relative image path and title', () async {
final Directory skillDir = await Directory('${tempDir.path}/test-skill').create();
await File('${skillDir.path}/SKILL.md').writeAsString(
'${buildFrontmatter(name: 'test-skill')}![Accessible description](images/screenshot.png "Hover description")\n');

final Directory imgDir = await Directory('${skillDir.path}/images').create();
await File('${imgDir.path}/screenshot.png').writeAsString('image content');

final validator =
Validator(ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning});
final ValidationResult result = await validator.validate(skillDir);

expect(result.isValid, isTrue);
expect(result.errors, isEmpty);
expect(result.warnings, isEmpty);
});

test('passes with relative path containing line fragments', () async {
final Directory skillDir =
await Directory('${tempDir.path}/a/b/c/test-skill').create(recursive: true);
await File('${skillDir.path}/SKILL.md').writeAsString(
'${buildFrontmatter(name: 'test-skill')}[Link to lines](../../../CONTRIBUTING.md#L64-L80)\n');

await File('${tempDir.path}/a/CONTRIBUTING.md').create(recursive: true);

final validator =
Validator(ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning});
final ValidationResult result = await validator.validate(skillDir);

expect(result.isValid, isTrue);
expect(result.errors, isEmpty);
expect(result.warnings, isEmpty);
});

test('passes with relative path containing anchor fragments', () async {
final Directory skillDir = await Directory('${tempDir.path}/test-skill').create();
await File('${skillDir.path}/SKILL.md').writeAsString(
'${buildFrontmatter(name: 'test-skill')}[Link to section](styleguide.md#miscellaneous-languages)\n');

await File('${skillDir.path}/styleguide.md').writeAsString('Styleguide content');

final validator =
Validator(ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning});
final ValidationResult result = await validator.validate(skillDir);

expect(result.isValid, isTrue);
expect(result.errors, isEmpty);
expect(result.warnings, isEmpty);
});

test('passes with leading and trailing whitespace in link', () async {
final Directory skillDir = await Directory('${tempDir.path}/test-skill').create();
await File('${skillDir.path}/SKILL.md').writeAsString(
'${buildFrontmatter(name: 'test-skill')}[Link with whitespace]( styleguide.md )\n');

await File('${skillDir.path}/styleguide.md').writeAsString('Styleguide content');

final validator =
Validator(ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning});
final ValidationResult result = await validator.validate(skillDir);

expect(result.isValid, isTrue);
expect(result.errors, isEmpty);
expect(result.warnings, isEmpty);
});
});
}
Loading