miller/pkg/dsl/cst/warn.go
Adam Lesperance 085e831668
The package version must match the major tag version (#1654)
* Update package version

* Update makefile targets

* Update readme packages

* Remaining old packages via rg/sd
2024-09-20 12:10:11 -04:00

213 lines
6.4 KiB
Go

// ================================================================
// Shows warnings for things like uninitialized variables. These are things
// that are statically computable from the AST by itself -- it confines itself
// to local-variable analysis. There are other uninitialization issues
// detectable only at runtime, which would benefit from a 'strict mode'.
// ================================================================
package cst
import (
"fmt"
"os"
"github.com/johnkerl/miller/v6/pkg/dsl"
"github.com/johnkerl/miller/v6/pkg/lib"
)
// ----------------------------------------------------------------
// Returns true if there are no warnings.
func WarnOnAST(
ast *dsl.AST,
) bool {
variableNamesWrittenTo := make(map[string]bool)
inAssignment := false
ok := true
for _, astChild := range ast.RootNode.Children {
ok1 := warnOnASTAux(
astChild,
variableNamesWrittenTo,
inAssignment,
)
// Don't end early on first warning; tree-walk to list them all.
ok = ok1 && ok
}
return ok
}
// ----------------------------------------------------------------
// Example ASTs:
//
// $ mlr -n put -v 'z = x + y'
// DSL EXPRESSION:
// z = x + y
//
// AST:
// * statement block
// * assignment "="
// * local variable "z"
// * operator "+"
// * local variable "x"
// * local variable "y"
//
// $ mlr -n put -v 'z[i] = x + y'
// DSL EXPRESSION:
// z[i] = x + y
//
// AST:
// * statement block
// * assignment "="
// * array or map index access "[]"
// * local variable "z"
// * local variable "i"
// * operator "+"
// * local variable "x"
// * local variable "y"
//
// $ mlr -n put -v 'func f(n) { return n+1}'
// DSL EXPRESSION:
// func f(n) { return n+1}
//
// AST:
// * statement block
// * function definition "f"
// * parameter list
// * parameter
// * parameter name "n"
// * statement block
// * return "return"
// * operator "+"
// * local variable "n"
// * int literal "1"
// Returns true if there are no warnings
func warnOnASTAux(
astNode *dsl.ASTNode,
variableNamesWrittenTo map[string]bool,
inAssignment bool,
) bool {
ok := true
// Check local-variable references, and see if they're reads or writes
// based on the AST parenting of this node.
if astNode.Type == dsl.NodeTypeLocalVariable {
variableName := string(astNode.Token.Lit)
if inAssignment {
variableNamesWrittenTo[variableName] = true
} else {
if !variableNamesWrittenTo[variableName] {
fmt.Fprintf(
os.Stderr,
"Variable name %s might not have been assigned yet%s.\n",
variableName,
dsl.TokenToLocationInfo(astNode.Token),
)
ok = false
}
}
} else if astNode.Type == dsl.NodeTypeBeginBlock {
// Locals are confined to begin/end blocks and func/subr blocks.
// Reset for this part of the treewalk.
variableNamesWrittenTo = make(map[string]bool)
} else if astNode.Type == dsl.NodeTypeEndBlock {
// Locals are confined to begin/end blocks and func/subr blocks.
// Reset for this part of the treewalk.
variableNamesWrittenTo = make(map[string]bool)
} else if astNode.Type == dsl.NodeTypeNamedFunctionDefinition {
// Locals are confined to begin/end blocks and func/subr blocks. Reset
// for this part of the treewalk, except mark the parameters as
// defined.
variableNamesWrittenTo = noteParametersForWarnings(astNode)
} else if astNode.Type == dsl.NodeTypeSubroutineDefinition {
// Locals are confined to begin/end blocks and func/subr blocks. Reset
// for this part of the treewalk, except mark the parameters as
// defined.
variableNamesWrittenTo = noteParametersForWarnings(astNode)
}
// Treewalk to check the rest of the AST below this node.
for i, astChild := range astNode.Children {
childInAssignment := inAssignment
if astNode.Type == dsl.NodeTypeAssignment && i == 0 {
// LHS of assignment statements
childInAssignment = true
} else if astNode.Type == dsl.NodeTypeForLoopOneVariable && i == 0 {
// The 'k' in 'for (k in $*)'
childInAssignment = true
} else if astNode.Type == dsl.NodeTypeForLoopTwoVariable && (i == 0 || i == 1) {
// The 'k' and 'v' in 'for (k,v in $*)'
childInAssignment = true
} else if astNode.Type == dsl.NodeTypeForLoopMultivariable && (i == 0 || i == 1) {
// The 'k1', 'k2', and 'v' in 'for ((k1,k2),v in $*)'
childInAssignment = true
} else if astNode.Type == dsl.NodeTypeParameterList {
childInAssignment = true
} else if inAssignment && astNode.Type == dsl.NodeTypeArrayOrMapIndexAccess {
// In 'z[i] = 1', the 'i' is a read and the 'z' is a write.
//
// mlr --from r put -v -W 'z[i] = 1'
// DSL EXPRESSION:
// z[i]=1
//
// AST:
// * statement block
// * assignment "="
// * array or map index access "[]"
// * local variable "z"
// * local variable "i"
// * int literal "1"
if i == 0 {
childInAssignment = true
} else {
childInAssignment = false
}
}
ok1 := warnOnASTAux(
astChild,
variableNamesWrittenTo,
childInAssignment,
)
// Don't end early on first error; tree-walk to list them all.
ok = ok1 && ok
}
return ok
}
// ----------------------------------------------------------------
// Given a func/subr block, find the names of its parameters. All the
// lib.InternalCodingErrorIf parts are shape-assertions to make sure this code
// is in sync with the BNF grammar which builds the AST from a Miller-DSL
// source string.
func noteParametersForWarnings(
astNode *dsl.ASTNode,
) map[string]bool {
variableNamesWrittenTo := make(map[string]bool)
lib.InternalCodingErrorIf(
astNode.Type != dsl.NodeTypeNamedFunctionDefinition &&
astNode.Type != dsl.NodeTypeSubroutineDefinition)
lib.InternalCodingErrorIf(len(astNode.Children) < 1)
parameterListNode := astNode.Children[0]
lib.InternalCodingErrorIf(parameterListNode.Type != dsl.NodeTypeParameterList)
for _, parameterNode := range parameterListNode.Children {
lib.InternalCodingErrorIf(parameterNode.Type != dsl.NodeTypeParameter)
lib.InternalCodingErrorIf(len(parameterNode.Children) != 1)
parameterNameNode := parameterNode.Children[0]
lib.InternalCodingErrorIf(parameterNameNode.Type != dsl.NodeTypeParameterName)
parameterName := string(parameterNameNode.Token.Lit)
variableNamesWrittenTo[parameterName] = true
}
return variableNamesWrittenTo
}