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
« 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.
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.
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.
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"""
21from __future__ import annotations
23import re
24import sys
25from pathlib import Path
26from typing import Any
28sys.path.insert(0, str(Path(__file__).resolve().parent))
29import _skeleton_common as common # noqa: E402
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
44def go_package_name(target: Path) -> str:
45 """Return the package name for the module's root package.
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.
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"
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.
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"
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}
107 module = go_module_path(target)
108 notes.append(f"module {module}" if module else "go.mod declares no module path")
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")
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")
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}