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  }