Skip to content

Commit b245ca0

Browse files
committed
fix: diagnostic panics and lambda file name
Bounds-check span data before slicing source strings in diagnostic creation. Invalid spans (from stale AST or converter edge cases) would panic on out-of-range access, crashing the LSP server during editing. Thread the script file name through FindCmdBlocks into the lambda converter. The eager conversion in step 7 lost the file parameter, causing error messages from command lambda callbacks to show <stdin> instead of the script name.
1 parent b820903 commit b245ca0

File tree

6 files changed

+37
-17
lines changed

6 files changed

+37
-17
lines changed

core/script_meta.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func ExtractMetadata(src string) *ScriptData {
5454
}
5555

5656
var commands []*ScriptCommand
57-
if cmdBlocks, ok := tree.FindCmdBlocks(); ok {
57+
if cmdBlocks, ok := tree.FindCmdBlocks(ScriptName); ok {
5858
RP.RadDebugf(fmt.Sprintf("Found %d command blocks", len(cmdBlocks)))
5959
commands = extractCommands(cmdBlocks)
6060
}

rts/check/ast_checks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ func (c *RadCheckerImpl) validateAssignmentTargetAST(node rl.Node, d *[]Diagnost
427427
// Valid assignment targets
428428
return
429429
case *rl.LitInt, *rl.LitFloat, *rl.LitString, *rl.LitBool, *rl.LitNull:
430-
content := c.src[node.Span().StartByte:node.Span().EndByte]
430+
content := safeSlice(c.src, node.Span().StartByte, node.Span().EndByte)
431431
msg := "Cannot assign to literal '" + truncate(content, 20) + "'"
432432
*d = append(*d, NewDiagnosticErrorFromSpan(node.Span(), c.src, msg, rl.ErrInvalidAssignmentTarget))
433433
case *rl.Call:
@@ -437,7 +437,7 @@ func (c *RadCheckerImpl) validateAssignmentTargetAST(node rl.Node, d *[]Diagnost
437437
msg := "Cannot assign to expression"
438438
*d = append(*d, NewDiagnosticErrorFromSpan(node.Span(), c.src, msg, rl.ErrInvalidAssignmentTarget))
439439
default:
440-
content := c.src[node.Span().StartByte:node.Span().EndByte]
440+
content := safeSlice(c.src, node.Span().StartByte, node.Span().EndByte)
441441
msg := "Cannot assign to '" + truncate(content, 20) + "'"
442442
*d = append(*d, NewDiagnosticErrorFromSpan(node.Span(), c.src, msg, rl.ErrInvalidAssignmentTarget))
443443
}

rts/check/check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ func (c *RadCheckerImpl) addUnknownCommandCallbackWarnings(d *[]Diagnostic) {
310310
hoistedFunctionSet[fnName] = true
311311
}
312312

313-
cmdBlocks, ok := c.tree.FindCmdBlocks()
313+
cmdBlocks, ok := c.tree.FindCmdBlocks("")
314314
if !ok {
315315
return
316316
}

rts/check/structs.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,36 @@ func NewDiagnosticFromSpan(
113113
Character: span.EndCol,
114114
},
115115
}
116-
lineSrc := strings.Split(originalSrc, "\n")[line]
116+
lines := strings.Split(originalSrc, "\n")
117+
lineSrc := ""
118+
if line >= 0 && line < len(lines) {
119+
lineSrc = lines[line]
120+
}
117121
return Diagnostic{
118122
OriginalSrc: originalSrc,
119123
Range: rang,
120-
RangedSrc: originalSrc[span.StartByte:span.EndByte],
124+
RangedSrc: safeSlice(originalSrc, span.StartByte, span.EndByte),
121125
LineSrc: lineSrc,
122126
Severity: severity,
123127
Message: msg,
124128
Code: code,
125129
}
126130
}
127131

132+
// safeSlice returns src[start:end] with bounds clamped to [0, len(src)].
133+
func safeSlice(src string, start, end int) string {
134+
if start < 0 {
135+
start = 0
136+
}
137+
if end > len(src) {
138+
end = len(src)
139+
}
140+
if start > end {
141+
start = end
142+
}
143+
return src[start:end]
144+
}
145+
128146
func NewDiagnosticErrorFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic {
129147
return NewDiagnosticFromSpan(span, originalSrc, Error, msg, &code)
130148
}

rts/nodes_commands.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ type CmdCallback struct {
4747
LambdaAST *rl.Lambda // Eagerly converted AST for lambda callbacks
4848
}
4949

