feat(tools): L10 ludic-fmt enforces a project's style
lint lines in package.ludic - one_statement, max_file_lines, max_function_lines, max_comment_lines, max_header_lines, paths, baseline - checked by ludic-fmt --check <files> and ludic-fmt --lint (the project), at the line; a baseline ratchet lets a rule arrive in a codebase that breaks it (--init-baseline), lowered as it is fixed. check-impl reads the alias declarations too (it had been blind to every namespace L6 moved out of the compiler), and eight methods get their pages; a test holds the formatter to keeping type arguments together (L5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e543525eb7
commit
334469ef61
16 changed files with 635 additions and 2 deletions
17
LANGUAGE.md
17
LANGUAGE.md
|
|
@ -1360,6 +1360,23 @@ checked. Diagnostics are limited to parse-level errors, reported as
|
|||
`file:line: error: message`; richer static checks (unknown identifiers,
|
||||
duplicate types, unknown fields, arity) are future work.
|
||||
|
||||
`ludic-fmt --check` also enforces a project's style, stated in its `package.ludic`:
|
||||
|
||||
```
|
||||
lint one_statement # two statements on one line
|
||||
lint max_file_lines 100
|
||||
lint max_function_lines 50
|
||||
lint max_comment_lines 2 # a comment says why, in a line or two
|
||||
lint max_header_lines 3 # the comment that opens a file
|
||||
lint paths "src" "lab" # what `ludic-fmt --lint` walks
|
||||
lint baseline "tests/lint-baseline.txt"
|
||||
```
|
||||
|
||||
`ludic-fmt --lint` checks the project's paths; the baseline is a ratchet - the violations each file
|
||||
had when a rule came in, which it may keep but not add to, lowered automatically as they are
|
||||
fixed - so a rule can arrive in a codebase that breaks it today (`ludic-fmt --init-baseline`
|
||||
writes it). A `;` or a `#` inside a string does not count.
|
||||
|
||||
### Editors
|
||||
|
||||
```bash
|
||||
|
|
|
|||
7
changes/lint.md
Normal file
7
changes/lint.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic-fmt` enforces a project's style.** `lint` lines in `package.ludic` state it - one statement
|
||||
per line, file, function and comment-block limits (a file's opening comment its own) - and
|
||||
`ludic-fmt --check <files>` and `ludic-fmt --lint` (the project's paths) fail what breaks them, at the
|
||||
line. A baseline file is the ratchet that lets a rule arrive in an existing codebase: the counts a
|
||||
file had may stay but not grow, and they are lowered as they are fixed.
|
||||
20
docs/language/app/app-monitor_count.md
Normal file
20
docs/language/app/app-monitor_count.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
id: app-monitor_count
|
||||
name: App.monitor_count
|
||||
category: app
|
||||
kind: namespace-method
|
||||
tokens: App.monitor_count
|
||||
sig: App.monitor_count() -> int
|
||||
tip: How many displays the machine has.
|
||||
order: 49
|
||||
ns: App
|
||||
member: monitor_count
|
||||
---
|
||||
|
||||
The number of displays attached, for a setting that chooses which one the game opens on; 1 on a platform that reports only the main one.
|
||||
|
||||
```ludic
|
||||
program Screens {
|
||||
entry { print(App.monitor_count()) }
|
||||
}
|
||||
```
|
||||
23
docs/language/app/app-window_fixed.md
Normal file
23
docs/language/app/app-window_fixed.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: app-window_fixed
|
||||
name: App.window_fixed
|
||||
category: app
|
||||
kind: namespace-method
|
||||
tokens: App.window_fixed
|
||||
sig: App.window_fixed(fixed)
|
||||
tip: A window that cannot be resized or maximised.
|
||||
order: 51
|
||||
ns: App
|
||||
member: window_fixed
|
||||
---
|
||||
|
||||
With `true`, the window has no resize grip and no maximise button - for a launcher or a dialog, which a player should not take for the game itself.
|
||||
|
||||
Parameters:
|
||||
- `fixed` — `true` to fix the window's size, `false` to free it
|
||||
|
||||
```ludic
|
||||
program Launcher {
|
||||
entry { App.window_fixed(true) }
|
||||
}
|
||||
```
|
||||
23
docs/language/app/app-window_to_monitor.md
Normal file
23
docs/language/app/app-window_to_monitor.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: app-window_to_monitor
|
||||
name: App.window_to_monitor
|
||||
category: app
|
||||
kind: namespace-method
|
||||
tokens: App.window_to_monitor
|
||||
sig: App.window_to_monitor(index)
|
||||
tip: Moves the window to that display.
|
||||
order: 50
|
||||
ns: App
|
||||
member: window_to_monitor
|
||||
---
|
||||
|
||||
Moves the game's window onto display `index` (0 is the main one), keeping its size. A no-op where the platform leaves moving windows between screens to the player.
|
||||
|
||||
Parameters:
|
||||
- `index` — which display, 0 to `App.monitor_count() - 1`
|
||||
|
||||
```ludic
|
||||
program Move {
|
||||
entry { App.window_to_monitor(0) }
|
||||
}
|
||||
```
|
||||
26
docs/language/fs/fs-read_bytes.md
Normal file
26
docs/language/fs/fs-read_bytes.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: fs-read_bytes
|
||||
name: Fs.read_bytes
|
||||
category: fs
|
||||
kind: namespace-method
|
||||
tokens: Fs.read_bytes
|
||||
sig: Fs.read_bytes(path) -> []byte
|
||||
tip: A whole file as bytes, or null.
|
||||
order: 11
|
||||
ns: Fs
|
||||
member: read_bytes
|
||||
---
|
||||
|
||||
Reads a whole file into a `[]byte` - bounds-checked, so no raw memory - or returns <code>null</code> when it cannot be read. It reads through the asset pack like every other load, so it finds a shipped game's files. Pair it with <code>text_of(b, n)</code> for text.
|
||||
|
||||
Parameters:
|
||||
- `path` — the file to read
|
||||
|
||||
```ludic
|
||||
program Header {
|
||||
entry {
|
||||
let b = Fs.read_bytes("sound.wav")
|
||||
if b != null { print(b[24]) }
|
||||
}
|
||||
}
|
||||
```
|
||||
30
docs/language/fs/fs-write_bytes.md
Normal file
30
docs/language/fs/fs-write_bytes.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
id: fs-write_bytes
|
||||
name: Fs.write_bytes
|
||||
category: fs
|
||||
kind: namespace-method
|
||||
tokens: Fs.write_bytes
|
||||
sig: Fs.write_bytes(path, data, count) -> bool
|
||||
tip: Writes the first count bytes of a []byte to a file.
|
||||
order: 12
|
||||
ns: Fs
|
||||
member: write_bytes
|
||||
---
|
||||
|
||||
Writes the first `count` bytes of `data` to `path`, replacing the file, and answers whether all of them were written. `count` is clamped to the slice's length.
|
||||
|
||||
Parameters:
|
||||
- `path` — the file to write
|
||||
- `data` — a `[]byte`, e.g. from <code>buffer(n)</code>
|
||||
- `count` — how many bytes of it
|
||||
|
||||
```ludic
|
||||
program Save {
|
||||
entry {
|
||||
let b = buffer(2)
|
||||
b[0] = 72
|
||||
b[1] = 105
|
||||
print(Fs.write_bytes("hi.bin", b, 2))
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/input/input-text.md
Normal file
24
docs/language/input/input-text.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: input-text
|
||||
name: Input.text
|
||||
category: input
|
||||
kind: namespace-method
|
||||
tokens: Input.text
|
||||
sig: Input.text() -> string
|
||||
tip: The text typed this frame.
|
||||
order: 61
|
||||
ns: Input
|
||||
member: text
|
||||
---
|
||||
|
||||
What the keyboard layout, the modifiers and any dead key produced this frame, as UTF-8 - the only right way to fill a name field, because it is the characters the player meant rather than the keys they pressed (a binding reads keys by position; text reads what they type). Empty in a headless run.
|
||||
|
||||
```ludic
|
||||
program Name {
|
||||
entry {
|
||||
var name = ""
|
||||
name = name + Input.text()
|
||||
print(name)
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/time/time-now_us.md
Normal file
23
docs/language/time/time-now_us.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: time-now_us
|
||||
name: Time.now_us
|
||||
category: time
|
||||
kind: namespace-method
|
||||
tokens: Time.now_us
|
||||
sig: Time.now_us() -> long
|
||||
tip: A microsecond clock, for measuring.
|
||||
order: 7
|
||||
ns: Time
|
||||
member: now_us
|
||||
---
|
||||
|
||||
Returns a steady microsecond count from the platform clock, for timing a piece of work - the difference between two readings is how long it took. It is not the game's time (that is <a href="time-now"><code>Time.now</code></a>) and it does not stop when the game is paused.
|
||||
|
||||
```ludic
|
||||
program Timed {
|
||||
entry {
|
||||
let t0 = Time.now_us()
|
||||
print(Time.now_us() - t0)
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/time/time-sleep_us.md
Normal file
26
docs/language/time/time-sleep_us.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: time-sleep_us
|
||||
name: Time.sleep_us
|
||||
category: time
|
||||
kind: namespace-method
|
||||
tokens: Time.sleep_us
|
||||
sig: Time.sleep_us(us)
|
||||
tip: Waits this many microseconds.
|
||||
order: 8
|
||||
ns: Time
|
||||
member: sleep_us
|
||||
---
|
||||
|
||||
Suspends the program for about `us` microseconds - a high-resolution wait, for pacing a loop to a frame rate. No operating system's sleep is exact, so a limiter sleeps most of the wait and spins the last of it.
|
||||
|
||||
Parameters:
|
||||
- `us` — how long to wait, in microseconds (a `long`)
|
||||
|
||||
```ludic
|
||||
program Paced {
|
||||
entry {
|
||||
Time.sleep_us(1000)
|
||||
print(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -239,6 +239,30 @@ function collect_impl_pairs(allsrc: pointer, pairs: []pointer) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# {ns}.{method} pairs a namespace declares with `alias` (L6) - the engine's in
|
||||
# runtime/native/namespaces.ludic - which the compiler dispatches as surely as its own
|
||||
function collect_alias_pairs(pairs: []pointer) -> void {
|
||||
let src = read_file("runtime/native/namespaces.ludic")
|
||||
if src == null { return }
|
||||
var ns = ""
|
||||
let n = slen(src)
|
||||
var i = 0
|
||||
while i < n {
|
||||
let ln = s_trim(line_at(src, i))
|
||||
i = i + slen(line_at(src, i)) + 1
|
||||
if s_starts(ln, "namespace ") {
|
||||
var e = 10
|
||||
while e < slen(ln) and ln[e] != ' ' and ln[e] != '{' { e += 1 }
|
||||
ns = sslice(ln, 10, e)
|
||||
}
|
||||
if s_starts(ln, "alias ") and slen(ns) > 0 {
|
||||
var e = 6
|
||||
while e < slen(ln) and ln[e] != '(' and ln[e] != ' ' and ln[e] != '=' { e += 1 }
|
||||
set_add(pairs, ns + "." + sslice(ln, 6, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# gather the docs facts: token set, and the (ns.member) pairs of ns-method pages
|
||||
function collect_docs(tokens: []pointer, docpairs: []pointer) -> void {
|
||||
let list = capture("find docs/language -name '*.md' ! -name '_section.md' 2>/dev/null")
|
||||
|
|
@ -298,6 +322,7 @@ function cmd_check_impl() -> int {
|
|||
let allsrc = read_all_selfhost()
|
||||
let impl = new []pointer
|
||||
collect_impl_pairs(allsrc, impl)
|
||||
collect_alias_pairs(impl)
|
||||
let tokens = new []pointer
|
||||
let docpairs = new []pointer
|
||||
collect_docs(tokens, docpairs)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,26 @@ function cmd_test_tools() -> int {
|
|||
if shq(`{fmt} {tmp_dir()}/q.ludic | grep -q '\\[Pos, {{Foe}}\\]'`) { ok("query tags stay tight ({Foe})") } else { bad("query tags stay tight") }
|
||||
write_file(`{tmp_dir()}/al.ludic`, "program A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n")
|
||||
if shq(`{fmt} {tmp_dir()}/al.ludic | grep -q 'R_A: int = 0 # one'`) { ok("hand alignment preserved") } else { bad("hand alignment preserved") }
|
||||
write_file(`{tmp_dir()}/gen.ludic`, "program G {\n function f<T>(p: Pool<T>, m: Map<string, []int>) -> Pool<Pool<T>> {\n let ok = a < b and c > d\n return null\n }\n}\n")
|
||||
if shq(`{fmt} {tmp_dir()}/gen.ludic | cmp -s - {tmp_dir()}/gen.ludic`) { ok("type arguments stay together, comparisons keep their spaces (L5)") } else { bad("type arguments stay together (L5)") }
|
||||
|
||||
# --- the style rules a project states (L10) --------------------------------
|
||||
let lp = `{tmp_dir()}/lintproj`
|
||||
let lfmt = `{capture_line("pwd")}/{fmt}` # the tests below run from inside the project
|
||||
shell(`rm -rf {lp} && mkdir -p {lp}/src`)
|
||||
write_file(`{lp}/package.ludic`, "package \"t\"\nlint one_statement\nlint max_file_lines 20\nlint max_function_lines 5\nlint max_comment_lines 2\nlint max_header_lines 3\nlint paths \"src\"\nlint baseline \"lint-baseline.txt\"\n")
|
||||
write_file(`{lp}/src/a.ludic`, "# a.ludic - a file\nfunction f() -> int {\n # one\n # two\n # three\n let a = 1; return a\n}\n")
|
||||
if not shq(`cd {lp} && {lfmt} --lint > out.txt 2>&1`) and shq(`grep -q 'src/a.ludic:3: max_comment_lines' {lp}/out.txt`) and shq(`grep -q 'src/a.ludic:6: one_statement' {lp}/out.txt`) {
|
||||
ok("--lint fails a comment over the limit and two statements on a line, at their lines")
|
||||
} else { bad("--lint fails a comment over the limit and two statements on a line") }
|
||||
if shq(`cd {lp} && {lfmt} --init-baseline > /dev/null && {lfmt} --lint > /dev/null`) { ok("--init-baseline allows what is there") } else { bad("--init-baseline allows what is there") }
|
||||
write_file(`{lp}/src/b.ludic`, "function g() -> int {\n let b = 2; return b\n}\n")
|
||||
if not shq(`cd {lp} && {lfmt} --lint > /dev/null 2>&1`) { ok("a new violation fails, whatever the baseline allows elsewhere") } else { bad("a new violation fails") }
|
||||
write_file(`{lp}/src/b.ludic`, "function g() -> int { return 2 }\n")
|
||||
write_file(`{lp}/src/a.ludic`, "# a.ludic - a file\nfunction f() -> int {\n let a = 1; return a\n}\n")
|
||||
if shq(`cd {lp} && {lfmt} --lint > /dev/null && ! grep -q max_comment_lines lint-baseline.txt`) { ok("fixing one lowers the baseline, and it stays lowered") } else { bad("fixing one lowers the baseline") }
|
||||
write_file(`{lp}/src/c.ludic`, "function h() -> string { return \"a; b\" }\n")
|
||||
if shq(`cd {lp} && {lfmt} --lint > /dev/null`) { ok("a ; inside a string is not a statement") } else { bad("a ; inside a string is not a statement") }
|
||||
|
||||
# --- --check exit status, for CI and pre-commit hooks ----------------------
|
||||
let chk1 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
# ludic-fmt a.md format the ```ludic fences in a document
|
||||
# cat a.ludic | ludic-fmt - filter mode (stdin -> stdout)
|
||||
program LudicFmt {
|
||||
import "fmt_lint.ludic"
|
||||
import "fmt_lint_rules.ludic"
|
||||
import "fmt_lint_run.ludic"
|
||||
# ---- token kinds (mirror ludic_syntax.h) ----
|
||||
const LT_EOF: int = 0
|
||||
const LT_NL: int = 1
|
||||
|
|
@ -589,6 +592,8 @@ program LudicFmt {
|
|||
var quiet = false
|
||||
var changed = false
|
||||
var failed = false
|
||||
var lint_only = false
|
||||
var lint_init = false
|
||||
let files = new []pointer
|
||||
var ai = 1
|
||||
while ai < arg_count() {
|
||||
|
|
@ -597,11 +602,32 @@ program LudicFmt {
|
|||
else { if (a == "--check") or (a == "-l") { check = true }
|
||||
else { if (a == "-q") or (a == "--quiet") { quiet = true }
|
||||
else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } }
|
||||
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source"); return }
|
||||
else { push(files, a) } } } } }
|
||||
else { if (a == "--lint") { lint_only = true }
|
||||
else { if (a == "--init-baseline") { lint_only = true; lint_init = true }
|
||||
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source; --check also checks package.ludic's lint rules, --lint checks the project"); return }
|
||||
else { push(files, a) } } } } } } }
|
||||
ai += 1
|
||||
}
|
||||
if indent < 1 or indent > 8 { indent = 2 }
|
||||
# --lint: the project's rules over its paths, against the baseline (L10)
|
||||
if lint_only {
|
||||
if not lint_config() { print("ludic-fmt --lint: package.ludic states no `lint` rules"); exit(2) }
|
||||
let all = new []string
|
||||
var li = 0
|
||||
while li < len(lt_paths) {
|
||||
lint_walk(lt_paths[li], all)
|
||||
li += 1
|
||||
}
|
||||
var lj = 0
|
||||
while lj < len(all) {
|
||||
let t = read_file(all[lj])
|
||||
if t != null { lint_file(all[lj], string(t)) }
|
||||
lj += 1
|
||||
}
|
||||
if not lint_judge(lint_init) { exit(1) }
|
||||
return
|
||||
}
|
||||
let linting = check and lint_config()
|
||||
|
||||
# stdin filter mode
|
||||
if len(files) == 0 or (len(files) == 1 and (files[0] == "-")) {
|
||||
|
|
@ -620,6 +646,7 @@ program LudicFmt {
|
|||
file_write(file_stderr(), m, cstr_len(m)); failed = true
|
||||
} else {
|
||||
let is_md = ends_with(path, ".md") or ends_with(path, ".markdown")
|
||||
if linting and not is_md { lint_file(path, string(text)) }
|
||||
let out = format_source(text, is_md, indent)
|
||||
let same = (out == text)
|
||||
if check {
|
||||
|
|
@ -638,6 +665,11 @@ program LudicFmt {
|
|||
fi += 1
|
||||
}
|
||||
if failed { exit(2) }
|
||||
# the files named are part of a project: judge them, but never rewrite its baseline
|
||||
if linting {
|
||||
lt_partial = true
|
||||
if not lint_judge(false) { exit(1) }
|
||||
}
|
||||
if check and changed { exit(1) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
111
tools/ludic-tools/fmt_lint.ludic
Normal file
111
tools/ludic-tools/fmt_lint.ludic
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# fmt_lint.ludic — L10: the style rules, checked by the formatter. A project states them in its
|
||||
# package.ludic, and `ludic-fmt --check` (the files given) or `ludic-fmt --lint` (the project's
|
||||
# paths) fails a file that breaks one:
|
||||
# lint one_statement two statements on one line (`a(); b()`)
|
||||
# lint max_file_lines 100 a file's length
|
||||
# lint max_function_lines 50 a function's, from `function` to its closing brace
|
||||
# lint max_comment_lines 2 a block of comment lines - a comment says why, in a line or two
|
||||
# lint max_header_lines 6 the block that opens a file, which says what the file is
|
||||
# lint paths "src" "lab" what --lint walks
|
||||
# lint baseline "tests/lint-baseline.txt"
|
||||
# The baseline is a ratchet: `<count> <rule> <file>` lines, the violations a file had when a rule
|
||||
# came in. A file may have that many and no more; when it has fewer the baseline is lowered, and
|
||||
# `--init-baseline` writes it from what is there now.
|
||||
var lt_one: bool = false
|
||||
var lt_max_file: int = 0
|
||||
var lt_max_fn: int = 0
|
||||
var lt_max_comment: int = 0
|
||||
var lt_max_header: int = 0
|
||||
var lt_baseline: string = ""
|
||||
var lt_paths: []string = new []string
|
||||
var lr_file: []string = new []string
|
||||
var lr_rule: []string = new []string
|
||||
var lr_line: []int = new []int
|
||||
var lr_msg: []string = new []string
|
||||
|
||||
function lt_atoi(s: string) -> int {
|
||||
var n = 0
|
||||
var i = 0
|
||||
while i < len(s) {
|
||||
let c = s[i]
|
||||
if c >= '0' and c <= '9' { n = n * 10 + (c - '0') }
|
||||
i += 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
function lt_unquote(s: string) -> string {
|
||||
let n = len(s)
|
||||
if n >= 2 and s[0] == '"' and s[n - 1] == '"' { return s[1 .. n - 1] }
|
||||
return s
|
||||
}
|
||||
# the words of a line, a "quoted" one kept whole, a # ending it
|
||||
function lt_words(line: string) -> []string {
|
||||
let out = new []string
|
||||
var i = 0
|
||||
let n = len(line)
|
||||
while i < n {
|
||||
while i < n and (line[i] == ' ' or line[i] == 9) { i += 1 }
|
||||
if i >= n or line[i] == '#' { break }
|
||||
var j = i
|
||||
if line[i] == '"' {
|
||||
j = i + 1
|
||||
while j < n and line[j] != '"' { j += 1 }
|
||||
if j < n { j += 1 }
|
||||
} else {
|
||||
while j < n and line[j] != ' ' and line[j] != 9 { j += 1 }
|
||||
}
|
||||
push(out, lt_unquote(line[i .. j]))
|
||||
i = j
|
||||
}
|
||||
return out
|
||||
}
|
||||
function lt_lines(text: string) -> []string {
|
||||
let out = new []string
|
||||
var a = 0
|
||||
var i = 0
|
||||
let n = len(text)
|
||||
while i < n {
|
||||
if text[i] == '\n' {
|
||||
push(out, text[a .. i])
|
||||
a = i + 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if a < n { push(out, text[a .. n]) }
|
||||
return out
|
||||
}
|
||||
# the project's rules, from package.ludic in the directory ludic-fmt runs in
|
||||
function lint_config() -> bool {
|
||||
let man = read_file("package.ludic")
|
||||
if man == null { return false }
|
||||
var any = false
|
||||
let ls = lt_lines(string(man))
|
||||
var i = 0
|
||||
while i < len(ls) {
|
||||
let w = lt_words(ls[i])
|
||||
if len(w) >= 2 and w[0] == "lint" {
|
||||
any = true
|
||||
if w[1] == "one_statement" { lt_one = true }
|
||||
if w[1] == "max_file_lines" and len(w) > 2 { lt_max_file = lt_atoi(w[2]) }
|
||||
if w[1] == "max_function_lines" and len(w) > 2 { lt_max_fn = lt_atoi(w[2]) }
|
||||
if w[1] == "max_comment_lines" and len(w) > 2 { lt_max_comment = lt_atoi(w[2]) }
|
||||
if w[1] == "max_header_lines" and len(w) > 2 { lt_max_header = lt_atoi(w[2]) }
|
||||
if w[1] == "baseline" and len(w) > 2 { lt_baseline = w[2] }
|
||||
if w[1] == "paths" {
|
||||
var k = 2
|
||||
while k < len(w) {
|
||||
push(lt_paths, w[k])
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return any
|
||||
}
|
||||
function lt_report(path: string, rule: string, line: int, msg: string) -> void {
|
||||
push(lr_file, path)
|
||||
push(lr_rule, rule)
|
||||
push(lr_line, line)
|
||||
push(lr_msg, msg)
|
||||
}
|
||||
109
tools/ludic-tools/fmt_lint_rules.ludic
Normal file
109
tools/ludic-tools/fmt_lint_rules.ludic
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# fmt_lint_rules.ludic — L10: the rules themselves, over one file's text and its tokens
|
||||
function lint_file(path: string, text: string) -> void {
|
||||
let ls = lt_lines(text)
|
||||
if lt_max_file > 0 and len(ls) > lt_max_file { lt_report(path, "max_file_lines", len(ls), `{len(ls)} lines (the limit is {lt_max_file})`) }
|
||||
lint_comments(path, ls)
|
||||
lex(text)
|
||||
if lt_one { lint_statements(path) }
|
||||
if lt_max_fn > 0 { lint_functions(path) }
|
||||
}
|
||||
function lint_comments(path: string, ls: []string) -> void {
|
||||
var run = 0
|
||||
var start = 0
|
||||
var code_seen = false
|
||||
var i = 0
|
||||
while i <= len(ls) {
|
||||
var is_c = false
|
||||
var blank = true
|
||||
if i < len(ls) {
|
||||
let w = lt_words_raw(ls[i])
|
||||
is_c = w == 1
|
||||
blank = w == 0
|
||||
}
|
||||
if is_c {
|
||||
if run == 0 { start = i }
|
||||
run += 1
|
||||
} else {
|
||||
if run > 0 {
|
||||
let header = not code_seen and start == 0
|
||||
if header and lt_max_header > 0 and run > lt_max_header { lt_report(path, "max_header_lines", start + 1, `the opening comment is {run} lines (the limit is {lt_max_header})`) }
|
||||
if not header and lt_max_comment > 0 and run > lt_max_comment { lt_report(path, "max_comment_lines", start + 1, `a comment of {run} lines (the limit is {lt_max_comment}): say why in a line or two; the story goes in the commit or docs/`) }
|
||||
run = 0
|
||||
}
|
||||
if not blank { code_seen = true }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
# 0 a blank line, 1 a comment line, 2 code
|
||||
function lt_words_raw(line: string) -> int {
|
||||
var i = 0
|
||||
while i < len(line) and (line[i] == ' ' or line[i] == 9) { i += 1 }
|
||||
if i >= len(line) { return 0 }
|
||||
if line[i] == '#' { return 1 }
|
||||
return 2
|
||||
}
|
||||
function lint_statements(path: string) -> void {
|
||||
var i = 0
|
||||
while i < ntok() {
|
||||
if tk_kind[i] == LT_OP and tok_len(i) == 1 and src[tk_start[i]] == ';' {
|
||||
let k = tk_kind[i + 1]
|
||||
if k != LT_NL and k != LT_COMMENT and k != LT_EOF { lt_report(path, "one_statement", tk_line[i] + 1, "two statements on one line") }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function lint_functions(path: string) -> void {
|
||||
var i = 0
|
||||
while i < ntok() {
|
||||
if tk_kind[i] == LT_KW and (tok_text(i) == "function") {
|
||||
let end = lt_fn_end(i)
|
||||
if end >= 0 {
|
||||
let n = tk_line[end] - tk_line[i] + 1
|
||||
if n > lt_max_fn { lt_report(path, "max_function_lines", tk_line[i] + 1, `{lt_fn_name(i)} is {n} lines (the limit is {lt_max_fn})`) }
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function lt_fn_name(i: int) -> string {
|
||||
if i + 1 < ntok() { return string(tok_text(i + 1)) }
|
||||
return "a function"
|
||||
}
|
||||
# the token closing a function's body, or -1 for one with none (an extern)
|
||||
function lt_fn_end(i: int) -> int {
|
||||
var j = i + 1
|
||||
var paren = 0
|
||||
var closed = false
|
||||
while j < ntok() {
|
||||
let k = tk_kind[j]
|
||||
if k == LT_OP and tok_len(j) == 1 {
|
||||
let c = src[tk_start[j]]
|
||||
if c == '(' { paren += 1 }
|
||||
if c == ')' {
|
||||
paren -= 1
|
||||
if paren == 0 { closed = true }
|
||||
}
|
||||
if c == '{' and paren == 0 and closed { return lt_close(j) }
|
||||
}
|
||||
if (k == LT_NL or k == LT_EOF) and paren == 0 and closed { return -1 }
|
||||
j += 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
function lt_close(open: int) -> int {
|
||||
var depth = 0
|
||||
var j = open
|
||||
while j < ntok() {
|
||||
if tk_kind[j] == LT_OP and tok_len(j) == 1 {
|
||||
let c = src[tk_start[j]]
|
||||
if c == '{' { depth += 1 }
|
||||
if c == '}' {
|
||||
depth -= 1
|
||||
if depth == 0 { return j }
|
||||
}
|
||||
}
|
||||
j += 1
|
||||
}
|
||||
return ntok() - 1
|
||||
}
|
||||
117
tools/ludic-tools/fmt_lint_run.ludic
Normal file
117
tools/ludic-tools/fmt_lint_run.ludic
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# fmt_lint_run.ludic — L10: walking a project, and the baseline that lets a rule arrive in a
|
||||
# codebase that breaks it today without letting it get worse
|
||||
var bl_file: []string = new []string
|
||||
var bl_rule: []string = new []string
|
||||
var bl_count: []int = new []int
|
||||
var lt_partial: bool = false # --check of some files: the baseline is read, never written
|
||||
|
||||
function lint_walk(dir: string, out: []string) -> void {
|
||||
let names = Fs.list(dir)
|
||||
if names == null { return }
|
||||
var i = 0
|
||||
while i < len(names) {
|
||||
let p = `{dir}/{names[i]}`
|
||||
if Fs.is_dir(p) { lint_walk(p, out) }
|
||||
else { if ends_with(p, ".ludic") { push(out, p) } }
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function lt_baseline_read() -> void {
|
||||
if len(lt_baseline) == 0 { return }
|
||||
let t = read_file(lt_baseline)
|
||||
if t == null { return }
|
||||
let ls = lt_lines(string(t))
|
||||
var i = 0
|
||||
while i < len(ls) {
|
||||
let w = lt_words(ls[i])
|
||||
if len(w) == 3 {
|
||||
push(bl_count, lt_atoi(w[0]))
|
||||
push(bl_rule, w[1])
|
||||
push(bl_file, w[2])
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function lt_allowed(path: string, rule: string) -> int {
|
||||
var i = 0
|
||||
while i < len(bl_file) {
|
||||
if bl_file[i] == path and bl_rule[i] == rule { return bl_count[i] }
|
||||
i += 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
# how many violations of `rule` the file has now
|
||||
function lt_count(path: string, rule: string) -> int {
|
||||
var n = 0
|
||||
var i = 0
|
||||
while i < len(lr_file) {
|
||||
if lr_file[i] == path and lr_rule[i] == rule { n += 1 }
|
||||
i += 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
function lt_seen(fs: []string, rs: []string, path: string, rule: string) -> bool {
|
||||
var i = 0
|
||||
while i < len(fs) {
|
||||
if fs[i] == path and rs[i] == rule { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
# true when every file is within its allowance; prints what is not
|
||||
function lint_judge(init: bool) -> bool {
|
||||
lt_baseline_read()
|
||||
let pf = new []string
|
||||
let pr = new []string
|
||||
var i = 0
|
||||
while i < len(lr_file) {
|
||||
if not lt_seen(pf, pr, lr_file[i], lr_rule[i]) {
|
||||
push(pf, lr_file[i])
|
||||
push(pr, lr_rule[i])
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
var ok = true
|
||||
var lowered = false
|
||||
var total = 0
|
||||
let out = buf_new()
|
||||
buf_puts(out, "# lint baseline: the violations each file had when a rule came in (ludic-fmt --lint); only ever lowered\n")
|
||||
var p = 0
|
||||
while p < len(pf) {
|
||||
let now = lt_count(pf[p], pr[p])
|
||||
let allow = lt_allowed(pf[p], pr[p])
|
||||
total += now
|
||||
if not init and now > allow {
|
||||
ok = false
|
||||
lt_print_pair(pf[p], pr[p], now, allow)
|
||||
}
|
||||
var keep = now
|
||||
if not init and now > allow { keep = allow }
|
||||
if not init and now < allow { lowered = true }
|
||||
if keep > 0 { buf_puts(out, `{keep} {pr[p]} {pf[p]}\n`) }
|
||||
p += 1
|
||||
}
|
||||
var b = 0
|
||||
while b < len(bl_file) {
|
||||
if lt_count(bl_file[b], bl_rule[b]) == 0 and bl_count[b] > 0 and not init { lowered = true }
|
||||
b += 1
|
||||
}
|
||||
if len(lt_baseline) > 0 and (init or lowered) and not lt_partial {
|
||||
let f = file_open(lt_baseline, "wb")
|
||||
if f != null {
|
||||
let s = buf_str(out)
|
||||
file_write(f, s, cstr_len(s))
|
||||
file_close(f)
|
||||
}
|
||||
}
|
||||
print(` lint: {total} violation(s) in the baseline, {len(pf)} file/rule pair(s)`)
|
||||
return ok
|
||||
}
|
||||
function lt_print_pair(path: string, rule: string, now: int, allow: int) -> void {
|
||||
var i = 0
|
||||
while i < len(lr_file) {
|
||||
if lr_file[i] == path and lr_rule[i] == rule { print(`{path}:{lr_line[i]}: {rule}: {lr_msg[i]}`) }
|
||||
i += 1
|
||||
}
|
||||
if allow > 0 { print(` ({path} may have {allow} of {rule}, and has {now})`) }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue