pulumi/pkg/codegen/docs/examples.go

181 lines
5.4 KiB
Go
Raw Normal View History

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
sdk/go: Remove 'nolint' directives from package docs Go treats comments that match the following regex as directives. //[a-z0-9]+:[a-z0-9] Comments that are directives don't show in an entity's documentation. https://github.com/golang/go/commit/5a550b695117f07a4f2454039a4871250cd3ed09#diff-f56160fd9fcea272966a8a1d692ad9f49206fdd8dbcbfe384865a98cd9bc2749R165 Our code has `//nolint` directives that now show in the API Reference. This is because these directives are in one of the following forms, which don't get this special treatment. // nolint:foo //nolint: foo This change fixes all such directives found by the regex: `// nolint|//nolint: `. See bottom of commit for command used for the fix. Verification: Here's the output of `go doc` on some entities before and after this change. Before ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi | head -n8 package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" nolint: lll, interfacer nolint: lll, interfacer const EnvOrganization = "PULUMI_ORGANIZATION" ... var ErrPlugins = errors.New("pulumi: plugins requested") ``` After ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi | head -n8 package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" const EnvOrganization = "PULUMI_ORGANIZATION" ... var ErrPlugins = errors.New("pulumi: plugins requested") func BoolRef(v bool) *bool func Float64Ref(v float64) *float64 func IntRef(v int) *int func IsSecret(o Output) bool ``` Before ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi URN_ package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" func URN_(o string) ResourceOption URN_ is an optional URN of a previously-registered resource of this type to read from the engine. nolint: revive ``` After: ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi URN_ package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" func URN_(o string) ResourceOption URN_ is an optional URN of a previously-registered resource of this type to read from the engine. ``` Note that golangci-lint offers a 'nolintlint' linter that finds such miuses of nolint, but it also finds other issues so I've deferred that to a follow up PR. Resolves #11785 Related: https://github.com/golangci/golangci-lint/issues/892 [git-generate] FILES=$(mktemp) rg -l '// nolint|//nolint: ' | tee "$FILES" | xargs perl -p -i -e ' s|// nolint|//nolint|g; s|//nolint: |//nolint:|g; ' rg '.go$' < "$FILES" | xargs gofmt -w -s
2023-01-06 00:07:45 +00:00
//nolint:lll, goconst
package docs
import (
"fmt"
"strings"
"github.com/pgavlin/goldmark/ast"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
const defaultMissingExampleSnippetPlaceholder = "Coming soon!"
type exampleSection struct {
Title string
// Snippets is a map of language to its code snippet, if any.
Snippets map[string]string
}
type docInfo struct {
description string
examples []exampleSection
importDetails string
}
func (dctx *docGenContext) decomposeDocstring(docstring string) docInfo {
if docstring == "" {
return docInfo{}
}
languages := codegen.NewStringSet(dctx.snippetLanguages...)
source := []byte(docstring)
parsed := schema.ParseDocs(source)
var examplesShortcode *schema.Shortcode
var exampleShortcode *schema.Shortcode
var examples []exampleSection
2022-10-04 01:06:38 +00:00
currentSection := exampleSection{
Snippets: map[string]string{},
}
var nextTitle string
2022-10-04 18:21:57 +00:00
var nextInferredTitle string
2022-10-04 01:06:38 +00:00
// Push any examples we have found. Since `pushExamples` is called between sections,
// it needs to behave correctly when no examples were found.
pushExamples := func() {
if len(currentSection.Snippets) > 0 {
2022-10-04 17:24:06 +00:00
for _, l := range dctx.snippetLanguages {
if _, ok := currentSection.Snippets[l]; !ok {
currentSection.Snippets[l] = defaultMissingExampleSnippetPlaceholder
}
}
2022-10-04 01:06:38 +00:00
examples = append(examples, currentSection)
}
2022-10-04 18:21:57 +00:00
if nextTitle == "" {
nextTitle = nextInferredTitle
}
2022-10-04 01:06:38 +00:00
currentSection = exampleSection{
Snippets: map[string]string{},
Title: nextTitle,
}
nextTitle = ""
2022-10-04 18:21:57 +00:00
nextInferredTitle = ""
2022-10-04 01:06:38 +00:00
}
err := ast.Walk(parsed, func(n ast.Node, enter bool) (ast.WalkStatus, error) {
2022-10-04 01:06:38 +00:00
// ast.Walk visits each node twice. The first time descending and the second time
// ascending. We only want to view the nodes while descending, so we skip when
// `enter` is false.
if !enter {
return ast.WalkContinue, nil
}
if shortcode, ok := n.(*schema.Shortcode); ok {
name := string(shortcode.Name)
switch name {
case schema.ExamplesShortcode:
if examplesShortcode == nil {
examplesShortcode = shortcode
}
case schema.ExampleShortcode:
if exampleShortcode == nil {
2022-10-04 01:06:38 +00:00
exampleShortcode = shortcode
currentSection.Title, currentSection.Snippets = "", map[string]string{}
} else if !enter && shortcode == exampleShortcode {
2022-10-04 01:06:38 +00:00
pushExamples()
exampleShortcode = nil
}
}
return ast.WalkContinue, nil
}
2022-10-04 01:06:38 +00:00
// We check to make sure we are in an examples section.
if exampleShortcode == nil {
return ast.WalkContinue, nil
}
switch n := n.(type) {
case *ast.Heading:
2022-10-04 01:06:38 +00:00
if n.Level == 3 {
title := strings.TrimSpace(schema.RenderDocsToString(source, n))
if currentSection.Title == "" && len(currentSection.Snippets) == 0 {
currentSection.Title = title
} else {
nextTitle = title
}
}
2022-10-04 18:21:57 +00:00
return ast.WalkSkipChildren, nil
2022-10-04 01:06:38 +00:00
case *ast.FencedCodeBlock:
language := string(n.Language(source))
2022-10-04 01:06:38 +00:00
snippet := schema.RenderDocsToString(source, n)
if !languages.Has(language) || len(snippet) == 0 {
return ast.WalkContinue, nil
}
2022-10-04 01:06:38 +00:00
if _, ok := currentSection.Snippets[language]; ok {
// We have the same language appearing multiple times in a {{% examples
// %}} without an {{% example %}} to break them up. We are going to just
// pretend there was an {{% example %}}
pushExamples()
}
currentSection.Snippets[language] = snippet
case *ast.Text:
// We only want to change the title before we collect any snippets
2022-10-04 01:07:48 +00:00
title := strings.TrimSuffix(string(n.Text(source)), ":")
2022-10-04 01:06:38 +00:00
if currentSection.Title == "" && len(currentSection.Snippets) == 0 {
currentSection.Title = title
} else {
// Since we might find out we are done with the previous section only
// after we have consumed the next title, we store the title.
2022-10-04 18:21:57 +00:00
nextInferredTitle = title
}
}
return ast.WalkContinue, nil
})
contract.AssertNoErrorf(err, "error walking AST")
2022-10-04 01:06:38 +00:00
pushExamples()
if examplesShortcode != nil {
p := examplesShortcode.Parent()
p.RemoveChild(p, examplesShortcode)
}
description := schema.RenderDocsToString(source, parsed)
importDetails := ""
parts := strings.Split(description, "\n\n## Import")
if len(parts) > 1 { // we only care about the Import section details here!!
importDetails = parts[1]
}
// When we split the description above, the main part of the description is always part[0]
// the description must have a blank line after it to render the examples correctly
description = fmt.Sprintf("%s\n", parts[0])
return docInfo{
description: description,
examples: examples,
importDetails: importDetails,
}
}