mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
* refine the plan
* Fix all staticcheck lint findings (uncapped)
golangci-lint's default max-same-issues=3 was hiding most of the backlog:
the true pre-fix count was 69 staticcheck findings, not 34. This fixes all
of them, driving staticcheck to zero:
- ST1023/QF1011 (37): omit explicit types inferred from the RHS
- S1009/S1031 (15): drop redundant nil checks before len()/range
- SA9003 (9): remove comment-only empty branches, keeping the comments
- QF1007 (3): merge conditional assignment into declaration
- QF1006 (3): lift break conditions into loop conditions
- QF1001 (3): apply De Morgan's law / name the negated predicate
Also updates plans/lintfixes.md with the cap discovery and the corrected
errcheck picture (1202 uncapped, ~949 of them fmt.Fprint*).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drive errcheck to zero: config for bulk categories, propagate real errors
Adds .golangci.yml with errcheck exclude-functions for fmt.Fprint* (usage
printers), (*bufio.Writer).Write/WriteString (sticky errors, surfaced at the
now-checked final Flush), and (*strings.Builder).WriteString; pins
max-issues-per-linter/max-same-issues to 0 so CI reports true counts.
Real error paths now propagate instead of being dropped:
- Finalize{Reader,Writer}Options in join/put/filter/split/tee and the
repl/script entry points: 'mlr join -i badformat' now errors instead of
silently using wrong separators
- final output-stream Flush in pkg/stream: write failure no longer exits 0
- DSL emit/print/dump redirect writes, matching their sibling branches
- CSV writer WriteCSVRecordMaybeColorized, close-time Flush in file output
handlers, ENV[...] Setenv, REPL record-write and redirect-close errors
- termcvt write-side Close before rename (had "TODO: check return status")
The rest are deliberate ignores, marked with _ = and a comment where the
reason isn't obvious: unset-of-missing-path no-ops, read-side closes,
mid-stream FlushOnEveryRecord, init-time strftime registrations, in-memory
usage-capture pipes, and regtest-harness env/temp-file teardown.
golangci-lint now reports 0 issues on ./cmd/mlr ./pkg/... with all caps off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
496 lines
15 KiB
Go
496 lines
15 KiB
Go
// Top-level entry point for building a CST from an AST at parse time, and for
|
|
// executing the CST at runtime.
|
|
|
|
package cst
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/cli"
|
|
"github.com/johnkerl/miller/v6/pkg/mlrval"
|
|
"github.com/johnkerl/miller/v6/pkg/output"
|
|
"github.com/johnkerl/miller/v6/pkg/parsing/lexer"
|
|
"github.com/johnkerl/miller/v6/pkg/parsing/parser"
|
|
"github.com/johnkerl/miller/v6/pkg/runtime"
|
|
|
|
"github.com/johnkerl/pgpg/go/lib/pkg/asts"
|
|
)
|
|
|
|
// NewEmptyRoot sets up an empty CST, before ingesting any DSL strings. For
|
|
// mlr put and mlr filter, CSTs are constructed, then ASTs are ingested from
|
|
// DSL strings. For the REPL, a CST is constructed once, then an AST is
|
|
// ingested on every line of input from the REPL.
|
|
func NewEmptyRoot(
|
|
recordWriterOptions *cli.TWriterOptions,
|
|
dslInstanceType DSLInstanceType, // mlr put, mlr filter, or mlr repl
|
|
) *RootNode {
|
|
return &RootNode{
|
|
beginBlocks: []*StatementBlockNode{},
|
|
mainBlock: NewStatementBlockNode(),
|
|
replImmediateBlock: NewStatementBlockNode(),
|
|
endBlocks: []*StatementBlockNode{},
|
|
udfManager: NewUDFManager(),
|
|
udsManager: NewUDSManager(),
|
|
allowUDFUDSRedefinitions: false,
|
|
unresolvedFunctionCallsites: []*UDFCallsite{},
|
|
unresolvedSubroutineCallsites: []*UDSCallsite{},
|
|
outputHandlerManagers: []output.OutputHandlerManager{},
|
|
recordWriterOptions: recordWriterOptions,
|
|
dslInstanceType: dslInstanceType,
|
|
}
|
|
}
|
|
|
|
// Nominally for mlr put/filter we want to flag overwritten UDFs/UDSs as an
|
|
// error. But in the REPL, which is interactive, people should be able to
|
|
// redefine. This method allows the latter use-case.
|
|
func (root *RootNode) WithRedefinableUDFUDS() *RootNode {
|
|
root.allowUDFUDSRedefinitions = true
|
|
return root
|
|
}
|
|
|
|
// WithStrictMode allows for runtime handling of absent-reads and untyped assignments.
|
|
func (root *RootNode) WithStrictMode(strictMode bool) *RootNode {
|
|
root.strictMode = strictMode
|
|
return root
|
|
}
|
|
|
|
// ASTBuildVisitorFunc is a callback, used by RootNode's Build method, which
|
|
// CST-builder callsites can use to visit parse-to-AST result of multi-string
|
|
// DSL inputs. Nominal use: mlr put -v, mlr put -d, etc.
|
|
type ASTBuildVisitorFunc func(dslString string, astNode *asts.AST)
|
|
|
|
// Used by DSL -> AST -> CST callsites including mlr put, mlr filter, and mlr
|
|
// repl. The RootNode must be separately instantiated (e.g. NewEmptyRoot())
|
|
// since the CST is partially reset on every line of input from the REPL
|
|
// prompt.
|
|
func (root *RootNode) Build(
|
|
dslStrings []string,
|
|
dslInstanceType DSLInstanceType,
|
|
isReplImmediate bool,
|
|
doWarnings bool,
|
|
astBuildVisitorFunc ASTBuildVisitorFunc,
|
|
) (hadWarnings bool, err error) {
|
|
for _, dslString := range dslStrings {
|
|
astRootNode, err := buildASTFromStringWithMessage(dslString)
|
|
if err != nil {
|
|
// Error message already printed out
|
|
return hadWarnings, err
|
|
}
|
|
|
|
// E.g. mlr put -v -- let it print out what it needs to.
|
|
if astBuildVisitorFunc != nil {
|
|
astBuildVisitorFunc(dslString, astRootNode)
|
|
}
|
|
|
|
hadWarnings, err = root.IngestAST(
|
|
astRootNode,
|
|
isReplImmediate,
|
|
doWarnings,
|
|
)
|
|
if err != nil {
|
|
return hadWarnings, err
|
|
}
|
|
}
|
|
|
|
err = root.Resolve()
|
|
if err != nil {
|
|
return hadWarnings, err
|
|
}
|
|
|
|
return hadWarnings, err
|
|
}
|
|
|
|
func buildASTFromStringWithMessage(dslString string) (*asts.AST, error) {
|
|
astRootNode, err := buildASTFromString(dslString)
|
|
if err != nil {
|
|
// Leave this out until we get better control over the error-messaging.
|
|
// At present it's overly parser-internal, and confusing. :(
|
|
fmt.Fprintln(os.Stderr, "mlr: cannot parse DSL expression.")
|
|
return nil, err
|
|
}
|
|
return astRootNode, nil
|
|
}
|
|
|
|
func buildASTFromString(dslString string) (*asts.AST, error) {
|
|
// For non-Windows, already stripped by the shell; helpful here for Windows.
|
|
if strings.HasPrefix(dslString, "'") && strings.HasSuffix(dslString, "'") {
|
|
dslString = dslString[1 : len(dslString)-1]
|
|
}
|
|
|
|
// The comment-stripping lex expression in Miller's grammar matches from '#' to '\n' ... in
|
|
// the case where there is a '#', then comment text, then end-of-string without any final
|
|
// newline, the comment text does _not_ get stripped out and is a parse error.
|
|
// It's simplest to ensure, here, that DSL-expression strings have a final newline.
|
|
if !strings.HasSuffix(dslString, "\n") {
|
|
dslString += "\n"
|
|
}
|
|
|
|
theLexer := lexer.NewLexer([]byte(dslString))
|
|
theParser := parser.NewMlrParser()
|
|
return theParser.Parse(theLexer, "")
|
|
}
|
|
|
|
// If the user has multiple put -f / put -e pieces, we can AST-parse each
|
|
// separately and build them. However we cannot resolve UDF/UDS references
|
|
// until after they're all ingested -- e.g. first piece calls a function which
|
|
// the second defines, or mutual recursion across pieces, etc.
|
|
func (root *RootNode) IngestAST(
|
|
ast *asts.AST,
|
|
// False for non-REPL use. Also false for bulk-load REPL use. True for
|
|
// interactive REPL statements which are intended to be executed once
|
|
// (immediately) but not retained.
|
|
isReplImmediate bool,
|
|
doWarnings bool,
|
|
) (hadWarnings bool, err error) {
|
|
if ast.RootNode == nil {
|
|
return hadWarnings, errors.New("cannot build CST from nil AST root")
|
|
}
|
|
|
|
// Check for things that are syntax errors but not done in the AST for
|
|
// pragmatic reasons. For example, $anything in begin/end blocks;
|
|
// begin/end/func not at top level; etc.
|
|
err = ValidateAST(ast, root.dslInstanceType)
|
|
if err != nil {
|
|
return hadWarnings, err
|
|
}
|
|
|
|
if doWarnings {
|
|
ok := WarnOnAST(ast)
|
|
if !ok {
|
|
hadWarnings = true
|
|
}
|
|
}
|
|
|
|
// For debug:
|
|
// fmt.Println("PRE")
|
|
// ast.Print()
|
|
root.regexProtectPrePass(ast)
|
|
// fmt.Println("POST")
|
|
// ast.Print()
|
|
|
|
err = root.buildMainPass(ast, isReplImmediate)
|
|
if err != nil {
|
|
return hadWarnings, err
|
|
}
|
|
|
|
return hadWarnings, nil
|
|
}
|
|
|
|
// Resolve is called after IngestAST has been called one or more times.
|
|
// See comments above IngestAST.
|
|
func (root *RootNode) Resolve() error {
|
|
|
|
err := root.resolveFunctionCallsites()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = root.resolveSubroutineCallsites()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// regexProtectPrePass rewrites string-literal nodes in regex position (e.g.
|
|
// second arg to gsub) to have regex node type. This is so we can have "\t" be
|
|
// a tab character for string literals generally, but remain backslash-t for
|
|
// regex literals.
|
|
//
|
|
// Callsites to have regexes protected:
|
|
// * sub/gsub second argument;
|
|
// * regextract/regextract_or_else second argument;
|
|
// * =~ and !=~ right-hand side -- since these are infix operators, this means
|
|
// (in the AST point of view) second argument.
|
|
//
|
|
// Sample ASTs:
|
|
//
|
|
// $ mlr -n put -v '$y =~ "\t"'
|
|
// AST:
|
|
// * statement block
|
|
// * bare boolean
|
|
// * operator "=~"
|
|
// * direct field value "y"
|
|
// * string literal " "
|
|
//
|
|
// $ mlr -n put -v '$y = sub($x, "\t", "TAB")'
|
|
// AST:
|
|
// * statement block
|
|
// * assignment "="
|
|
// * direct field value "y"
|
|
// * function callsite "sub"
|
|
// * direct field value "x"
|
|
// * string literal " "
|
|
// * string literal "TAB"
|
|
|
|
func (root *RootNode) regexProtectPrePass(ast *asts.AST) {
|
|
root.regexProtectPrePassAux(ast.RootNode)
|
|
}
|
|
|
|
func (root *RootNode) regexProtectPrePassAux(astNode *asts.ASTNode) {
|
|
|
|
if len(astNode.Children) == 0 {
|
|
return
|
|
}
|
|
|
|
isCallsiteOfInterest := false
|
|
if astNode.Type == asts.NodeType(NodeTypeOperator) {
|
|
nodeName := tokenLit(astNode)
|
|
if nodeName == "=~" || nodeName == "!=~" {
|
|
isCallsiteOfInterest = true
|
|
}
|
|
} else if astNode.Type == asts.NodeType(NodeTypeFunctionCallsite) {
|
|
nodeName := tokenLit(astNode)
|
|
if nodeName == "sub" || nodeName == "gsub" || nodeName == "regextract" || nodeName == "regextract_or_else" {
|
|
isCallsiteOfInterest = true
|
|
}
|
|
}
|
|
|
|
for i, astChild := range astNode.Children {
|
|
if isCallsiteOfInterest && i == 1 {
|
|
if astChild.Type == asts.NodeType(NodeTypeStringLiteral) {
|
|
astChild.Type = asts.NodeType(NodeTypeRegex)
|
|
}
|
|
}
|
|
root.regexProtectPrePassAux(astChild)
|
|
}
|
|
|
|
}
|
|
|
|
// This builds the CST almost entirely. The only afterwork is that user-defined
|
|
// functions may be called before they are defined, so a follow-up pass will
|
|
// need to resolve those callsites.
|
|
|
|
func (root *RootNode) buildMainPass(ast *asts.AST, isReplImmediate bool) error {
|
|
|
|
if ast.RootNode.Type != asts.NodeType(NodeTypeStatementBlock) {
|
|
return fmt.Errorf("at CST root build: non-statement-block AST root node unhandled")
|
|
}
|
|
astChildren := ast.RootNode.Children
|
|
|
|
// Example AST:
|
|
//
|
|
// $ mlr put -v 'begin{@a=1;@b=2} $x=3; $y=4' myfile.dkvp
|
|
// DSL EXPRESSION:
|
|
// begin{@a=1;@b=2} $x=3; $y=4
|
|
// AST:
|
|
// * StatementBlock
|
|
// * BeginBlock
|
|
// * StatementBlock
|
|
// * Assignment "="
|
|
// * DirectOosvarValue "a"
|
|
// * IntLiteral "1"
|
|
// * Assignment "="
|
|
// * DirectOosvarValue "b"
|
|
// * IntLiteral "2"
|
|
// * Assignment "="
|
|
// * DirectFieldValue "x"
|
|
// * IntLiteral "3"
|
|
// * Assignment "="
|
|
// * DirectFieldValue "y"
|
|
// * IntLiteral "4"
|
|
|
|
for _, astChild := range astChildren {
|
|
|
|
if astChild.Type == asts.NodeType(NodeTypeNamedFunctionDefinition) {
|
|
err := root.BuildAndInstallUDF(astChild)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
} else if astChild.Type == asts.NodeType(NodeTypeSubroutineDefinition) {
|
|
err := root.BuildAndInstallUDS(astChild)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
} else if astChild.Type == asts.NodeType(NodeTypeBeginBlock) || astChild.Type == asts.NodeType(NodeTypeEndBlock) {
|
|
statementBlockNode, err := root.BuildStatementBlockNodeFromBeginOrEnd(astChild)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if astChild.Type == asts.NodeType(NodeTypeBeginBlock) {
|
|
root.beginBlocks = append(root.beginBlocks, statementBlockNode)
|
|
} else {
|
|
root.endBlocks = append(root.endBlocks, statementBlockNode)
|
|
}
|
|
} else if isReplImmediate {
|
|
statementNode, err := root.BuildStatementNode(astChild)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root.replImmediateBlock.AppendStatementNode(statementNode)
|
|
} else {
|
|
statementNode, err := root.BuildStatementNode(astChild)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root.mainBlock.AppendStatementNode(statementNode)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// This is invoked within the buildMainPass call tree whenever a function is
|
|
// called before it's defined.
|
|
func (root *RootNode) rememberUnresolvedFunctionCallsite(udfCallsite *UDFCallsite) {
|
|
root.unresolvedFunctionCallsites = append(root.unresolvedFunctionCallsites, udfCallsite)
|
|
}
|
|
|
|
func (root *RootNode) rememberUnresolvedSubroutineCallsite(udsCallsite *UDSCallsite) {
|
|
root.unresolvedSubroutineCallsites = append(root.unresolvedSubroutineCallsites, udsCallsite)
|
|
}
|
|
|
|
// After-pass after buildMainPass returns, in case a function was called before
|
|
// it was defined. It may be the case that:
|
|
//
|
|
// * A user-defined function was called before it was defined, and was actually defined.
|
|
// * A user-defined function was called before it was defined, and it was not actually defined.
|
|
// * The user misspelled the name of a built-in function.
|
|
//
|
|
// So, our error message should reflect all those options.
|
|
|
|
func (root *RootNode) resolveFunctionCallsites() error {
|
|
for len(root.unresolvedFunctionCallsites) > 0 {
|
|
unresolvedFunctionCallsite := root.unresolvedFunctionCallsites[0]
|
|
root.unresolvedFunctionCallsites = root.unresolvedFunctionCallsites[1:]
|
|
|
|
functionName := unresolvedFunctionCallsite.udf.signature.funcOrSubrName
|
|
callsiteArity := unresolvedFunctionCallsite.udf.signature.arity
|
|
|
|
udf, err := root.udfManager.LookUp(functionName, callsiteArity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// A nil udf here is unresolvable at CST-build time but perhaps a local
|
|
// variable. For example, the UDF callsite '$z = f($x, $y)', and supposing
|
|
// there will be 'f = func(a, b) { return a*b }' in scope at runtime.
|
|
unresolvedFunctionCallsite.udf = udf
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (root *RootNode) resolveSubroutineCallsites() error {
|
|
for len(root.unresolvedSubroutineCallsites) > 0 {
|
|
unresolvedSubroutineCallsite := root.unresolvedSubroutineCallsites[0]
|
|
root.unresolvedSubroutineCallsites = root.unresolvedSubroutineCallsites[1:]
|
|
|
|
subroutineName := unresolvedSubroutineCallsite.uds.signature.funcOrSubrName
|
|
callsiteArity := unresolvedSubroutineCallsite.uds.signature.arity
|
|
|
|
uds, err := root.udsManager.LookUp(subroutineName, callsiteArity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if uds == nil {
|
|
return errors.New("subroutine name not found: " + subroutineName)
|
|
}
|
|
|
|
unresolvedSubroutineCallsite.uds = uds
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Various 'tee > $hostname . ".dat", $*' statements will have
|
|
// OutputHandlerManager instances to track file-descriptors for all unique
|
|
// values of $hostname in the input stream.
|
|
//
|
|
// At CST-build time, the builders are expected to call this so we can put
|
|
// OutputHandlerManager instances on a list. Then, at end of stream, we
|
|
// can close all the descriptors, flush the record-output streams, etc.
|
|
|
|
func (root *RootNode) RegisterOutputHandlerManager(
|
|
outputHandlerManager output.OutputHandlerManager,
|
|
) {
|
|
root.outputHandlerManagers = append(root.outputHandlerManagers, outputHandlerManager)
|
|
}
|
|
|
|
func (root *RootNode) ProcessEndOfStream() {
|
|
for _, outputHandlerManager := range root.outputHandlerManagers {
|
|
errs := outputHandlerManager.Close()
|
|
if len(errs) != 0 {
|
|
for _, err := range errs {
|
|
fmt.Fprintf(
|
|
os.Stderr,
|
|
"%s: error on end-of-stream close: %v\n",
|
|
"mlr",
|
|
err,
|
|
)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (root *RootNode) ExecuteBeginBlocks(state *runtime.State) error {
|
|
for _, beginBlock := range root.beginBlocks {
|
|
_, err := beginBlock.Execute(state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (root *RootNode) ExecuteMainBlock(state *runtime.State) (outrec *mlrval.Mlrmap, err error) {
|
|
_, err = root.mainBlock.Execute(state)
|
|
return state.Inrec, err
|
|
}
|
|
|
|
func (root *RootNode) ExecuteEndBlocks(state *runtime.State) error {
|
|
for _, endBlock := range root.endBlocks {
|
|
_, err := endBlock.Execute(state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// These are for the Miller REPL.
|
|
|
|
// If a DSL string was parsed into an AST and ingested in 'immediate' mode and
|
|
// build into the CST, it's not populated into the main-statements block for
|
|
// remembered execution on every record. Rather, it's just stored once, to be
|
|
// executed once, and then discarded.
|
|
|
|
// This is the 'execute once' part of that.
|
|
func (root *RootNode) ExecuteREPLImmediate(state *runtime.State) (outrec *mlrval.Mlrmap, err error) {
|
|
_, err = root.replImmediateBlock.ExecuteFrameless(state)
|
|
return state.Inrec, err
|
|
}
|
|
|
|
// This is the 'and then discarded' part of that.
|
|
func (root *RootNode) ResetForREPL() {
|
|
root.replImmediateBlock = NewStatementBlockNode()
|
|
root.unresolvedFunctionCallsites = []*UDFCallsite{}
|
|
root.unresolvedSubroutineCallsites = []*UDSCallsite{}
|
|
}
|
|
|
|
// This is for the REPL's context-printer command.
|
|
func (root *RootNode) ShowBlockReport() {
|
|
fmt.Printf("#begin %d\n", len(root.beginBlocks))
|
|
fmt.Printf("#main %d\n", len(root.mainBlock.executables))
|
|
fmt.Printf("#end %d\n", len(root.endBlocks))
|
|
}
|
|
|
|
// This is for the REPL's resetblocks command.
|
|
func (root *RootNode) ResetBeginBlocksForREPL() {
|
|
root.beginBlocks = []*StatementBlockNode{}
|
|
}
|
|
|
|
// This is for the REPL's resetblocks command.
|
|
func (root *RootNode) ResetMainBlockForREPL() {
|
|
root.mainBlock.executables = []IExecutable{}
|
|
}
|
|
|
|
// This is for the REPL's resetblocks command.
|
|
func (root *RootNode) ResetEndBlocksForREPL() {
|
|
root.endBlocks = []*StatementBlockNode{}
|
|
}
|