Skip to content

Commit 6adbb1e

Browse files
wickedevclaude
andcommitted
fix: correct line numbers in warning messages when scene directives present (#5)
When parsing wireframes with scene directives (e.g., @scene: login), directive lines are stripped before grid processing. This caused warning line numbers to be reported relative to the grid instead of the original file. Changes: - Add parseSceneDirectivesWithOffset to track stripped directive lines - Add adjustLineOffset helper to ErrorTypes for position adjustment - Apply line offset to all warnings/errors in parseSingleScene - Add integration tests for line number correction The warning "Misaligned closing border" now correctly reports Line 5 instead of Line 3 for the example in Issue #5. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a0c2d9c commit 6adbb1e

File tree

5 files changed

+333
-9
lines changed

5 files changed

+333
-9
lines changed

.claude/commands/fix-issue.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
allowed-tools: [Read, Write, Edit, Bash, Glob, Grep, TodoWrite, Task]
3+
description: "Fix a GitHub issue: analyze, implement, test, commit, and close"
4+
---
5+
6+
# /fix-issue - GitHub Issue Fix Workflow
7+
8+
## Purpose
9+
Automates the complete workflow for fixing a GitHub issue: analyze the problem, implement the fix, write tests, commit changes, and close the issue.
10+
11+
## Usage
12+
```
13+
/fix-issue <issue-number> [release-version]
14+
```
15+
16+
## Arguments
17+
- `issue-number` - GitHub issue number to fix (required)
18+
- `release-version` - Version number for changelog/commit message (optional)
19+
20+
**Input**: $ARGUMENTS
21+
22+
## Execution
23+
24+
### Step 1: Parse Arguments
25+
Extract the issue number and release version from `$ARGUMENTS`.
26+
- First word: Issue number (required)
27+
- Second word: Release version (optional)
28+
29+
### Step 2: Fetch Issue Details
30+
Run `gh issue view <issue-number>` to read:
31+
- Title and description
32+
- Labels
33+
- Expected vs actual behavior
34+
- Steps to reproduce
35+
36+
### Step 3: Analyze the Problem
37+
Based on the issue description:
38+
1. Identify affected files and components using Glob/Grep
39+
2. Read relevant source files to understand the root cause
40+
3. Plan the fix approach
41+
4. Identify what tests need to be written
42+
43+
### Step 4: Create Task List
44+
Use TodoWrite to track:
45+
- [ ] Implement the fix
46+
- [ ] Write test cases
47+
- [ ] Run tests to verify
48+
- [ ] Commit changes
49+
- [ ] Comment on issue
50+
- [ ] Close issue
51+
52+
### Step 5: Implement the Fix
53+
1. Make necessary code changes to fix the issue
54+
2. Follow existing code patterns and conventions
55+
3. Keep changes minimal and focused
56+
57+
### Step 6: Write Tests
58+
1. Add test cases that verify the fix
59+
2. Include edge cases mentioned in the issue
60+
3. Run all tests to verify nothing is broken
61+
62+
### Step 7: Commit Changes
63+
Create a commit with format:
64+
```
65+
fix: <concise description> (#<issue-number>)
66+
67+
<detailed explanation if needed>
68+
```
69+
70+
### Step 8: Comment and Close Issue
71+
1. Get commit hash: `git rev-parse --short HEAD`
72+
2. Comment on issue with changes summary:
73+
```bash
74+
gh issue comment <issue-number> --body "Fixed in commit <hash>
75+
76+
Changes made:
77+
- <list of changes>
78+
79+
Tests added:
80+
- <list of tests>"
81+
```
82+
3. Close the issue: `gh issue close <issue-number>`
83+
84+
## Claude Code Integration
85+
- Uses Read/Write/Edit for code modifications
86+
- Leverages Glob and Grep for codebase exploration
87+
- Applies TodoWrite for progress tracking
88+
- Uses Bash for git and gh CLI operations
89+
90+
## Examples
91+
```
92+
/fix-issue 5
93+
/fix-issue 5 v0.1.3
94+
/fix-issue 12 v0.2.0
95+
```
96+
97+
## Notes
98+
- Always run full test suite before committing
99+
- If tests fail, fix issues before proceeding
100+
- If the issue is unclear, ask for clarification first

src/parser/Errors/ErrorTypes.res

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,61 @@ let getCodeName = (code: errorCode): string => {
174174
| MisalignedClosingBorder(_) => "MisalignedClosingBorder"
175175
}
176176
}
177+
178+
/**
179+
* Adjust the line offset in an error code's position.
180+
* This is used to correct line numbers when scene directive lines
181+
* are stripped before grid processing.
182+
*
183+
* @param code - The error code to adjust
184+
* @param offset - The number of lines to add to the row (e.g., number of directive lines stripped)
185+
* @returns A new error code with adjusted position
186+
*/
187+
let adjustCodeLineOffset = (code: errorCode, offset: int): errorCode => {
188+
let adjustPos = (pos: Position.t): Position.t => {
189+
Position.make(pos.row + offset, pos.col)
190+
}
191+
192+
switch code {
193+
| InvalidInput(_) as c => c
194+
| InvalidStartPosition(pos) => InvalidStartPosition(adjustPos(pos))
195+
| UncloseBox({corner, direction}) => UncloseBox({corner: adjustPos(corner), direction})
196+
| MismatchedWidth({topLeft, topWidth, bottomWidth}) =>
197+
MismatchedWidth({topLeft: adjustPos(topLeft), topWidth, bottomWidth})
198+
| MisalignedPipe({position, expectedCol, actualCol}) =>
199+
MisalignedPipe({position: adjustPos(position), expectedCol, actualCol})
200+
| OverlappingBoxes({box1Name, box2Name, position}) =>
201+
OverlappingBoxes({box1Name, box2Name, position: adjustPos(position)})
202+
| InvalidElement({content, position}) =>
203+
InvalidElement({content, position: adjustPos(position)})
204+
| UnclosedBracket({opening}) => UnclosedBracket({opening: adjustPos(opening)})
205+
| EmptyButton({position}) => EmptyButton({position: adjustPos(position)})
206+
| InvalidInteractionDSL({message, position}) =>
207+
InvalidInteractionDSL({message, position: position->Option.map(adjustPos)})
208+
| UnusualSpacing({position, issue}) =>
209+
UnusualSpacing({position: adjustPos(position), issue})
210+
| DeepNesting({depth, position}) => DeepNesting({depth, position: adjustPos(position)})
211+
| MisalignedClosingBorder({position, expectedCol, actualCol}) =>
212+
MisalignedClosingBorder({position: adjustPos(position), expectedCol, actualCol})
213+
}
214+
}
215+
216+
/**
217+
* Adjust the line offset in an error's position.
218+
* Creates a new error with the position adjusted by the given offset.
219+
*
220+
* @param error - The error to adjust
221+
* @param offset - The number of lines to add to the row
222+
* @returns A new error with adjusted position
223+
*/
224+
let adjustLineOffset = (error: t, offset: int): t => {
225+
if offset === 0 {
226+
error
227+
} else {
228+
{
229+
code: adjustCodeLineOffset(error.code, offset),
230+
severity: error.severity,
231+
context: error.context,
232+
}
233+
}
234+
}

