1// ================================================================
2// Shows warnings for things like uninitialized variables. These are things
3// that are statically computable from the AST by itself -- it confines itself
4// to local-variable analysis. There are other uninitialization issues
5// detectable only at runtime, which would benefit from a 'strict mode'.
6// ================================================================
7
8package cst
9
10import (
11	"fmt"
12	"os"
13
14	"miller/src/dsl"
15	"miller/src/lib"
16)
17
18// ----------------------------------------------------------------
19// Returns true if there are no warnings.
20func WarnOnAST(
21	ast *dsl.AST,
22) bool {
23	variableNamesWrittenTo := make(map[string]bool)
24	inAssignment := false
25	ok := true
26
27	if ast.RootNode.Children != nil {
28		for _, astChild := range ast.RootNode.Children {
29			ok1 := warnOnASTAux(
30				astChild,
31				variableNamesWrittenTo,
32				inAssignment,
33			)
34			// Don't end early on first warning; tree-walk to list them all.
35			ok = ok1 && ok
36		}
37	}
38
39	return ok
40}
41
42// ----------------------------------------------------------------
43// Example ASTs:
44//
45// $ mlr -n put -v 'z = x + y'
46// DSL EXPRESSION:
47// z = x + y
48//
49// AST:
50// * statement block
51//     * assignment "="
52//         * local variable "z"
53//         * operator "+"
54//             * local variable "x"
55//             * local variable "y"
56//
57// $ mlr -n put -v 'z[i] = x + y'
58// DSL EXPRESSION:
59// z[i] = x + y
60//
61// AST:
62// * statement block
63//     * assignment "="
64//         * array or map index access "[]"
65//             * local variable "z"
66//             * local variable "i"
67//         * operator "+"
68//             * local variable "x"
69//             * local variable "y"
70//
71// $ mlr -n put -v 'func f(n) { return n+1}'
72// DSL EXPRESSION:
73// func f(n) { return n+1}
74//
75// AST:
76// * statement block
77//     * function definition "f"
78//         * parameter list
79//             * parameter
80//                 * parameter name "n"
81//         * statement block
82//             * return "return"
83//                 * operator "+"
84//                     * local variable "n"
85//                     * int literal "1"
86
87// Returns true if there are no warnings
88func warnOnASTAux(
89	astNode *dsl.ASTNode,
90	variableNamesWrittenTo map[string]bool,
91	inAssignment bool,
92) bool {
93
94	ok := true
95
96	// Check local-variable references, and see if they're reads or writes
97	// based on the AST parenting of this node.
98	if astNode.Type == dsl.NodeTypeLocalVariable {
99		variableName := string(astNode.Token.Lit)
100		if inAssignment {
101			variableNamesWrittenTo[variableName] = true
102		} else {
103			if !variableNamesWrittenTo[variableName] {
104				// TODO: this would be much more useful with line numbers. :(
105				// That would be a big of work with the parser.  Fortunately,
106				// Miller is designed around low-keystroke little expressions
107				// -- not thousands of lines of Miller-DSL source code -- so
108				// people can look at their few lines of Miller-DSL code and
109				// spot their error.
110				fmt.Fprintf(
111					os.Stderr,
112					"Variable name %s might not have been assigned yet.\n",
113					variableName,
114				)
115				ok = false
116			}
117		}
118
119	} else if astNode.Type == dsl.NodeTypeBeginBlock {
120		// Locals are confined to begin/end blocks and func/subr blocks.
121		// Reset for this part of the treewalk.
122		variableNamesWrittenTo = make(map[string]bool)
123	} else if astNode.Type == dsl.NodeTypeEndBlock {
124		// Locals are confined to begin/end blocks and func/subr blocks.
125		// Reset for this part of the treewalk.
126		variableNamesWrittenTo = make(map[string]bool)
127
128	} else if astNode.Type == dsl.NodeTypeFunctionDefinition {
129		// Locals are confined to begin/end blocks and func/subr blocks.  Reset
130		// for this part of the treewalk, except mark the parameters as
131		// defined.
132		variableNamesWrittenTo = noteParametersForWarnings(astNode)
133	} else if astNode.Type == dsl.NodeTypeSubroutineDefinition {
134		// Locals are confined to begin/end blocks and func/subr blocks.  Reset
135		// for this part of the treewalk, except mark the parameters as
136		// defined.
137		variableNamesWrittenTo = noteParametersForWarnings(astNode)
138	}
139
140	// Treewalk to check the rest of the AST below this node.
141
142	if astNode.Children != nil {
143		for i, astChild := range astNode.Children {
144			childInAssignment := inAssignment
145
146			if astNode.Type == dsl.NodeTypeAssignment && i == 0 {
147				// LHS of assignment statements
148				childInAssignment = true
149			} else if astNode.Type == dsl.NodeTypeForLoopOneVariable && i == 0 {
150				// The 'k' in 'for (k in $*)'
151				childInAssignment = true
152			} else if astNode.Type == dsl.NodeTypeForLoopTwoVariable && (i == 0 || i == 1) {
153				// The 'k' and 'v' in 'for (k,v in $*)'
154				childInAssignment = true
155			} else if astNode.Type == dsl.NodeTypeForLoopMultivariable && (i == 0 || i == 1) {
156				// The 'k1', 'k2', and 'v' in 'for ((k1,k2),v in $*)'
157				childInAssignment = true
158			} else if astNode.Type == dsl.NodeTypeParameterList {
159				childInAssignment = true
160			} else if inAssignment && astNode.Type == dsl.NodeTypeArrayOrMapIndexAccess {
161				// In 'z[i] = 1', the 'i' is a read and the 'z' is a write.
162				//
163				// mlr --from r put -v -W 'z[i] = 1'
164				// DSL EXPRESSION:
165				// z[i]=1
166				//
167				// AST:
168				// * statement block
169				//     * assignment "="
170				//         * array or map index access "[]"
171				//             * local variable "z"
172				//             * local variable "i"
173				//         * int literal "1"
174				if i == 0 {
175					childInAssignment = true
176				} else {
177					childInAssignment = false
178				}
179			}
180			ok1 := warnOnASTAux(
181				astChild,
182				variableNamesWrittenTo,
183				childInAssignment,
184			)
185			// Don't end early on first error; tree-walk to list them all.
186			ok = ok1 && ok
187		}
188	}
189
190	return ok
191}
192
193// ----------------------------------------------------------------
194// Given a func/subr block, find the names of its parameters.  All the
195// lib.InternalCodingErrorIf parts are shape-assertions to make sure this code
196// is in sync with the BNF grammar which builds the AST from a Miller-DSL
197// source string.
198func noteParametersForWarnings(
199	astNode *dsl.ASTNode,
200) map[string]bool {
201
202	variableNamesWrittenTo := make(map[string]bool)
203
204	lib.InternalCodingErrorIf(
205		astNode.Type != dsl.NodeTypeFunctionDefinition &&
206			astNode.Type != dsl.NodeTypeSubroutineDefinition)
207	lib.InternalCodingErrorIf(len(astNode.Children) < 1)
208	parameterListNode := astNode.Children[0]
209
210	lib.InternalCodingErrorIf(parameterListNode.Type != dsl.NodeTypeParameterList)
211
212	for _, parameterNode := range parameterListNode.Children {
213		lib.InternalCodingErrorIf(parameterNode.Type != dsl.NodeTypeParameter)
214		lib.InternalCodingErrorIf(len(parameterNode.Children) != 1)
215		parameterNameNode := parameterNode.Children[0]
216		lib.InternalCodingErrorIf(parameterNameNode.Type != dsl.NodeTypeParameterName)
217		parameterName := string(parameterNameNode.Token.Lit)
218		variableNamesWrittenTo[parameterName] = true
219	}
220
221	return variableNamesWrittenTo
222}
223