@@ -60,6 +60,10 @@ func (c *RadCheckerImpl) Check(opts Opts) (Result, error) {
6060 c .addFunctionNameShadowingErrors (& diagnostics )
6161 c .addUnknownFunctionHints (& diagnostics )
6262 c .addUnknownCommandCallbackWarnings (& diagnostics )
63+ // Semantic grammar checks
64+ c .addBreakContinueOutsideLoopErrors (& diagnostics )
65+ c .addReturnOutsideFunctionErrors (& diagnostics )
66+ c .addInvalidAssignmentLHSErrors (& diagnostics )
6367 return Result {
6468 Diagnostics : diagnostics ,
6569 }, nil
@@ -319,3 +323,193 @@ func (c *RadCheckerImpl) addUnknownCommandCallbackWarnings(d *[]Diagnostic) {
319323 * d = append (* d , NewDiagnosticWarn (identifierNode , c .src , msg , rl .ErrUnknownFunction ))
320324 }
321325}
326+
327+ // addBreakContinueOutsideLoopErrors checks for break/continue statements outside of loops.
328+ func (c * RadCheckerImpl ) addBreakContinueOutsideLoopErrors (d * []Diagnostic ) {
329+ root := c .tree .Root ()
330+ c .walkForBreakContinue (root , d , 0 )
331+ }
332+
333+ func (c * RadCheckerImpl ) walkForBreakContinue (node * ts.Node , d * []Diagnostic , loopDepth int ) {
334+ if node == nil {
335+ return
336+ }
337+
338+ kind := node .Kind ()
339+
340+ // Track loop entry
341+ newLoopDepth := loopDepth
342+ if kind == rl .K_FOR_LOOP || kind == rl .K_WHILE_LOOP {
343+ newLoopDepth ++
344+ }
345+
346+ // Check for break/continue outside loop
347+ if kind == rl .K_BREAK_STMT {
348+ if loopDepth == 0 {
349+ msg := "'break' can only be used inside a loop"
350+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrBreakOutsideLoop ))
351+ }
352+ } else if kind == rl .K_CONTINUE_STMT {
353+ if loopDepth == 0 {
354+ msg := "'continue' can only be used inside a loop"
355+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrContinueOutsideLoop ))
356+ }
357+ }
358+
359+ // Recursively walk children
360+ for i := uint (0 ); i < node .ChildCount (); i ++ {
361+ c .walkForBreakContinue (node .Child (i ), d , newLoopDepth )
362+ }
363+ }
364+
365+ // addReturnOutsideFunctionErrors checks for return statements outside of functions.
366+ func (c * RadCheckerImpl ) addReturnOutsideFunctionErrors (d * []Diagnostic ) {
367+ root := c .tree .Root ()
368+ c .walkForReturn (root , d , false , false )
369+ }
370+
371+ // walkForReturn tracks both function context and switch/yield context.
372+ // yieldContext is true when inside a switch expression (where yield is valid).
373+ func (c * RadCheckerImpl ) walkForReturn (node * ts.Node , d * []Diagnostic , inFunction , inYieldContext bool ) {
374+ if node == nil {
375+ return
376+ }
377+
378+ kind := node .Kind ()
379+
380+ // Track context changes
381+ newInFunction := inFunction
382+ newInYieldContext := inYieldContext
383+
384+ if kind == rl .K_FN_NAMED || kind == rl .K_FN_LAMBDA {
385+ newInFunction = true
386+ }
387+
388+ // Switch expressions create a yield context (yield produces the switch result)
389+ if kind == rl .K_SWITCH_STMT {
390+ newInYieldContext = true
391+ }
392+
393+ // Check for return outside function
394+ if kind == rl .K_RETURN_STMT {
395+ if ! inFunction {
396+ msg := "'return' can only be used inside a function"
397+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrReturnOutsideFunction ))
398+ }
399+ }
400+
401+ // Check yield - valid inside functions OR switch expressions
402+ if kind == rl .K_YIELD_STMT {
403+ if ! inFunction && ! inYieldContext {
404+ msg := "'yield' can only be used inside a function or switch expression"
405+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrYieldOutsideFunction ))
406+ }
407+ }
408+
409+ // Recursively walk children
410+ for i := uint (0 ); i < node .ChildCount (); i ++ {
411+ c .walkForReturn (node .Child (i ), d , newInFunction , newInYieldContext )
412+ }
413+ }
414+
415+ // addInvalidAssignmentLHSErrors checks for invalid assignment targets.
416+ func (c * RadCheckerImpl ) addInvalidAssignmentLHSErrors (d * []Diagnostic ) {
417+ root := c .tree .Root ()
418+ c .walkForAssignments (root , d )
419+ }
420+
421+ func (c * RadCheckerImpl ) walkForAssignments (node * ts.Node , d * []Diagnostic ) {
422+ if node == nil {
423+ return
424+ }
425+
426+ if node .Kind () == rl .K_ASSIGN {
427+ c .checkAssignmentLHS (node , d )
428+ }
429+
430+ // Recursively walk children
431+ for i := uint (0 ); i < node .ChildCount (); i ++ {
432+ c .walkForAssignments (node .Child (i ), d )
433+ }
434+ }
435+
436+ func (c * RadCheckerImpl ) checkAssignmentLHS (assignNode * ts.Node , d * []Diagnostic ) {
437+ // Get the left-hand side(s) of the assignment
438+ lefts := assignNode .ChildrenByFieldName (rl .F_LEFTS , assignNode .Walk ())
439+ if len (lefts ) == 0 {
440+ // Single left
441+ left := assignNode .ChildByFieldName (rl .F_LEFT )
442+ if left != nil {
443+ c .validateAssignmentTarget (left , d )
444+ }
445+ return
446+ }
447+
448+ // Multiple lefts (unpacking)
449+ for i := range lefts {
450+ c .validateAssignmentTarget (& lefts [i ], d )
451+ }
452+ }
453+
454+ func (c * RadCheckerImpl ) validateAssignmentTarget (node * ts.Node , d * []Diagnostic ) {
455+ if node == nil {
456+ return
457+ }
458+
459+ kind := node .Kind ()
460+
461+ // Valid assignment targets:
462+ // - identifier (x = 1)
463+ // - var_path (x.y = 1 or x[0] = 1)
464+ // - indexed_expr (x[0] = 1)
465+
466+ switch kind {
467+ case rl .K_IDENTIFIER , rl .K_VAR_PATH , rl .K_INDEXED_EXPR :
468+ // These are valid assignment targets
469+ return
470+ case rl .K_INT , rl .K_FLOAT , rl .K_STRING , rl .K_BOOL , rl .K_NULL :
471+ // Literals cannot be assigned to
472+ content := c .src [node .StartByte ():node .EndByte ()]
473+ msg := "Cannot assign to literal '" + truncate (content , 20 ) + "'"
474+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrInvalidAssignmentTarget ))
475+ case rl .K_CALL :
476+ // Function calls cannot be assigned to
477+ msg := "Cannot assign to function call result"
478+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrInvalidAssignmentTarget ))
479+ case rl .K_ADD_EXPR , rl .K_MULT_EXPR , rl .K_COMPARE_EXPR , rl .K_OR_EXPR , rl .K_AND_EXPR , rl .K_TERNARY_EXPR :
480+ // Expressions cannot be assigned to
481+ msg := "Cannot assign to expression"
482+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrInvalidAssignmentTarget ))
483+ case rl .K_PARENTHESIZED_EXPR :
484+ // Check inside parentheses
485+ inner := node .ChildByFieldName (rl .F_EXPR )
486+ if inner != nil {
487+ c .validateAssignmentTarget (inner , d )
488+ }
489+ default :
490+ // For any other unexpected node type
491+ if ! isValidAssignmentTarget (kind ) {
492+ content := c .src [node .StartByte ():node .EndByte ()]
493+ msg := "Cannot assign to '" + truncate (content , 20 ) + "'"
494+ * d = append (* d , NewDiagnosticError (node , c .src , msg , rl .ErrInvalidAssignmentTarget ))
495+ }
496+ }
497+ }
498+
499+ // isValidAssignmentTarget returns true if the node kind is a valid LHS for assignment.
500+ func isValidAssignmentTarget (kind string ) bool {
501+ switch kind {
502+ case rl .K_IDENTIFIER , rl .K_VAR_PATH , rl .K_INDEXED_EXPR :
503+ return true
504+ default :
505+ return false
506+ }
507+ }
508+
509+ // truncate shortens a string to maxLen, adding "..." if truncated.
510+ func truncate (s string , maxLen int ) string {
511+ if len (s ) <= maxLen {
512+ return s
513+ }
514+ return s [:maxLen - 3 ] + "..."
515+ }
0 commit comments