src/parser/Parser.res

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,26 @@ let mergeInteractionsIntoAST = (
191191
*
192192
* @param sceneContent ASCII wireframe content for one scene (without directives)
193193
* @param sceneMetadata Scene metadata from directives
194+
* @param lineOffset Number of directive lines stripped (for adjusting error line numbers)
194195
* @param errors Accumulator for errors
195196
* @returns Parsed scene or None if parsing failed
196197
*/
197198
let parseSingleScene = (
198199
sceneContent: string,
199200
sceneMetadata: SemanticParser.sceneMetadata,
201+
lineOffset: int,
200202
errors: array<ErrorTypes.t>,
201203
): option<Types.scene> => {
202204
// Stage 1: Grid Scanner
203205
let gridResult = scanGrid(sceneContent)
204206

205207
switch gridResult {
206208
| Error(gridErrors) => {
207-
gridErrors->Array.forEach(err => errors->Array.push(err)->ignore)
209+
// Adjust line numbers by offset before adding to errors
210+
gridErrors->Array.forEach(err => {
211+
let adjusted = ErrorTypes.adjustLineOffset(err, lineOffset)
212+
errors->Array.push(adjusted)->ignore
213+
})
208214
None
209215
}
210216
| Ok(grid) => {
@@ -213,12 +219,20 @@ let parseSingleScene = (
213219

214220
let shapes = switch shapesResult {
215221
| Error(shapeErrors) => {
216-
shapeErrors->Array.forEach(err => errors->Array.push(err)->ignore)
222+
// Adjust line numbers by offset before adding to errors
223+
shapeErrors->Array.forEach(err => {
224+
let adjusted = ErrorTypes.adjustLineOffset(err, lineOffset)
225+
errors->Array.push(adjusted)->ignore
226+
})
217227
[]
218228
}
219229
| Ok((boxes, warnings)) => {
220230
// Collect warnings (non-fatal issues like misaligned borders)
221-
warnings->Array.forEach(w => errors->Array.push(w)->ignore)
231+
// Adjust line numbers by offset before adding to errors
232+
warnings->Array.forEach(w => {
233+
let adjusted = ErrorTypes.adjustLineOffset(w, lineOffset)
234+
errors->Array.push(adjusted)->ignore
235+
})
222236
boxes
223237
}
224238
}
@@ -294,15 +308,16 @@ let parseInternal = (wireframe: string, interactions: option<string>): parseResu
294308
let scenes = []
295309

296310
sceneBlocks->Array.forEach(block => {
297-
// Parse scene directives
311+
// Parse scene directives and get line offset
298312
let lines = block->String.split("\n")
299-
let (metadata, contentLines) = SemanticParser.parseSceneDirectives(lines)
313+
let {metadata, contentLines, lineOffset} = SemanticParser.parseSceneDirectivesWithOffset(lines)
300314

301315
// Rejoin content lines (without directives)
302316
let sceneContent = contentLines->Array.join("\n")
303317

304318
// Parse this scene through 3-stage pipeline
305-
switch parseSingleScene(sceneContent, metadata, allIssues) {
319+
// Pass lineOffset to adjust error/warning line numbers
320+
switch parseSingleScene(sceneContent, metadata, lineOffset, allIssues) {
306321
| Some(scene) => scenes->Array.push(scene)->ignore
307322
| None => () // Scene parsing failed, errors already collected
308323
}

src/parser/Semantic/SemanticParser.res

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,43 @@ let defaultSceneMetadata = (): sceneMetadata => {
261261
* ```
262262
* Returns: ({id: "login", title: "Login Page", transition: "slide"}, ["+--Login--+", ...])
263263
*/
264-
let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>) => {
264+
/**
265+
* Result type for parseSceneDirectives that includes line offset information.
266+
* The lineOffset indicates how many lines were removed from the beginning
267+
* before the first content line, which is needed to correctly report
268+
* line numbers in warnings and errors.
269+
*/
270+
type directiveParseResult = {
271+
metadata: sceneMetadata,
272+
contentLines: array<string>,
273+
lineOffset: int, // Number of directive lines stripped from the beginning
274+
}
275+
276+
/**
277+
* Parse scene directives and track line offset.
278+
* Returns metadata, content lines, and the number of lines stripped from the beginning.
279+
*
280+
* The lineOffset is calculated as the index of the first content line in the original
281+
* input. This offset is needed to convert grid row numbers back to original file line numbers.
282+
*
283+
* Example:
284+
* - Input: ["@scene: login", "", "+---+", ...]
285+
* - Output: contentLines = ["", "+---+", ...], lineOffset = 1
286+
*
287+
* Grid row 0 corresponds to original line (0 + lineOffset + 1) = line 2 (1-indexed)
288+
*/
289+
let parseSceneDirectivesWithOffset = (lines: array<string>): directiveParseResult => {
265290
// Use mutable refs to accumulate directive values
266291
let sceneId = ref(None)
267292
let title = ref(None)
268293
let transition = ref(None)
269294
let device = ref(None)
270295
let contentLines = []
271296

272-
lines->Array.forEach(line => {
297+
// Track the index of the first content line (for line offset calculation)
298+
let firstContentLineIndex = ref(None)
299+
300+
lines->Array.forEachWithIndex((line, lineIndex) => {
273301
let trimmed = line->String.trim
274302

275303
if trimmed->String.startsWith("@scene:") {
@@ -301,6 +329,10 @@ let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>
301329
()
302330
} else {
303331
// Non-directive line - add to content
332+
// Track the index of the first content line
333+
if firstContentLineIndex.contents === None {
334+
firstContentLineIndex := Some(lineIndex)
335+
}
304336
contentLines->Array.push(line)
305337
}
306338
})
@@ -334,7 +366,27 @@ let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>
334366
device: finalDevice,
335367
}
336368

337-
(metadata, contentLines)
369+
// Calculate line offset: index of first content line
370+
// If no content lines, offset is 0
371+
let lineOffset = switch firstContentLineIndex.contents {
372+
| Some(idx) => idx
373+
| None => 0
374+
}
375+
376+
{
377+
metadata,
378+
contentLines,
379+
lineOffset,
380+
}
381+
}
382+
383+
/**
384+
* Parse scene directives from an array of lines.
385+
* This is the original API that returns just (metadata, contentLines) for backward compatibility.
386+
*/
387+
let parseSceneDirectives = (lines: array<string>): (sceneMetadata, array<string>) => {
388+
let result = parseSceneDirectivesWithOffset(lines)
389+
(result.metadata, result.contentLines)
338390
}
339391

340392
/**

0 commit comments

Comments
 (0)