From 6ac72974eb8b48f791aacd6d40b9a69097d5a696 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Fri, 6 Jun 2025 00:32:13 -0400 Subject: [PATCH] lang: ast, interfaces: Move textarea to a common package We're going to use it everywhere. We also make it more forgiving in the meanwhile while we're porting things over. --- lang/ast/structs.go | 84 +++++++++------ lang/ast/util.go | 155 ---------------------------- lang/interfaces/textarea.go | 198 ++++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 184 deletions(-) create mode 100644 lang/interfaces/textarea.go diff --git a/lang/ast/structs.go b/lang/ast/structs.go index c6cbe999..c9f0a1ed 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -183,7 +183,8 @@ var ( // StmtBind is a representation of an assignment, which binds a variable to an // expression. type StmtBind struct { - Textarea + interfaces.Textarea + data *interfaces.Data Ident string @@ -378,7 +379,8 @@ func (obj *StmtBind) Output(map[interfaces.Func]types.Value) (*interfaces.Output // TODO: Consider expanding Name to have this return a list of Res's in the // Output function if it is a map[name]struct{}, or even a map[[]name]struct{}. type StmtRes struct { - Textarea + interfaces.Textarea + data *interfaces.Data Kind string // kind of resource, eg: pkg, file, svc, etc... @@ -1594,7 +1596,8 @@ type StmtResContents interface { // StmtResField represents a single field in the parsed resource representation. // This does not satisfy the Stmt interface. type StmtResField struct { - Textarea + interfaces.Textarea + data *interfaces.Data Field string @@ -1879,7 +1882,8 @@ func (obj *StmtResField) Graph(env *interfaces.Env) (*pgraph.Graph, error) { // StmtResEdge represents a single edge property in the parsed resource // representation. This does not satisfy the Stmt interface. type StmtResEdge struct { - Textarea + interfaces.Textarea + data *interfaces.Data Property string // TODO: iota constant instead? @@ -2129,7 +2133,8 @@ func (obj *StmtResEdge) Graph(env *interfaces.Env) (*pgraph.Graph, error) { // correspond to the particular meta parameter specified. This does not satisfy // the Stmt interface. type StmtResMeta struct { - Textarea + interfaces.Textarea + data *interfaces.Data Property string // TODO: iota constant instead? @@ -2683,7 +2688,8 @@ func (obj *StmtResCollect) Graph(env *interfaces.Env) (*pgraph.Graph, error) { // names are compatible and listed. In this case of Send/Recv, only lists of // length two are legal. type StmtEdge struct { - Textarea + interfaces.Textarea + data *interfaces.Data EdgeHalfList []*StmtEdgeHalf // represents a chain of edges @@ -3032,7 +3038,8 @@ func (obj *StmtEdge) Output(table map[interfaces.Func]types.Value) (*interfaces. // is assumed that a list of strings should be expected. More mechanisms to // determine if the value is static may be added over time. type StmtEdgeHalf struct { - Textarea + interfaces.Textarea + data *interfaces.Data Kind string // kind of resource, eg: pkg, file, svc, etc... @@ -3203,7 +3210,8 @@ func (obj *StmtEdgeHalf) Graph(env *interfaces.Env) (*pgraph.Graph, error) { // optional, it is the else branch, although this struct allows either to be // optional, even if it is not commonly used. type StmtIf struct { - Textarea + interfaces.Textarea + data *interfaces.Data Condition interfaces.Expr @@ -3569,7 +3577,8 @@ func (obj *StmtIf) Output(table map[interfaces.Func]types.Value) (*interfaces.Ou // StmtFor represents an iteration over a list. The body contains statements. type StmtFor struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -4077,7 +4086,8 @@ func (obj *StmtFor) Output(table map[interfaces.Func]types.Value) (*interfaces.O // StmtForKV represents an iteration over a map. The body contains statements. type StmtForKV struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -4583,7 +4593,8 @@ func (obj *StmtForKV) Output(table map[interfaces.Func]types.Value) (*interfaces // the bind statement's are correctly applied in this scope, and irrespective of // their order of definition. type StmtProg struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for use by imports @@ -6177,7 +6188,8 @@ func (obj *StmtProg) IsModuleUnsafe() error { // TODO: rename this function? // the supplied function in the current scope and irrespective of the order of // definition. type StmtFunc struct { - Textarea + interfaces.Textarea + data *interfaces.Data Name string @@ -6391,7 +6403,8 @@ func (obj *StmtFunc) Output(map[interfaces.Func]types.Value) (*interfaces.Output // TODO: We don't currently support defining polymorphic classes (eg: different // signatures for the same class name) but it might be something to consider. type StmtClass struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -6601,7 +6614,8 @@ func (obj *StmtClass) Output(table map[interfaces.Func]types.Value) (*interfaces // to call a class except that it produces output instead of a value. Most of // the interesting logic for classes happens here or in StmtProg. type StmtInclude struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope @@ -7099,7 +7113,8 @@ func (obj *StmtInclude) Output(table map[interfaces.Func]types.Value) (*interfac // file. As with any statement, it produces output, but that output is empty. To // benefit from its inclusion, reference the scope definitions you want. type StmtImport struct { - Textarea + interfaces.Textarea + data *interfaces.Data Name string @@ -7208,7 +7223,7 @@ func (obj *StmtImport) Output(map[interfaces.Func]types.Value) (*interfaces.Outp // formatting) but so that they can exist anywhere in the code. Currently these // are dropped by the lexer. type StmtComment struct { - Textarea + interfaces.Textarea Value string } @@ -7290,7 +7305,8 @@ func (obj *StmtComment) Output(map[interfaces.Func]types.Value) (*interfaces.Out // ExprBool is a representation of a boolean. type ExprBool struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -7443,7 +7459,8 @@ func (obj *ExprBool) Value() (types.Value, error) { // ExprStr is a representation of a string. type ExprStr struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -7645,7 +7662,8 @@ func (obj *ExprStr) Value() (types.Value, error) { // ExprInt is a representation of an int. type ExprInt struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -7797,7 +7815,8 @@ func (obj *ExprInt) Value() (types.Value, error) { // ExprFloat is a representation of a float. type ExprFloat struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -7951,7 +7970,8 @@ func (obj *ExprFloat) Value() (types.Value, error) { // ExprList is a representation of a list. type ExprList struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -8315,7 +8335,8 @@ func (obj *ExprList) Value() (types.Value, error) { // ExprMap is a representation of a (dictionary) map. type ExprMap struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -8804,7 +8825,7 @@ func (obj *ExprMap) Value() (types.Value, error) { // ExprMapKV represents a key and value pair in a (dictionary) map. This does // not satisfy the Expr interface. type ExprMapKV struct { - Textarea + interfaces.Textarea Key interfaces.Expr // keys can be strings, int's, etc... Val interfaces.Expr @@ -8812,7 +8833,8 @@ type ExprMapKV struct { // ExprStruct is a representation of a struct. type ExprStruct struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -9208,7 +9230,7 @@ func (obj *ExprStruct) Value() (types.Value, error) { // ExprStructField represents a name value pair in a struct field. This does not // satisfy the Expr interface. type ExprStructField struct { - Textarea + interfaces.Textarea Name string Value interfaces.Expr @@ -9224,7 +9246,8 @@ type ExprStructField struct { // 4. A pure built-in function (set Values to a singleton) // 5. A pure polymorphic built-in function (set Values to a list) type ExprFunc struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -10139,7 +10162,8 @@ func (obj *ExprFunc) Value() (types.Value, error) { // declaration or implementation of a new function value. This struct has an // analogous symmetry with ExprVar. type ExprCall struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -11118,7 +11142,8 @@ func (obj *ExprCall) Value() (types.Value, error) { // ExprVar is a representation of a variable lookup. It returns the expression // that that variable refers to. type ExprVar struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -12322,7 +12347,8 @@ func (obj *ExprSingleton) Value() (types.Value, error) { // returns a value. As a result, it has a type. This is different from a StmtIf, // which does not need to have both branches, and which does not return a value. type ExprIf struct { - Textarea + interfaces.Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type diff --git a/lang/ast/util.go b/lang/ast/util.go index e676ef10..b07bfdc4 100644 --- a/lang/ast/util.go +++ b/lang/ast/util.go @@ -31,7 +31,6 @@ package ast import ( "fmt" - "os" "sort" "strings" "sync" @@ -41,7 +40,6 @@ import ( "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -589,156 +587,3 @@ func highlightHelper(node interfaces.Node, logf func(format string, v ...interfa return err } - -// Textarea stores the coordinates of a statement or expression in the form of a -// starting line/column and ending line/column. -type Textarea struct { - // debug represents if we're running in debug mode or not. - debug bool - - // logf is a logger which should be used. - logf func(format string, v ...interface{}) - - // sf is the SourceFinder function implementation that maps a filename - // to the source. - sf interfaces.SourceFinderFunc - - // path is the full path/filename where this text area exists. - path string - - // This data is zero-based. (Eg: first line of file is 0) - startLine int // first - startColumn int // left - endLine int // last - endColumn int // right - - isSet bool - - // Bug5819 works around issue https://github.com/golang/go/issues/5819 - Bug5819 interface{} // XXX: workaround -} - -// Setup is used during AST initialization in order to store in each AST node -// the name of the source file from which it was generated. -func (obj *Textarea) Setup(data *interfaces.Data) { - obj.debug = data.Debug - obj.logf = data.Logf - obj.sf = data.SourceFinder - obj.path = data.AbsFilename() -} - -// IsSet returns if the position was already set with Locate already. -func (obj *Textarea) IsSet() bool { - return obj.isSet -} - -// Locate is used by the parser to store the token positions in AST nodes. The -// path will be filled during AST node initialization usually, because the -// parser does not know the name of the file it is processing. -func (obj *Textarea) Locate(line int, col int, endline int, endcol int) { - obj.startLine = line - obj.startColumn = col - obj.endLine = endline - obj.endColumn = endcol - obj.isSet = true -} - -// Pos returns the starting line/column of an AST node. -func (obj *Textarea) Pos() (int, int) { - return obj.startLine, obj.startColumn -} - -// End returns the end line/column of an AST node. -func (obj *Textarea) End() (int, int) { - return obj.endLine, obj.endColumn -} - -// Path returns the name of the source file that holds the code for an AST node. -func (obj *Textarea) Path() string { - return obj.path -} - -// Filename returns the printable filename that we'd like to display. It tries -// to return a relative version if possible. -func (obj *Textarea) Filename() string { - if obj.path == "" { - return "" // TODO: should this be ? - } - - wd, _ := os.Getwd() // ignore error since "" would just pass through - wd += "/" // it's a dir - if s, err := util.RemoveBasePath(obj.path, wd); err == nil { - return s - } - - return obj.path -} - -// Byline gives a succinct representation of the Textarea, but is useful only in -// debugging. In order to generate pretty error messages, see HighlightText. -func (obj *Textarea) Byline() string { - // We convert to 1-based for user display. - return fmt.Sprintf("%s @ %d:%d-%d:%d", obj.Filename(), obj.startLine+1, obj.startColumn+1, obj.endLine+1, obj.endColumn+1) -} - -// HighlightText generates a generic description that just visually indicates -// part of the line described by a Textarea. If the coordinates that are passed -// span multiple lines, don't show those lines, but just a description of the -// area. If it can't generate a valid snippet, then it returns the empty string. -func (obj *Textarea) HighlightText() string { - b, err := obj.sf(obj.path) // source finder! - if err != nil { - return "" - } - contents := string(b) - - result := &strings.Builder{} - - result.WriteString(obj.Byline()) - - lines := strings.Split(contents, "\n") - if len(lines) < obj.endLine-1 { - // XXX: out of bounds? - return "" - } - - result.WriteString("\n\n") - - if obj.startLine == obj.endLine { - line := lines[obj.startLine] + "\n" - text := strings.TrimLeft(line, " \t") - indent := strings.TrimSuffix(line, text) - offset := len(indent) - - result.WriteString(line) - result.WriteString(indent) - result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) - // TODO: add on the width of the second element as well - result.WriteString(strings.Repeat("^", obj.endColumn-obj.startColumn+1)) - result.WriteString("\n") - - return result.String() - } - - line := lines[obj.startLine] + "\n" - text := strings.TrimLeft(line, " \t") - indent := strings.TrimSuffix(line, text) - offset := len(indent) - - result.WriteString(line) - result.WriteString(indent) - result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) - result.WriteString("^ from here ...\n") - - line = lines[obj.endLine] + "\n" - text = strings.TrimLeft(line, " \t") - indent = strings.TrimSuffix(line, text) - offset = len(indent) - - result.WriteString(line) - result.WriteString(indent) - result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) - result.WriteString("^ ... to here\n") - - return result.String() -} diff --git a/lang/interfaces/textarea.go b/lang/interfaces/textarea.go new file mode 100644 index 00000000..9f0a83a6 --- /dev/null +++ b/lang/interfaces/textarea.go @@ -0,0 +1,198 @@ +// Mgmt +// Copyright (C) James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package interfaces + +import ( + "fmt" + "os" + "strings" + + "github.com/purpleidea/mgmt/util" +) + +// Textarea stores the coordinates of a statement or expression in the form of a +// starting line/column and ending line/column. +type Textarea struct { + // debug represents if we're running in debug mode or not. + debug bool + + // logf is a logger which should be used. + logf func(format string, v ...interface{}) + + // sf is the SourceFinder function implementation that maps a filename + // to the source. + sf SourceFinderFunc + + // path is the full path/filename where this text area exists. + path string + + // This data is zero-based. (Eg: first line of file is 0) + startLine int // first + startColumn int // left + endLine int // last + endColumn int // right + + isSet bool + + // Bug5819 works around issue https://github.com/golang/go/issues/5819 + Bug5819 interface{} // XXX: workaround +} + +// Setup is used during AST initialization in order to store in each AST node +// the name of the source file from which it was generated. +func (obj *Textarea) Setup(data *Data) { + obj.debug = data.Debug + obj.logf = data.Logf + obj.sf = data.SourceFinder + obj.path = data.AbsFilename() +} + +// IsSet returns if the position was already set with Locate already. +func (obj *Textarea) IsSet() bool { + return obj.isSet +} + +// Locate is used by the parser to store the token positions in AST nodes. The +// path will be filled during AST node initialization usually, because the +// parser does not know the name of the file it is processing. +func (obj *Textarea) Locate(line int, col int, endline int, endcol int) { + obj.startLine = line + obj.startColumn = col + obj.endLine = endline + obj.endColumn = endcol + obj.isSet = true +} + +// Pos returns the starting line/column of an AST node. +func (obj *Textarea) Pos() (int, int) { + return obj.startLine, obj.startColumn +} + +// End returns the end line/column of an AST node. +func (obj *Textarea) End() (int, int) { + return obj.endLine, obj.endColumn +} + +// Path returns the name of the source file that holds the code for an AST node. +func (obj *Textarea) Path() string { + return obj.path +} + +// Filename returns the printable filename that we'd like to display. It tries +// to return a relative version if possible. +func (obj *Textarea) Filename() string { + if obj.path == "" { + return "" // TODO: should this be ? + } + + wd, _ := os.Getwd() // ignore error since "" would just pass through + wd += "/" // it's a dir + if s, err := util.RemoveBasePath(obj.path, wd); err == nil { + return s + } + + return obj.path +} + +// Byline gives a succinct representation of the Textarea, but is useful only in +// debugging. In order to generate pretty error messages, see HighlightText. +func (obj *Textarea) Byline() string { + // We convert to 1-based for user display. + return fmt.Sprintf("%s @ %d:%d-%d:%d", obj.Filename(), obj.startLine+1, obj.startColumn+1, obj.endLine+1, obj.endColumn+1) +} + +// HighlightText generates a generic description that just visually indicates +// part of the line described by a Textarea. If the coordinates that are passed +// span multiple lines, don't show those lines, but just a description of the +// area. If it can't generate a valid snippet, then it returns the empty string. +func (obj *Textarea) HighlightText() string { + if obj.sf == nil { + // XXX: when all functions are ported over to use ast.Textarea, + // then uncomment this return and add in the panic below. + return "" // XXX: temporary + // programming error + //panic("nil SourceFinderFunc") + } + b, err := obj.sf(obj.path) // source finder! + if err != nil { + return "" + } + contents := string(b) + + result := &strings.Builder{} + + result.WriteString(obj.Byline()) + + lines := strings.Split(contents, "\n") + if len(lines) < obj.endLine-1 { + // XXX: out of bounds? + return "" + } + + result.WriteString("\n\n") + + if obj.startLine == obj.endLine { + line := lines[obj.startLine] + "\n" + text := strings.TrimLeft(line, " \t") + indent := strings.TrimSuffix(line, text) + offset := len(indent) + + result.WriteString(line) + result.WriteString(indent) + result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) + // TODO: add on the width of the second element as well + result.WriteString(strings.Repeat("^", obj.endColumn-obj.startColumn+1)) + result.WriteString("\n") + + return result.String() + } + + line := lines[obj.startLine] + "\n" + text := strings.TrimLeft(line, " \t") + indent := strings.TrimSuffix(line, text) + offset := len(indent) + + result.WriteString(line) + result.WriteString(indent) + result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) + result.WriteString("^ from here ...\n") + + line = lines[obj.endLine] + "\n" + text = strings.TrimLeft(line, " \t") + indent = strings.TrimSuffix(line, text) + offset = len(indent) + + result.WriteString(line) + result.WriteString(indent) + result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) + result.WriteString("^ ... to here\n") + + return result.String() +}