github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/parse/asp/parser.go (about)

     1  // Package asp implements an experimental BUILD-language parser.
     2  // Parsing is doing using Participle (github.com/alecthomas/participle) in native Go,
     3  // with a custom and also native partial Python interpreter.
     4  package asp
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/gob"
     9  	"io"
    10  	"os"
    11  	"reflect"
    12  	"strings"
    13  
    14  	"gopkg.in/op/go-logging.v1"
    15  
    16  	"core"
    17  )
    18  
    19  var log = logging.MustGetLogger("asp")
    20  
    21  func init() {
    22  	// gob needs to know how to encode and decode our types.
    23  	gob.Register(None)
    24  	gob.Register(pyInt(0))
    25  	gob.Register(pyString(""))
    26  	gob.Register(pyList{})
    27  	gob.Register(pyDict{})
    28  }
    29  
    30  // A Parser implements parsing of BUILD files.
    31  type Parser struct {
    32  	interpreter *interpreter
    33  	// Stashed set of source code for builtin rules.
    34  	builtins map[string][]byte
    35  }
    36  
    37  // NewParser creates a new parser instance. One is normally sufficient for a process lifetime.
    38  func NewParser(state *core.BuildState) *Parser {
    39  	p := newParser()
    40  	p.interpreter = newInterpreter(state, p)
    41  	return p
    42  }
    43  
    44  // newParser creates just the parser with no interpreter.
    45  func newParser() *Parser {
    46  	return &Parser{builtins: map[string][]byte{}}
    47  }
    48  
    49  // LoadBuiltins instructs the parser to load rules from this file as built-ins.
    50  // Optionally the file contents can be supplied directly.
    51  // Also optionally a previously parsed form (acquired from ParseToFile) can be supplied.
    52  func (p *Parser) LoadBuiltins(filename string, contents, encoded []byte) error {
    53  	var statements []*Statement
    54  	if len(encoded) != 0 {
    55  		decoder := gob.NewDecoder(bytes.NewReader(encoded))
    56  		if err := decoder.Decode(&statements); err != nil {
    57  			log.Fatalf("Failed to decode pre-parsed rules: %s", err)
    58  		}
    59  	}
    60  	if len(contents) != 0 {
    61  		p.builtins[filename] = contents
    62  	}
    63  	if err := p.interpreter.LoadBuiltins(filename, contents, statements); err != nil {
    64  		return p.annotate(err, nil)
    65  	}
    66  	return nil
    67  }
    68  
    69  // MustLoadBuiltins calls LoadBuiltins, and dies on any errors.
    70  func (p *Parser) MustLoadBuiltins(filename string, contents, encoded []byte) {
    71  	if err := p.LoadBuiltins(filename, contents, encoded); err != nil {
    72  		log.Fatalf("Error loading builtin rules: %s", err)
    73  	}
    74  }
    75  
    76  // ParseFile parses the contents of a single file in the BUILD language.
    77  // It returns true if the call was deferred at some point awaiting  target to build,
    78  // along with any error encountered.
    79  func (p *Parser) ParseFile(pkg *core.Package, filename string) error {
    80  	statements, err := p.parse(filename)
    81  	if err != nil {
    82  		return err
    83  	}
    84  	_, err = p.interpreter.interpretAll(pkg, statements)
    85  	if err != nil {
    86  		f, _ := os.Open(filename)
    87  		p.annotate(err, f)
    88  	}
    89  	return err
    90  }
    91  
    92  // ParseReader parses the contents of the given ReadSeeker as a BUILD file.
    93  // This is provided as a helper for fuzzing and isn't generally useful otherwise.
    94  // The first return value is true if parsing succeeds - if the error is still non-nil
    95  // that indicates that interpretation failed.
    96  func (p *Parser) ParseReader(pkg *core.Package, r io.ReadSeeker) (bool, error) {
    97  	stmts, err := p.parseAndHandleErrors(r, "")
    98  	if err != nil {
    99  		return false, err
   100  	}
   101  	_, err = p.interpreter.interpretAll(pkg, stmts)
   102  	return true, err
   103  }
   104  
   105  // ParseToFile parses the given file and writes a binary form of the result to the output file.
   106  func (p *Parser) ParseToFile(input, output string) error {
   107  	stmts, err := p.parse(input)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	stmts = p.optimise(stmts)
   112  	p.interpreter.optimiseExpressions(reflect.ValueOf(stmts))
   113  	for _, stmt := range stmts {
   114  		if stmt.FuncDef != nil {
   115  			stmt.FuncDef.KeywordsOnly = !whitelistedKwargs(stmt.FuncDef.Name, input)
   116  		}
   117  	}
   118  	f, err := os.Create(output)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	encoder := gob.NewEncoder(f)
   123  	if err := encoder.Encode(stmts); err != nil {
   124  		return err
   125  	}
   126  	return f.Close()
   127  }
   128  
   129  // ParseFileOnly parses the given file but does not interpret it.
   130  func (p *Parser) ParseFileOnly(filename string) ([]*Statement, error) {
   131  	return p.parse(filename)
   132  }
   133  
   134  // parse reads the given file and parses it into a set of statements.
   135  func (p *Parser) parse(filename string) ([]*Statement, error) {
   136  	f, err := os.Open(filename)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	stmts, err := p.parseAndHandleErrors(f, filename)
   141  	if err == nil {
   142  		// This appears a bit weird, but the error will still use the file if it's open
   143  		// to print additional information about it.
   144  		f.Close()
   145  	}
   146  	return stmts, err
   147  }
   148  
   149  // ParseData reads the given byteslice and parses it into a set of statements.
   150  // The 'filename' argument is only used in case of errors so doesn't necessarily have to correspond to a real file.
   151  func (p *Parser) ParseData(data []byte, filename string) ([]*Statement, error) {
   152  	r := &namedReader{r: bytes.NewReader(data), name: filename}
   153  	return p.parseAndHandleErrors(r, filename)
   154  }
   155  
   156  // parseAndHandleErrors handles errors nicely if the given input fails to parse.
   157  func (p *Parser) parseAndHandleErrors(r io.ReadSeeker, filename string) ([]*Statement, error) {
   158  	input, err := parseFileInput(r)
   159  	if err == nil {
   160  		return input.Statements, nil
   161  	}
   162  	// If we get here, something went wrong. Try to give some nice feedback about it.
   163  	return nil, p.annotate(err, r)
   164  }
   165  
   166  // annotate annotates the given error with whatever source information we have.
   167  func (p *Parser) annotate(err error, r io.ReadSeeker) error {
   168  	err = AddReader(err, r)
   169  	// Now annotate with any builtin rules we might have loaded.
   170  	for filename, contents := range p.builtins {
   171  		err = AddReader(err, &namedReader{r: bytes.NewReader(contents), name: filename})
   172  	}
   173  	return err
   174  }
   175  
   176  // optimise implements some (very) mild optimisations on the given set of statements to translate them
   177  // into a form we find slightly more useful.
   178  // This also sneaks in some rewrites to .append and .extend which are very troublesome otherwise
   179  // (technically that changes the meaning of the code, #dealwithit)
   180  func (p *Parser) optimise(statements []*Statement) []*Statement {
   181  	ret := make([]*Statement, 0, len(statements))
   182  	for _, stmt := range statements {
   183  		if stmt.Literal != nil || stmt.Pass {
   184  			continue // Neither statement has any effect.
   185  		} else if stmt.FuncDef != nil {
   186  			stmt.FuncDef.Statements = p.optimise(stmt.FuncDef.Statements)
   187  		} else if stmt.For != nil {
   188  			stmt.For.Statements = p.optimise(stmt.For.Statements)
   189  		} else if stmt.If != nil {
   190  			stmt.If.Statements = p.optimise(stmt.If.Statements)
   191  			for i, elif := range stmt.If.Elif {
   192  				stmt.If.Elif[i].Statements = p.optimise(elif.Statements)
   193  			}
   194  			stmt.If.ElseStatements = p.optimise(stmt.If.ElseStatements)
   195  		} else if stmt.Ident != nil && stmt.Ident.Action != nil && stmt.Ident.Action.Property != nil && len(stmt.Ident.Action.Property.Action) == 1 {
   196  			call := stmt.Ident.Action.Property.Action[0].Call
   197  			name := stmt.Ident.Action.Property.Name
   198  			if (name == "append" || name == "extend") && call != nil && len(call.Arguments) == 1 {
   199  				stmt = &Statement{
   200  					Pos: stmt.Pos,
   201  					Ident: &IdentStatement{
   202  						Name: stmt.Ident.Name,
   203  						Action: &IdentStatementAction{
   204  							AugAssign: &call.Arguments[0].Value,
   205  						},
   206  					},
   207  				}
   208  				if name == "append" {
   209  					stmt.Ident.Action.AugAssign = &Expression{Val: &ValueExpression{
   210  						List: &List{
   211  							Values: []*Expression{&call.Arguments[0].Value},
   212  						},
   213  					}}
   214  				}
   215  			}
   216  		}
   217  		ret = append(ret, stmt)
   218  	}
   219  	return ret
   220  }
   221  
   222  // whitelistedKwargs returns true if the given built-in function name is allowed to
   223  // be called as non-kwargs.
   224  // TODO(peterebden): Come up with a syntax that exposes this directly in the file.
   225  func whitelistedKwargs(name, filename string) bool {
   226  	if name[0] == '_' || (strings.HasSuffix(filename, "builtins.build_defs") && name != "build_rule") {
   227  		return true // Don't care about anything private, or non-rule builtins.
   228  	}
   229  	return map[string]bool{
   230  		"workspace":    true,
   231  		"decompose":    true,
   232  		"check_config": true,
   233  		"select":       true,
   234  	}[name]
   235  }