feat: lifetime elision, tooling CI, registry, and LSP locals
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1 lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks, stdlib goldens, package registry (bux search/add), and LSP 0.4 position-sensitive locals with inferred let types. Full-tree format pass plus Map/Set remove double-free fix.
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
## docgen.nim — Extract `///` (and adjacent `/* */`) docs into Markdown.
|
||||
## Used by `bux doc [path...]`.
|
||||
|
||||
import std/[os, strutils, strformat, algorithm]
|
||||
|
||||
type
|
||||
DocItem* = object
|
||||
kind*: string ## module | func | struct | enum | interface | extern
|
||||
name*: string
|
||||
signature*: string ## first declaration line (trimmed)
|
||||
docs*: string
|
||||
file*: string
|
||||
line*: int
|
||||
|
||||
proc isDeclStart(line: string): bool =
|
||||
let s = line.strip()
|
||||
if s.len == 0: return false
|
||||
# Leading attributes @[Checked] etc. — not a decl by themselves
|
||||
if s.startsWith("@["): return false
|
||||
if s.startsWith("func ") or s.startsWith("pub func ") or
|
||||
s.startsWith("extern func ") or s.startsWith("const func ") or
|
||||
s.startsWith("async func "):
|
||||
return true
|
||||
if s.startsWith("struct ") or s.startsWith("pub struct ") or
|
||||
s.startsWith("enum ") or s.startsWith("pub enum ") or
|
||||
s.startsWith("union ") or s.startsWith("interface ") or
|
||||
s.startsWith("module ") or s.startsWith("type "):
|
||||
return true
|
||||
return false
|
||||
|
||||
proc declKindAndName(line: string): tuple[kind, name: string] =
|
||||
var s = line.strip()
|
||||
# Strip leading pub/extern/const/async
|
||||
for prefix in ["pub ", "extern ", "const ", "async "]:
|
||||
if s.startsWith(prefix):
|
||||
s = s[prefix.len .. ^1].strip()
|
||||
var kind = "item"
|
||||
if s.startsWith("func "):
|
||||
kind = "func"
|
||||
s = s["func ".len .. ^1]
|
||||
elif s.startsWith("struct "):
|
||||
kind = "struct"
|
||||
s = s["struct ".len .. ^1]
|
||||
elif s.startsWith("enum "):
|
||||
kind = "enum"
|
||||
s = s["enum ".len .. ^1]
|
||||
elif s.startsWith("union "):
|
||||
kind = "union"
|
||||
s = s["union ".len .. ^1]
|
||||
elif s.startsWith("interface "):
|
||||
kind = "interface"
|
||||
s = s["interface ".len .. ^1]
|
||||
elif s.startsWith("module "):
|
||||
kind = "module"
|
||||
s = s["module ".len .. ^1]
|
||||
elif s.startsWith("type "):
|
||||
kind = "type"
|
||||
s = s["type ".len .. ^1]
|
||||
# Name: until `<` `(` `{` `:` space
|
||||
var name = ""
|
||||
for ch in s:
|
||||
if ch in {' ', '<', '(', '{', ':', ';'}:
|
||||
break
|
||||
name.add(ch)
|
||||
if name.len == 0:
|
||||
name = s
|
||||
# extern funcs already stripped "extern "
|
||||
if kind == "func" and line.strip().startsWith("extern"):
|
||||
kind = "extern"
|
||||
return (kind, name)
|
||||
|
||||
proc extractDocsFromSource*(source, path: string): seq[DocItem] =
|
||||
result = @[]
|
||||
var pending: seq[string] = @[]
|
||||
var inBlockComment = false
|
||||
var blockDoc: seq[string] = @[]
|
||||
var lineNo = 0
|
||||
for rawLine in source.splitLines():
|
||||
inc lineNo
|
||||
var line = rawLine
|
||||
let stripped = line.strip()
|
||||
|
||||
# Block comment handling (/* ... */ used in stdlib today)
|
||||
if inBlockComment:
|
||||
let endIdx = stripped.find("*/")
|
||||
if endIdx >= 0:
|
||||
let before = stripped[0 ..< endIdx].strip()
|
||||
if before.len > 0:
|
||||
blockDoc.add(before)
|
||||
inBlockComment = false
|
||||
# Treat completed block as pending doc if non-empty
|
||||
if blockDoc.len > 0:
|
||||
pending = blockDoc
|
||||
blockDoc = @[]
|
||||
continue
|
||||
else:
|
||||
blockDoc.add(stripped)
|
||||
continue
|
||||
if stripped.startsWith("/*") and not stripped.startsWith("/***"):
|
||||
let rest = stripped["/*".len .. ^1]
|
||||
let endIdx = rest.find("*/")
|
||||
if endIdx >= 0:
|
||||
let body = rest[0 ..< endIdx].strip()
|
||||
if body.len > 0:
|
||||
pending = @[body]
|
||||
else:
|
||||
inBlockComment = true
|
||||
blockDoc = @[]
|
||||
let body = rest.strip()
|
||||
if body.len > 0:
|
||||
blockDoc.add(body)
|
||||
continue
|
||||
|
||||
# Triple-slash doc comments
|
||||
if stripped.startsWith("///"):
|
||||
var body = stripped["///".len .. ^1]
|
||||
if body.startsWith(" "):
|
||||
body = body[1 .. ^1]
|
||||
pending.add(body)
|
||||
continue
|
||||
|
||||
# Empty line: keep pending docs (allow blank lines inside doc blocks)
|
||||
if stripped.len == 0:
|
||||
continue
|
||||
|
||||
# Attributes immediately before decl: keep pending
|
||||
if stripped.startsWith("@["):
|
||||
continue
|
||||
|
||||
if isDeclStart(line):
|
||||
if pending.len > 0:
|
||||
let (kind, name) = declKindAndName(line)
|
||||
result.add(DocItem(
|
||||
kind: kind,
|
||||
name: name,
|
||||
signature: stripped,
|
||||
docs: pending.join("\n"),
|
||||
file: path,
|
||||
line: lineNo
|
||||
))
|
||||
pending = @[]
|
||||
continue
|
||||
|
||||
# Other code clears pending (except plain // comments)
|
||||
if stripped.startsWith("//"):
|
||||
continue
|
||||
pending = @[]
|
||||
|
||||
proc collectBuxFilesForDoc*(root: string): seq[string] =
|
||||
result = @[]
|
||||
if fileExists(root) and root.endsWith(".bux"):
|
||||
result.add(root)
|
||||
return
|
||||
if not dirExists(root):
|
||||
return
|
||||
for path in walkDirRec(root):
|
||||
if path.endsWith(".bux"):
|
||||
result.add(path)
|
||||
result.sort(system.cmp)
|
||||
|
||||
proc renderMarkdown*(items: seq[DocItem], title: string = "API Reference"): string =
|
||||
var sb: string
|
||||
sb.add(&"# {title}\n\n")
|
||||
sb.add("Generated by `bux doc` from `///` and `/* */` documentation comments.\n\n")
|
||||
if items.len == 0:
|
||||
sb.add("_No documented items found._\n")
|
||||
return sb
|
||||
|
||||
# Group by file
|
||||
var byFile: seq[string] = @[]
|
||||
for it in items:
|
||||
if it.file notin byFile:
|
||||
byFile.add(it.file)
|
||||
byFile.sort(system.cmp)
|
||||
|
||||
for f in byFile:
|
||||
let base = splitFile(f).name
|
||||
sb.add(&"## `{base}`\n\n")
|
||||
sb.add(&"_Source: `{f}`_\n\n")
|
||||
for it in items:
|
||||
if it.file != f:
|
||||
continue
|
||||
sb.add(&"### `{it.name}` _{it.kind}_\n\n")
|
||||
sb.add("```bux\n")
|
||||
sb.add(it.signature)
|
||||
sb.add("\n```\n\n")
|
||||
if it.docs.len > 0:
|
||||
sb.add(it.docs)
|
||||
sb.add("\n\n")
|
||||
return sb
|
||||
|
||||
proc generateDocs*(paths: seq[string]): seq[DocItem] =
|
||||
result = @[]
|
||||
var files: seq[string] = @[]
|
||||
for p in paths:
|
||||
for f in collectBuxFilesForDoc(p):
|
||||
if f notin files:
|
||||
files.add(f)
|
||||
files.sort(system.cmp)
|
||||
for f in files:
|
||||
let src = readFile(f)
|
||||
result.add(extractDocsFromSource(src, f))
|
||||
Reference in New Issue
Block a user