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 }