Coverage for plugin/scripts/_skeleton_go.py: 100%

53 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-15 14:46 +0000

1#!/usr/bin/env python3 

2"""Finish what `go mod init` leaves out of a Go module. 

3 

4The largest gap of the three languages to open, and the shortest module to close it, 

5because the two facts pull in opposite directions. `go mod init` writes exactly one file 

6— `go.mod`, holding a module path and a Go version — so almost nothing is there. But 

7`go.mod` has no description, repository, homepage, author or licence field either, so 

8there is no manifest step to write: everything the other two languages put in a manifest 

9is, for Go, the git remote's job or the `LICENSE` file's. 

10 

11What is left is a `README.md` (go writes none) and a `doc.go` carrying the package comment 

12revive's `exported` rule wants — the template runs that as `make docs-coverage`, and with 

13no Go file at all there is nothing for the rule to find and nothing for `go test ./...` 

14to run. 

15 

16**No version location is written here.** A Go module's version *is* its git tag, and the 

17`go-core` bundle owns the declaration; see `_skeleton_version` for why writing one would 

18be actively wrong. 

19""" 

20 

21from __future__ import annotations 

22 

23import re 

24import sys 

25from pathlib import Path 

26from typing import Any 

27 

28sys.path.insert(0, str(Path(__file__).resolve().parent)) 

29import _skeleton_common as common # noqa: E402 

30 

31 

32def go_module_path(target: Path) -> str | None: 

33 """Return the `module` path `go.mod` declares, or None.""" 

34 manifest = target / "go.mod" 

35 if not manifest.is_file(): 

36 return None 

37 for line in manifest.read_text(encoding="utf-8", errors="ignore").splitlines(): 

38 match = re.match(r"^\s*module\s+(\S+)", line) 

39 if match: 

40 return match.group(1) 

41 return None 

42 

43 

44def go_package_name(target: Path) -> str: 

45 """Return the package name for the module's root package. 

46 

47 The convention is the last element of the module path, minus a major-version suffix — 

48 a `/v2` belongs to the *import* path and never to the package name. 

49 

50 That element then has to be a Go identifier, which is narrower than a path element: 

51 lowercase, and no dots or hyphens. Underscores are kept, because `_` is a legal 

52 identifier character and renaming `example.com/my_lib`'s package to `mylib` would 

53 surprise anyone importing it. A name that cannot start an identifier — nothing left, 

54 or a leading digit — falls back to `pkg`: valid, neutral, and deliberately not `main`, 

55 which in Go declares an executable rather than a library. 

56 """ 

57 path = go_module_path(target) or target.name 

58 last = path.rstrip("/").split("/")[-1] 

59 if re.fullmatch(r"v[0-9]+", last): 

60 parts = path.rstrip("/").split("/") 

61 last = parts[-2] if len(parts) > 1 else target.name 

62 cleaned = re.sub(r"[^a-z0-9_]", "", last.lower()) 

63 return cleaned if re.fullmatch(r"[a-z_][a-z0-9_]*", cleaned) else "pkg" 

64 

65 

66def seed_package_doc(target: Path, *, description: str | None) -> str | None: 

67 """Write `doc.go` with the module's package comment; return the path, or None. 

68 

69 `doc.go` is the convention for a package comment with no code attached, which keeps 

70 this from inventing API. Written only into a module with no root package yet: a 

71 second package comment in a package that already has one is itself a lint finding, 

72 so an existing `.go` file at the root means hands off. 

73 """ 

74 if any(target.glob("*.go")): 

75 return None 

76 package = go_package_name(target) 

77 module = go_module_path(target) or package 

78 # Go's convention is that the first sentence is a summary beginning "Package <name>", 

79 # which a description pasted straight in would not be ("Package widget A widget 

80 # library." is a fragment). So the summary is generated and the description, if there 

81 # is one, becomes the paragraph under it. 

82 body = f"// Package {package} is the root package of {module}.\n" 

83 summary = description.strip() if description and description.strip() else None 

84 if summary: 

85 if not summary.endswith("."): 

86 summary += "." 

87 body += f"//\n// {summary}\n" 

88 (target / "doc.go").write_text(f"{body}package {package}\n", encoding="utf-8") 

89 return "doc.go" 

90 

91 

92def finish_go( 

93 target: Path, 

94 *, 

95 repo: str, 

96 description: str | None, 

97 modified: list[str], 

98 notes: list[str], 

99) -> dict[str, Any]: 

100 """Finish a `go mod init` skeleton; return a summary dict.""" 

101 # `is_file`, not `exists`: a directory named go.mod would pass the gate here and then 

102 # be read as an absent manifest by every helper downstream. 

103 if not (target / "go.mod").is_file(): 

104 notes.append("go.mod absent — run `go mod init <module path>` first") 

105 return {"modified": modified, "changes": [], "notes": notes, "ok": False} 

106 

107 module = go_module_path(target) 

108 notes.append(f"module {module}" if module else "go.mod declares no module path") 

109 

110 doc = seed_package_doc(target, description=description) 

111 if doc: 

112 modified.append(doc) 

113 notes.append( 

114 "wrote doc.go with the package comment revive's `exported` rule wants — " 

115 "`go mod init` creates no Go file at all" 

116 ) 

117 else: 

118 notes.append("root package already has Go files — left alone") 

119 

120 if common.seed_readme(target, repo=repo, description=description, create=True): 

121 modified.append("README.md") 

122 notes.append("seeded the README.md go never writes — /rhiza:docs owns the real one") 

123 

124 notes.append("go.mod holds no metadata to fill in; license is /rhiza:license's job") 

125 return {"modified": modified, "changes": [], "notes": notes, "ok": True}