50-
// newCmdBlock constructs a CmdBlock from a tree-sitter node
51-
func newCmdBlock(src string, node *ts.Node) (*CmdBlock, bool) {
50+
// newCmdBlock constructs a CmdBlock from a tree-sitter node.
51+
// file is threaded to the lambda converter for span metadata.
52+
func newCmdBlock(src string, node *ts.Node, file string) (*CmdBlock, bool) {
5253
// Extract command name
5354
nameNode := node.ChildByFieldName(rl.F_NAME)
5455
if nameNode == nil {
@@ -82,7 +83,7 @@ func newCmdBlock(src string, node *ts.Node) (*CmdBlock, bool) {
8283
exclusions := findArgExclusions(src, node)
8384

8485
// Extract callback
85-
callback := extractCmdCallback(src, node)
86+
callback := extractCmdCallback(src, node, file)
8687

8788
return &CmdBlock{
8889
BaseNode: newBaseNode(src, node),
@@ -98,8 +99,9 @@ func newCmdBlock(src string, node *ts.Node) (*CmdBlock, bool) {
9899
}, true
99100
}
100101

101-
// extractCmdCallback extracts the callback from a cmd_calls node
102-
func extractCmdCallback(src string, node *ts.Node) CmdCallback {
102+
// extractCmdCallback extracts the callback from a cmd_calls node.
103+
// file is used for span metadata in eagerly-converted lambda AST.
104+
func extractCmdCallback(src string, node *ts.Node, file string) CmdCallback {
103105
callsNode := node.ChildByFieldName(rl.F_CALLS)
104106
if callsNode == nil {
105107
panic(fmt.Sprintf("Bug! Command block missing 'calls' field at byte %d", node.StartByte()))
@@ -128,7 +130,7 @@ func extractCmdCallback(src string, node *ts.Node) CmdCallback {
128130
// Check for lambda callback
129131
lambdaNode := callsNode.ChildByFieldName(rl.F_CALLBACK_LAMBDA)
130132
if lambdaNode != nil {
131-
lambdaAST := safeConvertLambda(lambdaNode, src)
133+
lambdaAST := safeConvertLambda(lambdaNode, src, file)
132134
return CmdCallback{
133135
BaseNode: newBaseNode(src, callsNode),
134136
Type: CallbackLambda,
@@ -141,11 +143,11 @@ func extractCmdCallback(src string, node *ts.Node) CmdCallback {
141143

142144
// safeConvertLambda converts a lambda CST node to AST, recovering from panics
143145
// caused by malformed syntax. Returns nil if conversion fails.
144-
func safeConvertLambda(lambdaNode *ts.Node, src string) (result *rl.Lambda) {
146+
func safeConvertLambda(lambdaNode *ts.Node, src, file string) (result *rl.Lambda) {
145147
defer func() {
146148
if r := recover(); r != nil {
147149
result = nil
148150
}
149151
}()
150-
return ConvertLambda(lambdaNode, src, "")
152+
return ConvertLambda(lambdaNode, src, file)
151153
}

rts/rts_tree.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (rt *RadTree) FindArgBlock() (*ArgBlock, bool) {
7070
return argBlocks[0], true // todo bad if multiple
7171
}
7272

73-
func (rt *RadTree) FindCmdBlocks() ([]*CmdBlock, bool) {
73+
func (rt *RadTree) FindCmdBlocks(file string) ([]*CmdBlock, bool) {
7474
// Commands can appear multiple times, so we need to find all of them
7575
nodes := rt.FindNodes(rl.K_CMD_BLOCK)
7676
if len(nodes) == 0 {
@@ -79,7 +79,7 @@ func (rt *RadTree) FindCmdBlocks() ([]*CmdBlock, bool) {
7979

8080
cmdBlocks := make([]*CmdBlock, 0, len(nodes))
8181
for _, node := range nodes {
82-
cmdBlock, ok := newCmdBlock(rt.src, node)
82+
cmdBlock, ok := newCmdBlock(rt.src, node, file)
8383
if ok {
8484
cmdBlocks = append(cmdBlocks, cmdBlock)
8585
}
@@ -201,7 +201,7 @@ func createNode[T Node](src string, node *ts.Node) (T, bool) {
201201
argBlock, _ := newArgBlock(src, node)
202202
return any(argBlock).(T), true
203203
case *CmdBlock:
204-
cmdBlock, _ := newCmdBlock(src, node)
204+
cmdBlock, _ := newCmdBlock(src, node, "")
205205
return any(cmdBlock).(T), true
206206
case *StringNode:
207207
stringNode, _ := newStringNode(src, node)

0 commit comments

Comments
 (0)