| // Copyright 2019 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // Package sample provides functionality for generating sample values of | |
| // the types contained in the internal package. | |
| package sample | |
| import ( | |
| "context" | |
| "fmt" | |
| "go/parser" | |
| "go/token" | |
| "math" | |
| "net/http" | |
| "path" | |
| "strings" | |
| "time" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/google/go-cmp/cmp/cmpopts" | |
| "github.com/google/licensecheck" | |
| "golang.org/x/pkgsite/internal" | |
| "golang.org/x/pkgsite/internal/godoc" | |
| "golang.org/x/pkgsite/internal/licenses" | |
| "golang.org/x/pkgsite/internal/source" | |
| "golang.org/x/pkgsite/internal/stdlib" | |
| ) | |
| // These sample values can be used to construct test cases. | |
| var ( | |
| ModulePath = "github.com/valid/module_name" | |
| RepositoryURL = "https://github.com/valid/module_name" | |
| VersionString = "v1.0.0" | |
| CommitTime = NowTruncated() | |
| LicenseType = "MIT" | |
| LicenseFilePath = "LICENSE" | |
| PackageName = "foo" | |
| Suffix = "foo" | |
| PackagePath = path.Join(ModulePath, Suffix) | |
| V1Path = PackagePath | |
| ReadmeFilePath = "README.md" | |
| ReadmeContents = "readme" | |
| GOOS = internal.All | |
| GOARCH = internal.All | |
| Doc = Documentation(GOOS, GOARCH, DocContents) | |
| API = []*internal.Symbol{ | |
| Constant, | |
| Variable, | |
| Function, | |
| Type, | |
| } | |
| DocContents = ` | |
| // Package p is a package. | |
| // | |
| // | |
| // Links | |
| // | |
| // - pkg.go.dev, https://pkg.go.dev | |
| package p | |
| var V int | |
| ` | |
| Constant = &internal.Symbol{ | |
| SymbolMeta: internal.SymbolMeta{ | |
| Name: "Constant", | |
| Synopsis: "const Constant", | |
| Section: internal.SymbolSectionConstants, | |
| Kind: internal.SymbolKindConstant, | |
| }, | |
| GOOS: internal.All, | |
| GOARCH: internal.All, | |
| } | |
| Variable = &internal.Symbol{ | |
| SymbolMeta: internal.SymbolMeta{ | |
| Name: "Variable", | |
| Synopsis: "var Variable", | |
| Section: internal.SymbolSectionVariables, | |
| Kind: internal.SymbolKindVariable, | |
| }, | |
| GOOS: internal.All, | |
| GOARCH: internal.All, | |
| } | |
| Function = &internal.Symbol{ | |
| SymbolMeta: internal.SymbolMeta{ | |
| Name: "Function", | |
| Synopsis: "func Function() error", | |
| Section: internal.SymbolSectionFunctions, | |
| Kind: internal.SymbolKindFunction, | |
| }, | |
| GOOS: internal.All, | |
| GOARCH: internal.All, | |
| } | |
| FunctionNew = &internal.Symbol{ | |
| SymbolMeta: internal.SymbolMeta{ | |
| Name: "New", | |
| Synopsis: "func New() *Type", | |
| Section: internal.SymbolSectionTypes, | |
| Kind: internal.SymbolKindFunction, | |
| ParentName: "Type", | |
| }, | |
| GOOS: internal.All, | |
| GOARCH: internal.All, | |
| } | |
| Type = &internal.Symbol{ | |
| SymbolMeta: internal.SymbolMeta{ | |
| Name: "Type", | |
| Synopsis: "type Type struct", | |
| Section: internal.SymbolSectionTypes, | |
| Kind: internal.SymbolKindType, | |
| }, | |
| GOOS: internal.All, | |
| GOARCH: internal.All, | |
| Children: []*internal.SymbolMeta{ | |
| &FunctionNew.SymbolMeta, | |
| &Field, | |
| &Method, | |
| }, | |
| } | |
| Field = internal.SymbolMeta{ | |
| Name: "Type.Field", | |
| Synopsis: "field", | |
| Section: internal.SymbolSectionTypes, | |
| Kind: internal.SymbolKindField, | |
| ParentName: "Type", | |
| } | |
| Method = internal.SymbolMeta{ | |
| Name: "Type.Method", | |
| Synopsis: "method", | |
| Section: internal.SymbolSectionTypes, | |
| Kind: internal.SymbolKindMethod, | |
| ParentName: "Type", | |
| } | |
| ) | |
| // LicenseCmpOpts are options to use when comparing licenses with the cmp package. | |
| var LicenseCmpOpts = []cmp.Option{ | |
| cmp.Comparer(coveragePercentEqual), | |
| cmpopts.IgnoreFields(licensecheck.Match{}, "Start", "End"), | |
| } | |
| // coveragePercentEqual considers two floats the same if they are within 4 | |
| // percentage points, and both are on the same side of 90% (our threshold). | |
| func coveragePercentEqual(a, b float64) bool { | |
| if (a >= 90) != (b >= 90) { | |
| return false | |
| } | |
| return math.Abs(a-b) <= 4 | |
| } | |
| // NowTruncated returns time.Now() truncated to Microsecond precision. | |
| // | |
| // This makes it easier to work with timestamps in PostgreSQL, which have | |
| // Microsecond precision: | |
| // | |
| // https://www.postgresql.org/docs/9.1/datatype-datetime.html | |
| func NowTruncated() time.Time { | |
| return time.Now().In(time.UTC).Truncate(time.Microsecond) | |
| } | |
| func DefaultModule() *internal.Module { | |
| fp := constructFullPath(ModulePath, Suffix) | |
| return AddPackage(Module(ModulePath, VersionString), UnitForPackage(fp, ModulePath, VersionString, path.Base(fp), true)) | |
| } | |
| // Module creates a Module with the given path and version. | |
| // The list of suffixes is used to create Units within the module. | |
| func Module(modulePath, version string, suffixes ...string) *internal.Module { | |
| mi := ModuleInfo(modulePath, version) | |
| m := &internal.Module{ | |
| ModuleInfo: *mi, | |
| Licenses: Licenses(), | |
| } | |
| m.Units = []*internal.Unit{UnitForModuleRoot(mi)} | |
| for _, s := range suffixes { | |
| fp := constructFullPath(modulePath, s) | |
| lp := UnitForPackage(fp, modulePath, VersionString, path.Base(fp), m.IsRedistributable) | |
| if s != "" { | |
| AddPackage(m, lp) | |
| } else { | |
| u := UnitForPackage(lp.Path, modulePath, version, lp.Name, lp.IsRedistributable) | |
| m.Units[0].Documentation = u.Documentation | |
| m.Units[0].Name = u.Name | |
| } | |
| } | |
| if modulePath == stdlib.ModulePath { | |
| m.Units[0].Readme = nil | |
| } | |
| // Fill in license contents. | |
| for _, u := range m.Units { | |
| u.LicenseContents = m.Licenses | |
| } | |
| return m | |
| } | |
| func UnitForModuleRoot(m *internal.ModuleInfo) *internal.Unit { | |
| u := &internal.Unit{ | |
| UnitMeta: *UnitMeta(m.ModulePath, m.ModulePath, m.Version, "", m.IsRedistributable), | |
| IsRedistributable: m.IsRedistributable, | |
| Licenses: LicenseMetadata(), | |
| LicenseContents: Licenses(), | |
| } | |
| u.Readme = &internal.Readme{ | |
| Filepath: ReadmeFilePath, | |
| Contents: ReadmeContents, | |
| } | |
| return u | |
| } | |
| // UnitForPackage constructs a unit with the given module path and suffix. | |
| // | |
| // If modulePath is the standard library, the package path is the | |
| // suffix, which must not be empty. Otherwise, the package path | |
| // is the concatenation of modulePath and suffix. | |
| // | |
| // The package name is last component of the package path. | |
| func UnitForPackage(path, modulePath, version, name string, isRedistributable bool) *internal.Unit { | |
| // Copy Doc because some tests modify it. | |
| doc := *Doc | |
| imps := Imports() | |
| return &internal.Unit{ | |
| UnitMeta: *UnitMeta(path, modulePath, version, name, isRedistributable), | |
| IsRedistributable: isRedistributable, | |
| Licenses: LicenseMetadata(), | |
| Documentation: []*internal.Documentation{&doc}, | |
| BuildContexts: []internal.BuildContext{{GOOS: doc.GOOS, GOARCH: doc.GOARCH}}, | |
| LicenseContents: Licenses(), | |
| Imports: imps, | |
| NumImports: len(imps), | |
| } | |
| } | |
| func AddPackage(m *internal.Module, pkg *internal.Unit) *internal.Module { | |
| if m.ModulePath != stdlib.ModulePath && !strings.HasPrefix(pkg.Path, m.ModulePath) { | |
| panic(fmt.Sprintf("package path %q not a prefix of module path %q", | |
| pkg.Path, m.ModulePath)) | |
| } | |
| AddUnit(m, UnitForPackage(pkg.Path, m.ModulePath, m.Version, pkg.Name, pkg.IsRedistributable)) | |
| minLen := len(m.ModulePath) | |
| if m.ModulePath == stdlib.ModulePath { | |
| minLen = 1 | |
| } | |
| for pth := pkg.Path; len(pth) > minLen; pth = path.Dir(pth) { | |
| found := false | |
| for _, u := range m.Units { | |
| if u.Path == pth { | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| AddUnit(m, UnitEmpty(pth, m.ModulePath, m.Version)) | |
| } | |
| } | |
| return m | |
| } | |
| func PackageMeta(fullPath string) *internal.PackageMeta { | |
| return &internal.PackageMeta{ | |
| Path: fullPath, | |
| IsRedistributable: true, | |
| Name: path.Base(fullPath), | |
| Synopsis: Doc.Synopsis, | |
| Licenses: LicenseMetadata(), | |
| } | |
| } | |
| func ModuleInfo(modulePath, versionString string) *internal.ModuleInfo { | |
| return &internal.ModuleInfo{ | |
| ModulePath: modulePath, | |
| Version: versionString, | |
| CommitTime: CommitTime, | |
| // Assume the module path is a GitHub-like repo name. | |
| SourceInfo: source.NewGitHubInfo("https://"+modulePath, "", versionString), | |
| IsRedistributable: true, | |
| HasGoMod: true, | |
| LatestVersion: versionString, | |
| } | |
| } | |
| func DefaultVersionMap() *internal.VersionMap { | |
| return &internal.VersionMap{ | |
| ModulePath: ModulePath, | |
| RequestedVersion: VersionString, | |
| ResolvedVersion: VersionString, | |
| Status: http.StatusOK, | |
| GoModPath: "", | |
| Error: "", | |
| } | |
| } | |
| func AddUnit(m *internal.Module, u *internal.Unit) { | |
| for _, e := range m.Units { | |
| if e.Path == u.Path { | |
| panic(fmt.Sprintf("module already has path %q", e.Path)) | |
| } | |
| } | |
| m.Units = append(m.Units, u) | |
| } | |
| // NonRedistributableLicense returns a license that is not redistributable. | |
| // | |
| // It returns a new License instance each time, as non-redistributable licenses | |
| // may be mutated during insertion. | |
| func NonRedistributableLicense() *licenses.License { | |
| return &licenses.License{ | |
| Metadata: &licenses.Metadata{ | |
| FilePath: "NONREDIST_LICENSE", | |
| Types: []string{"UNKNOWN"}, | |
| }, | |
| Contents: []byte(`unknown`), | |
| } | |
| } | |
| func AddLicense(m *internal.Module, lic *licenses.License) { | |
| m.Licenses = append(m.Licenses, lic) | |
| dir := path.Dir(lic.FilePath) | |
| if dir == "." { | |
| dir = "" | |
| } | |
| for _, u := range m.Units { | |
| if strings.TrimPrefix(u.Path, m.ModulePath+"/") == dir { | |
| u.Licenses = append(u.Licenses, lic.Metadata) | |
| u.LicenseContents = append(u.LicenseContents, lic) | |
| } | |
| } | |
| } | |
| // ReplaceLicense replaces all licenses having the same file path as lic with lic. | |
| func ReplaceLicense(m *internal.Module, lic *licenses.License) { | |
| replaceLicense(lic, m.Licenses) | |
| for _, u := range m.Units { | |
| for i, lm := range u.Licenses { | |
| if lm.FilePath == lic.FilePath { | |
| u.Licenses[i] = lic.Metadata | |
| } | |
| } | |
| replaceLicense(lic, u.LicenseContents) | |
| } | |
| } | |
| func replaceLicense(lic *licenses.License, lics []*licenses.License) { | |
| for i, l := range lics { | |
| if l.FilePath == lic.FilePath { | |
| lics[i] = lic | |
| } | |
| } | |
| } | |
| func UnitEmpty(path, modulePath, version string) *internal.Unit { | |
| return &internal.Unit{ | |
| UnitMeta: *UnitMeta(path, modulePath, version, "", true), | |
| IsRedistributable: true, | |
| Licenses: LicenseMetadata(), | |
| } | |
| } | |
| func UnitMeta(path, modulePath, version, name string, moduleIsRedistributable bool) *internal.UnitMeta { | |
| return &internal.UnitMeta{ | |
| Path: path, | |
| Name: name, | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: modulePath, | |
| Version: version, | |
| CommitTime: NowTruncated(), | |
| IsRedistributable: moduleIsRedistributable, | |
| SourceInfo: source.NewGitHubInfo("https://"+modulePath, "", version), | |
| LatestVersion: version, | |
| }, | |
| } | |
| } | |
| func constructFullPath(modulePath, suffix string) string { | |
| if modulePath != stdlib.ModulePath { | |
| return path.Join(modulePath, suffix) | |
| } | |
| return suffix | |
| } | |
| // Documentation returns a Documentation value for the given Go sources. | |
| // It panics if there are errors parsing or encoding the source. | |
| func Documentation(goos, goarch, fileContents string) *internal.Documentation { | |
| return DocumentationWithTest(goos, goarch, fileContents, "") | |
| } | |
| // DocumentationWithText returns a Documentation value for the given Go sources. | |
| // The fileContents arg is the contents of a non-test file. | |
| // The testFileContents arg is the contents of a non-test file. | |
| // It panics if there are errors parsing or encoding the source. | |
| func DocumentationWithTest(goos, goarch, fileContents, testFileContents string) *internal.Documentation { | |
| m := map[string]string{ | |
| "sample.go": fileContents, | |
| } | |
| if testFileContents != "" { | |
| m["sample_test.go"] = testFileContents | |
| } | |
| return documentation(goos, goarch, m) | |
| } | |
| func documentation(goos, goarch string, files map[string]string) *internal.Documentation { | |
| fset := token.NewFileSet() | |
| docPkg := godoc.NewPackage(fset, nil) | |
| for name, contents := range files { | |
| pf, err := parser.ParseFile(fset, name, contents, parser.ParseComments) | |
| if err != nil { | |
| panic(err) | |
| } | |
| docPkg.AddFile(pf, true) | |
| } | |
| src, err := docPkg.Encode(context.Background()) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return &internal.Documentation{ | |
| GOOS: goos, | |
| GOARCH: goarch, | |
| Synopsis: fmt.Sprintf("This is a package synopsis for GOOS=%s, GOARCH=%s", goos, goarch), | |
| Source: src, | |
| } | |
| } | |
| func LicenseMetadata() []*licenses.Metadata { | |
| return []*licenses.Metadata{ | |
| { | |
| Types: []string{LicenseType}, | |
| FilePath: LicenseFilePath, | |
| Coverage: licensecheck.Coverage{ | |
| Percent: 100, | |
| Match: []licensecheck.Match{{ID: "MIT", Type: 0}}, | |
| }, | |
| }, | |
| } | |
| } | |
| func Licenses() []*licenses.License { | |
| return []*licenses.License{ | |
| {Metadata: LicenseMetadata()[0], Contents: []byte(`Lorem Ipsum`)}, | |
| } | |
| } | |
| func Imports() []string { | |
| return []string{"fmt", "path/to/bar"} | |
| } |