go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/docgen/ast/parser_test.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 ast 16 17 import ( 18 "fmt" 19 "io" 20 "strings" 21 "testing" 22 23 "go.starlark.net/syntax" 24 25 . "github.com/smartystreets/goconvey/convey" 26 ) 27 28 const goodInput = ` 29 # Copyright comment or something. 30 31 """Module doc string. 32 33 Multiline. 34 """ 35 36 # Load comment. 37 load('@stdlib//another.star', 'ext1', ext2='ext_name') 38 39 # Skipped comment. 40 41 # Function comment. 42 # 43 # With indent. 44 # 45 # More. 46 def func1(*, a, b, c=None, **kwargs): 47 """Doc string. 48 49 Multiline. 50 """ 51 body 52 53 # No docstring 54 def func2(): 55 pass 56 57 # Weird doc string: number instead of a string. 58 def func3(): 59 42 60 61 const_int = 123 62 const_str = 'str' 63 64 ellipsis = 1 + 2 65 66 alias1 = func1 67 alias2 = ext2.deeper.deep 68 69 decl = namespace.declare( 70 skipped, 71 arg1 = 123, 72 arg2 = 1 + 1, 73 arg3 = func1, 74 arg4 = ext2.deeper.deep, 75 arg5 = nested(nested_arg = 1), 76 ) 77 78 # Struct comment. 79 struct_stuff = struct( 80 const = 123, 81 stuff = 1+2+3, 82 # Key comment 1. 83 key1 = func1, 84 key2 = ext2.deeper.deep, 85 # Nested namespace. 86 nested = struct(key1=v1, key2=v2), 87 88 **skipped 89 ) 90 91 skipped, skipped = a, b 92 skipped += 123 93 ` 94 95 const expectedDumpOutput = `mod.star = module 96 ext1 -> ext1 in @stdlib//another.star 97 ext2 -> ext_name in @stdlib//another.star 98 func1 = func 99 func2 = func 100 func3 = func 101 const_int = 123 102 const_str = str 103 ellipsis = ... 104 alias1 = func1 105 alias2 = ext2.deeper.deep 106 decl = namespace.declare(...) 107 arg1 = 123 108 arg2 = ... 109 arg3 = func1 110 arg4 = ext2.deeper.deep 111 arg5 = nested(...) 112 nested_arg = 1 113 struct_stuff = namespace 114 const = 123 115 stuff = ... 116 key1 = func1 117 key2 = ext2.deeper.deep 118 nested = namespace 119 key1 = v1 120 key2 = v2 121 ` 122 123 func TestParseModule(t *testing.T) { 124 t.Parallel() 125 126 Convey("Works", t, func() { 127 mod, err := ParseModule("mod.star", goodInput, func(s string) (string, error) { return s, nil }) 128 So(err, ShouldBeNil) 129 buf := strings.Builder{} 130 dumpTree(mod, &buf, "") 131 So(buf.String(), ShouldEqual, expectedDumpOutput) 132 133 Convey("Docstrings extraction", func() { 134 So(mod.Doc(), ShouldEqual, "Module doc string.\n\nMultiline.\n") 135 So( 136 nodeByName(mod, "func1").Doc(), ShouldEqual, 137 "Doc string.\n\n Multiline.\n ", 138 ) 139 }) 140 141 Convey("Spans extraction", func() { 142 l, r := nodeByName(mod, "func1").Span() 143 So(l.Filename(), ShouldEqual, "mod.star") 144 So(r.Filename(), ShouldEqual, "mod.star") 145 146 // Spans the entire function definition. 147 So(extractSpan(goodInput, l, r), ShouldEqual, `def func1(*, a, b, c=None, **kwargs): 148 """Doc string. 149 150 Multiline. 151 """ 152 body`) 153 }) 154 155 Convey("Comments extraction", func() { 156 So(nodeByName(mod, "func1").Comments(), ShouldEqual, `Function comment. 157 158 With indent. 159 160 More.`) 161 162 strct := nodeByName(mod, "struct_stuff").(*Namespace) 163 So(strct.Comments(), ShouldEqual, "Struct comment.") 164 165 // Individual struct entries are annotated with comments too. 166 So(nodeByName(strct, "key1").Comments(), ShouldEqual, "Key comment 1.") 167 168 // Nested structs are also annotated. 169 nested := nodeByName(strct, "nested").(*Namespace) 170 So(nested.Comments(), ShouldEqual, "Nested namespace.") 171 172 // Keys do not pick up comments not intended for them. 173 So(nodeByName(nested, "key1").Comments(), ShouldEqual, "") 174 175 // Top module comment is not extracted currently, it is relatively hard 176 // to do. We have a docstring though, so it's not a big deal. 177 So(mod.Comments(), ShouldEqual, "") 178 }) 179 }) 180 } 181 182 func dumpTree(nd Node, w io.Writer, indent string) { 183 // recurseInto is used to visit Namespace and Module. 184 recurseInto := func(nodes []Node) { 185 if len(nodes) != 0 { 186 for _, n := range nodes { 187 dumpTree(n, w, indent+" ") 188 } 189 } else { 190 fmt.Fprintf(w, "%s <empty>\n", indent) 191 } 192 } 193 194 switch n := nd.(type) { 195 case *Var: 196 fmt.Fprintf(w, "%s%s = %v\n", indent, n.name, n.Value) 197 case *Function: 198 fmt.Fprintf(w, "%s%s = func\n", indent, n.name) 199 case *Reference: 200 fmt.Fprintf(w, "%s%s = %s\n", indent, n.name, strings.Join(n.Path, ".")) 201 case *ExternalReference: 202 fmt.Fprintf(w, "%s%s -> %s in %s\n", indent, n.name, n.ExternalName, n.Module) 203 case *Invocation: 204 fmt.Fprintf(w, "%s%s = %s(...)\n", indent, n.name, strings.Join(n.Func, ".")) 205 recurseInto(n.Args) 206 case *Namespace: 207 fmt.Fprintf(w, "%s%s = namespace\n", indent, n.name) 208 recurseInto(n.Nodes) 209 case *Module: 210 fmt.Fprintf(w, "%s%s = module\n", indent, n.name) 211 recurseInto(n.Nodes) 212 default: 213 panic(fmt.Sprintf("unknown node kind %T", nd)) 214 } 215 } 216 217 func nodeByName(n EnumerableNode, name string) Node { 218 for _, node := range n.EnumNodes() { 219 if node.Name() == name { 220 return node 221 } 222 } 223 return nil 224 } 225 226 func extractSpan(body string, start, end syntax.Position) string { 227 lines := strings.Split(body, "\n") 228 229 // Note: sloppy, but good enough for the test. Also note that Line and Col 230 // are 1-based indexes. 231 var out []string 232 out = append(out, lines[start.Line-1][start.Col-1:]) 233 out = append(out, lines[start.Line:end.Line-1]...) 234 out = append(out, lines[end.Line-1][:end.Col-1]) 235 236 return strings.Join(out, "\n") 237 }