go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/docgen/symbols/symbols.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package symbols defines a data model representing Starlark symbols.
    16  //
    17  // A symbol is a like a variable: it has a name and points to some object
    18  // somewhere. This package allows to load symbols defined in a starlark module,
    19  // following references. For example, if "a = b", then symbol 'a' points to the
    20  // same object as symbol 'b'.
    21  //
    22  // The loader understands how to follow references across module boundaries and
    23  // struct()s.
    24  package symbols
    25  
    26  import (
    27  	"fmt"
    28  
    29  	"go.chromium.org/luci/starlark/docgen/ast"
    30  	"go.chromium.org/luci/starlark/docgen/docstring"
    31  )
    32  
    33  // Symbol is something defined in a Starlark module.
    34  //
    35  // It has a name and it points to some declaration.
    36  type Symbol interface {
    37  	// Name is a name of this symbol within its parent namespace.
    38  	//
    39  	// E.g. this is just "a", not "parent.a".
    40  	Name() string
    41  
    42  	// Def is an AST node where the object this symbol points to was defined.
    43  	//
    44  	// Nil for broken symbols.
    45  	Def() ast.Node
    46  
    47  	// Doc is a parsed docstring for this symbol.
    48  	Doc() *docstring.Parsed
    49  }
    50  
    51  // symbol is common base for different types of symbols.
    52  //
    53  // Implements Symbol interface for them.
    54  type symbol struct {
    55  	name string
    56  	def  ast.Node
    57  	doc  *docstring.Parsed
    58  }
    59  
    60  func (s *symbol) Name() string  { return s.name }
    61  func (s *symbol) Def() ast.Node { return s.def }
    62  
    63  func (s *symbol) Doc() *docstring.Parsed {
    64  	if s.doc == nil {
    65  		if s.def != nil {
    66  			s.doc = docstring.Parse(s.def.Doc())
    67  		} else {
    68  			s.doc = &docstring.Parsed{Description: "broken"}
    69  		}
    70  	}
    71  	return s.doc
    72  }
    73  
    74  func (s *symbol) String() string {
    75  	node := s.Def()
    76  	pos, _ := node.Span()
    77  	return fmt.Sprintf("%s = %s %T at %s", s.name, node.Name(), node, pos)
    78  }
    79  
    80  // BrokenSymbol is a symbol that refers to something we can't resolve.
    81  //
    82  // For example, if "b" is undefined in "a = b", then "a" becomes BrokenSymbol.
    83  type BrokenSymbol struct {
    84  	symbol
    85  }
    86  
    87  // newBrokenSymbol returns a new broken symbol with the given name.
    88  func newBrokenSymbol(name string) *BrokenSymbol {
    89  	return &BrokenSymbol{
    90  		symbol: symbol{
    91  			name: name,
    92  		},
    93  	}
    94  }
    95  
    96  // Term is a symbol that represents some single terminal definition, not a
    97  // struct nor a function invocation.
    98  type Term struct {
    99  	symbol
   100  }
   101  
   102  // newTerm returns a new Term symbol.
   103  func newTerm(name string, def ast.Node) *Term {
   104  	return &Term{
   105  		symbol: symbol{
   106  			name: name,
   107  			def:  def,
   108  		},
   109  	}
   110  }
   111  
   112  // Invocation is a symbol assigned a return value of some function call.
   113  //
   114  // The name of the function, as well as value of all keyword arguments, are
   115  // represented by symbols too.
   116  type Invocation struct {
   117  	symbol
   118  
   119  	fn   Symbol
   120  	args []Symbol
   121  }
   122  
   123  // newInvocation returns a new Invocation symbol.
   124  func newInvocation(name string, def ast.Node, fn Symbol, args []Symbol) *Invocation {
   125  	return &Invocation{
   126  		symbol: symbol{
   127  			name: name,
   128  			def:  def,
   129  		},
   130  		fn:   fn,
   131  		args: args,
   132  	}
   133  }
   134  
   135  // Func is a symbol that represents the function being invoked.
   136  func (inv *Invocation) Func() Symbol { return inv.fn }
   137  
   138  // Args is keyword arguments passed to the function.
   139  func (inv *Invocation) Args() []Symbol { return inv.args }
   140  
   141  // Struct is a symbol that represents a struct (or equivalent) that has more
   142  // symbols inside it.
   143  //
   144  // Basically, a struct symbol is something that supports "." operation to
   145  // "look" inside it.
   146  type Struct struct {
   147  	symbol
   148  
   149  	// symbols is a list of symbols inside this struct.
   150  	symbols []Symbol
   151  	// frozen is true if 'symbols' must not be modified anymore.
   152  	frozen bool
   153  }
   154  
   155  // newStruct returns a new struct with empty list of symbols inside.
   156  //
   157  // The caller then can populate it via AddSymbol and finalize with Freeze when
   158  // done.
   159  func newStruct(name string, def ast.Node) *Struct {
   160  	return &Struct{
   161  		symbol: symbol{
   162  			name: name,
   163  			def:  def,
   164  		},
   165  	}
   166  }
   167  
   168  // addSymbol appends a symbol to the symbols list in the struct.
   169  //
   170  // Note that starlark forbids reassigning variables in the module scope, so we
   171  // don't check that 'sym' wasn't added before.
   172  //
   173  // Panics if the struct is frozen.
   174  func (s *Struct) addSymbol(sym Symbol) {
   175  	if s.frozen {
   176  		panic("frozen")
   177  	}
   178  	s.symbols = append(s.symbols, sym)
   179  }
   180  
   181  // freeze makes the struct immutable.
   182  func (s *Struct) freeze() {
   183  	s.frozen = true
   184  }
   185  
   186  // Symbols returns all symbols in the struct.
   187  //
   188  // The caller must not modify the returned slice.
   189  func (s *Struct) Symbols() []Symbol {
   190  	return s.symbols
   191  }
   192  
   193  // Transform returns a new struct made by applying a transformation to the
   194  // receiver struct, recursively.
   195  func (s *Struct) Transform(tr func(Symbol) (Symbol, error)) (*Struct, error) {
   196  	out := &Struct{
   197  		symbol:  s.symbol,
   198  		symbols: make([]Symbol, 0, len(s.symbols)),
   199  	}
   200  	for _, sym := range s.symbols {
   201  		// Recursive branch.
   202  		if strct, ok := sym.(*Struct); ok {
   203  			t, err := strct.Transform(tr)
   204  			if err != nil {
   205  				return nil, err
   206  			}
   207  			out.symbols = append(out.symbols, t)
   208  			continue
   209  		}
   210  		// Leafs.
   211  		switch t, err := tr(sym); {
   212  		case err != nil:
   213  			return nil, err
   214  		case t != nil:
   215  			out.symbols = append(out.symbols, t)
   216  		}
   217  	}
   218  	out.frozen = true
   219  	return out, nil
   220  }
   221  
   222  // Lookup essentially does "ns.p0.p1.p2" operation.
   223  //
   224  // Returns a broken symbol if this lookup is not possible, e.g. some field path
   225  // element is not a struct or doesn't exist at all.
   226  func Lookup(ns Symbol, path ...string) Symbol {
   227  	cur := ns
   228  	for _, p := range path {
   229  		var next Symbol
   230  		if strct, _ := cur.(*Struct); strct != nil {
   231  			for _, sym := range strct.symbols {
   232  				if sym.Name() == p {
   233  					next = sym
   234  					break
   235  				}
   236  			}
   237  		}
   238  		if next == nil {
   239  			return newBrokenSymbol(p)
   240  		}
   241  		cur = next
   242  	}
   243  	return cur
   244  }
   245  
   246  // NewAlias handles definitions like "a = <symbol>".
   247  //
   248  // It returns a new symbol of the same type as the RHS and new name ('a'). It
   249  // points to the same definition the symbol on the RHS points to.
   250  func NewAlias(name string, symbol Symbol) Symbol {
   251  	switch s := symbol.(type) {
   252  	case *BrokenSymbol:
   253  		return newBrokenSymbol(name)
   254  	case *Term:
   255  		return newTerm(name, s.Def())
   256  	case *Invocation:
   257  		return newInvocation(name, s.Def(), s.fn, s.args)
   258  	case *Struct:
   259  		// Structs are copied by value too, but we check they are frozen at this
   260  		// point, so it should be fine. It is possible the struct is not frozen yet
   261  		// in the following self-referential case: 'a = {}; a = struct(k = a)'. But
   262  		// it is not allowed by starlark (redefinitions are forbidden). We still
   263  		// cautiously treat this case as broken.
   264  		if !s.frozen {
   265  			return newBrokenSymbol(name)
   266  		}
   267  		strct := newStruct(name, s.Def())
   268  		strct.symbols = s.symbols // it is immutable, copying the pointer is fine
   269  		strct.freeze()
   270  		return strct
   271  	default:
   272  		panic(fmt.Sprintf("unrecognized symbol type %T", s))
   273  	}
   274  }