| 87 | * Returns LintIssue[] with no `fix`. |
| 88 | */ |
| 89 | export function checkDuplicateBlocks(resource: Resource): LintIssue[] { |
| 90 | const issues: LintIssue[] = []; |
| 91 | const blocksByID = new Map<string, Block[]>(); |
| 92 | |
| 93 | for (const block of resource.blocks) { |
| 94 | if (!blocksByID.has(block.id)) { |
| 95 | blocksByID.set(block.id, []); |
| 96 | } |
| 97 | blocksByID.get(block.id)!.push(block); |
| 98 | } |
| 99 | |
| 100 | for (const [id, blocks] of blocksByID) { |
| 101 | if (blocks.length < 2) { |
| 102 | continue; |
| 103 | } |
| 104 | // Only flag duplicates (2nd occurrence onwards); the first is fine. |
| 105 | for (const block of blocks.slice(1)) { |
| 106 | issues.push({ |
| 107 | code: DUPLICATE_BLOCK_ID_CODE, |
| 108 | message: `Duplicate block ID "^${id}" - ignored`, |
| 109 | range: block.markerRange, |
| 110 | relatedInfo: blocks |
| 111 | .filter(b => b !== block) |
| 112 | .map(b => ({ |
| 113 | uri: resource.uri, |
| 114 | range: b.markerRange, |
| 115 | message: `Other occurrence of "^${id}"`, |
| 116 | })), |
| 117 | }); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | return issues; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Returns the range covering `#fragment` within a wikilink's raw text. |