diff --git a/LANGUAGE.md b/LANGUAGE.md index 8509d175..23244163 100644 --- a/LANGUAGE.md +++ b/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 diff --git a/changes/lint.md b/changes/lint.md new file mode 100644 index 00000000..7a3e16ac --- /dev/null +++ b/changes/lint.md @@ -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 ` 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. diff --git a/docs/language/app/app-monitor_count.md b/docs/language/app/app-monitor_count.md new file mode 100644 index 00000000..0ee0bcc0 --- /dev/null +++ b/docs/language/app/app-monitor_count.md @@ -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()) } +} +``` diff --git a/docs/language/app/app-window_fixed.md b/docs/language/app/app-window_fixed.md new file mode 100644 index 00000000..764dc429 --- /dev/null +++ b/docs/language/app/app-window_fixed.md @@ -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) } +} +``` diff --git a/docs/language/app/app-window_to_monitor.md b/docs/language/app/app-window_to_monitor.md new file mode 100644 index 00000000..f9c8f1ab --- /dev/null +++ b/docs/language/app/app-window_to_monitor.md @@ -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) } +} +``` diff --git a/docs/language/fs/fs-read_bytes.md b/docs/language/fs/fs-read_bytes.md new file mode 100644 index 00000000..92704804 --- /dev/null +++ b/docs/language/fs/fs-read_bytes.md @@ -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 null 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 text_of(b, n) 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]) } + } +} +``` diff --git a/docs/language/fs/fs-write_bytes.md b/docs/language/fs/fs-write_bytes.md new file mode 100644 index 00000000..c2b441cf --- /dev/null +++ b/docs/language/fs/fs-write_bytes.md @@ -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 buffer(n) +- `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)) + } +} +``` diff --git a/docs/language/input/input-text.md b/docs/language/input/input-text.md new file mode 100644 index 00000000..36725770 --- /dev/null +++ b/docs/language/input/input-text.md @@ -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) + } +} +``` diff --git a/docs/language/time/time-now_us.md b/docs/language/time/time-now_us.md new file mode 100644 index 00000000..fb3cd4d5 --- /dev/null +++ b/docs/language/time/time-now_us.md @@ -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 Time.now) and it does not stop when the game is paused. + +```ludic +program Timed { + entry { + let t0 = Time.now_us() + print(Time.now_us() - t0) + } +} +``` diff --git a/docs/language/time/time-sleep_us.md b/docs/language/time/time-sleep_us.md new file mode 100644 index 00000000..2589326f --- /dev/null +++ b/docs/language/time/time-sleep_us.md @@ -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) + } +} +``` diff --git a/tools/ludic-cli/checks.ludic b/tools/ludic-cli/checks.ludic index 5aeb8499..d5e95642 100644 --- a/tools/ludic-cli/checks.ludic +++ b/tools/ludic-cli/checks.ludic @@ -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) diff --git a/tools/ludic-cli/tools.ludic b/tools/ludic-cli/tools.ludic index 0d77c24f..1a041cb0 100644 --- a/tools/ludic-cli/tools.ludic +++ b/tools/ludic-cli/tools.ludic @@ -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(p: Pool, m: Map) -> Pool> {\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`) diff --git a/tools/ludic-tools/fmt.ludic b/tools/ludic-tools/fmt.ludic index 5d273075..ebb6e0d5 100644 --- a/tools/ludic-tools/fmt.ludic +++ b/tools/ludic-tools/fmt.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) } } } diff --git a/tools/ludic-tools/fmt_lint.ludic b/tools/ludic-tools/fmt_lint.ludic new file mode 100644 index 00000000..7d7ffa5a --- /dev/null +++ b/tools/ludic-tools/fmt_lint.ludic @@ -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: ` ` 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) +} diff --git a/tools/ludic-tools/fmt_lint_rules.ludic b/tools/ludic-tools/fmt_lint_rules.ludic new file mode 100644 index 00000000..13c2898e --- /dev/null +++ b/tools/ludic-tools/fmt_lint_rules.ludic @@ -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 +} diff --git a/tools/ludic-tools/fmt_lint_run.ludic b/tools/ludic-tools/fmt_lint_run.ludic new file mode 100644 index 00000000..dced52b1 --- /dev/null +++ b/tools/ludic-tools/fmt_lint_run.ludic @@ -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})`) } +}