bend-lint 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ SPDX-License-Identifier: MIT
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2026 Mathews Fernando
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # bend-lint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A linter and formatter for Bend 2, with rules written in Bend.
4
+
5
+ ## Install
6
+
7
+ Install bend-lint in each project, never globally:
8
+
9
+ ```sh
10
+ bun add -D --trust bend-lint
11
+ ```
12
+
13
+ The install writes `bend-lint.json` and `bend-lint-rules/format.bend`, which turns on the formatter,
14
+ and downloads Bend. Without `--trust`, Bun skips that: run `bunx bend-lint init`.
15
+
16
+ > **Why per project:** a Bend limitation. Bend imports a file only by a relative path of plain names
17
+ > (or from BendHub), so bend-lint can reach your rules only inside your project. bend-lint refuses
18
+ > to run from anywhere else.
19
+
20
+ Needs Bun 1.4.3. bend-lint downloads the Bend it is tested with, once. With clang 14 or newer, it
21
+ builds your rules natively; without clang, it builds them to JavaScript, about 3 to 8 times slower,
22
+ and says so on each run.
23
+
24
+ > **On Windows, bend-lint uses a fork of Bend**,
25
+ > [MattCozendey/bend](https://github.com/MattCozendey/bend/tree/feature/cuda-and-windows), because
26
+ > Bend's releases do not run on Windows. bend-lint says so on each run.
27
+
28
+ ## Run
29
+
30
+ ```sh
31
+ bunx bend-lint [<file or glob>...] [--fix | --fix-dangerously] [--show-suppressed] [--json]
32
+ [--jobs <number or max>]
33
+ ```
34
+
35
+ | Flag | Does |
36
+ | ------------------- | ---------------------------------------------------------------------------------- |
37
+ | `--fix` | makes no fix until Bend's checker in Bend can prove that a fix keeps the program |
38
+ | `--fix-dangerously` | applies every fix. These can change what the program does, or break it |
39
+ | `--show-suppressed` | also prints the findings that a directive covers |
40
+ | `--json` | prints one JSON object on stdout, with every fix. The summary goes to stderr |
41
+ | `--jobs` | how many files to lint at once: a number, or `max` (default: half the CPU threads) |
42
+
43
+ - With no files, it lints the `.bend` files in the folder you run it in: those git lists (so none
44
+ that `.gitignore` lists), and none in `node_modules` or a hidden folder.
45
+ - Quote globs, and use `/` in paths.
46
+ - bend-lint builds your rules on its first run (about 40 s), and keeps the build until a rule or
47
+ Bend changes.
48
+ - A fix that touches an earlier fix's edits waits for the next run.
49
+ - bend-lint runs at low priority: a long run slows down, not your computer.
50
+ - bend-lint does not report Bend's own errors: `bend <file> --check-only` does. If Bend's check of a
51
+ file fails, the rules that need its types do not run on it, and bend-lint names them on stderr.
52
+ - Exit code: 0 no errors (warnings do not count), 1 an error, 2 a usage or config error.
53
+ - The downloaded Bend, builds and cached checks go to `~/.cache/bend-lint/`. You can delete it at
54
+ any time.
55
+
56
+ ## Config
57
+
58
+ `bend-lint.json` is the only place that picks the rules: a rule runs only if `rules` names it.
59
+ bend-lint reads the nearest `bend-lint.json` above the folder it runs in.
60
+
61
+ ```json
62
+ {
63
+ "entries": ["src/main.bend"],
64
+ "imports": true,
65
+ "packages": { "jane-rules": "1.0.0.0" },
66
+ "rules": {
67
+ "format": { "tabWidth": 4, "severity": "hint" },
68
+ "style/no_foo": "error",
69
+ "style/no_bar": "off",
70
+ "jane-rules/no_baz": "warning"
71
+ }
72
+ }
73
+ ```
74
+
75
+ - `rules`: each rule to run, by its id: its path in `bend-lint-rules/`, without `.bend`
76
+ (`bend-lint-rules/style/no_foo.bend` is `style/no_foo`). Give it `"off"`, a severity (`error`,
77
+ `warning`, `information`, `hint`), or an object of its options and a `"severity"`.
78
+ - `packages`: BendHub packages of rules, names to versions. A rule whose id starts with a package's
79
+ name, then `/`, is that package's file at the rest of the id: `jane-rules/no_baz` is `no_baz.bend`
80
+ in `jane-rules@1.0.0.0`.
81
+ - `entries`: your program's entry files, as paths from the folder of `bend-lint.json`. A rule that
82
+ reads the whole program ("this def is never used") then sees it, even when you lint one file.
83
+ - `imports`: also lint every file the named file imports (not Base, nor hub packages).
84
+
85
+ bend-lint's own rules are on BendHub, as the package `bend-lint-rules` at bend-lint's version, with
86
+ `.0` added. Name it in `packages`, then turn a rule on by its id, like
87
+ `bend-lint-rules/layout/case`. Each file's header in [bend-lint-rules/](bend-lint-rules/) says what
88
+ the rule reports, what its fix does, and its options.
89
+
90
+ ## Suppress findings
91
+
92
+ ```
93
+ # bend-lint: disable-next my/rule -- <reason> not-expected-because: <why>
94
+ # bend-lint: expect-next my/rule@hint -- <reason>
95
+ ```
96
+
97
+ Form: `# bend-lint: <disable|expect>-<scope> <rule>[@severity] -- <reason>`.
98
+
99
+ | Scope | Covers |
100
+ | --------------- | -------------------------------------------- |
101
+ | `file` | the whole file |
102
+ | `begin` … `end` | the lines between them (`end` has no reason) |
103
+ | `next` | the next line with code |
104
+ | `line` | its own line, after code |
105
+
106
+ - A finding is covered when it starts on a covered line.
107
+ - `bend-lint/...` findings cannot be suppressed.
108
+ - bend-lint reports a wrong directive and an unmet `expect` as errors, and a `disable` that covers
109
+ nothing as a warning.
110
+
111
+ ## Write a rule
112
+
113
+ A rule is a file in `bend-lint-rules/`. It imports bend-lint's rule API from BendHub,
114
+ [lint.bend](src/linter/lint.bend), whose comments are the API, and defines `rule()`:
115
+
116
+ ```
117
+ import Base
118
+ import bend-lint-api@0.1.1.0/linter/lint.bend as Lint
119
+
120
+ def rule() -> Lint.Rule:
121
+ ...
122
+ ```
123
+
124
+ Then name it in `bend-lint.json`. The version is your bend-lint's, with `.0` added. After you update
125
+ bend-lint, change it if Bend's check of your rule fails.
126
+
127
+ To share rules, publish them to BendHub. A project then names your package in `packages`.
package/package.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "name": "bend-lint",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Diagnostics for Bend 2 from rules outside the language: source text, and the checker's types",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "bend-lint": "src/linter/seam.ts"
8
+ },
9
+ "type": "module",
10
+ "engines": {
11
+ "bun": "1.4.3"
12
+ },
13
+ "bendRelease": "bendlang/bend/v2.0.37",
14
+ "bendWindows": "MattCozendey/bend/8621b563d53ab015dad5af3b07e679643931d780",
15
+ "scripts": {
16
+ "postinstall": "bun ./src/linter/seam.ts postinstall"
17
+ }
18
+ }
@@ -0,0 +1,150 @@
1
+ // Files
2
+ // =====
3
+ // The C twins of files.js. A path crosses as UTF-8: Windows reads it so, as
4
+ // every Bend binary has a UTF-8 manifest.
5
+
6
+ #include <sys/stat.h>
7
+ #ifndef _WIN32
8
+ #include <dirent.h>
9
+ #endif
10
+
11
+ // The names a folder listing collects, each ended by a NUL.
12
+ typedef struct {
13
+ char* text;
14
+ u64 len;
15
+ u64 cap;
16
+ } FilesNames;
17
+
18
+ #ifdef _WIN32
19
+ // The errno of a failed Win32 call.
20
+ static int files_errno(DWORD code) {
21
+ return code == ERROR_FILE_NOT_FOUND || code == ERROR_PATH_NOT_FOUND ? ENOENT
22
+ : code == ERROR_ACCESS_DENIED ? EACCES
23
+ : code == ERROR_DIRECTORY ? ENOTDIR
24
+ : EIO;
25
+ }
26
+ #endif
27
+
28
+ static void files_put(FilesNames* names, const char* name) {
29
+ u64 k = strlen(name);
30
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
31
+ return;
32
+ }
33
+ while (names->len + k + 1 > names->cap) {
34
+ names->cap *= 2;
35
+ names->text = realloc(names->text, names->cap);
36
+ }
37
+ memcpy(names->text + names->len, name, k + 1);
38
+ names->len += k + 1;
39
+ }
40
+
41
+ // Puts the names in the folder at `path` (`n` bytes) into `names`. Gives 0,
42
+ // or the errno.
43
+ static int files_list_into(FilesNames* names, const char* path, u64 n) {
44
+ #ifdef _WIN32
45
+ char* all = malloc(n + 3);
46
+ memcpy(all, path, n);
47
+ memcpy(all + n, "/*", 3);
48
+ WIN32_FIND_DATAA found;
49
+ HANDLE h = FindFirstFileA(all, &found);
50
+ free(all);
51
+ if (h == INVALID_HANDLE_VALUE) {
52
+ return files_errno(GetLastError());
53
+ }
54
+ do {
55
+ files_put(names, found.cFileName);
56
+ } while (FindNextFileA(h, &found));
57
+ FindClose(h);
58
+ return 0;
59
+ #else
60
+ DIR* d = opendir(path);
61
+ if (d == NULL) {
62
+ return errno;
63
+ }
64
+ for (struct dirent* de = readdir(d); de != NULL; de = readdir(d)) {
65
+ files_put(names, de->d_name);
66
+ }
67
+ closedir(d);
68
+ return 0;
69
+ #endif
70
+ }
71
+
72
+ // The names in a folder, but "." and "..", each ended by a NUL, in the
73
+ // order the OS gives them.
74
+ Term files_list_run(Env e, Term* f, IoWork* w) {
75
+ u64 n = 0;
76
+ char* path = io_cstr(e, f[0], &n);
77
+ FilesNames names = { malloc(256), 0, 256 };
78
+ int code = io_nul(path, n) ? EINVAL : files_list_into(&names, path, n);
79
+ free(path);
80
+ Term r = code != 0 ? io_fail(e, code, NULL) : io_done(e, io_str(e, names.text, names.len));
81
+ free(names.text);
82
+ return r;
83
+ }
84
+
85
+ // What is at a path, links followed: 0 nothing, 1 a file, 2 a folder, 3
86
+ // something else.
87
+ Term files_kind_run(Env e, Term* f, IoWork* w) {
88
+ u64 n = 0;
89
+ char* path = io_cstr(e, f[0], &n);
90
+ struct stat st;
91
+ u32 kind = io_nul(path, n) || stat(path, &st) != 0 ? 0
92
+ : (st.st_mode & S_IFMT) == S_IFREG ? 1
93
+ : (st.st_mode & S_IFMT) == S_IFDIR ? 2
94
+ : 3;
95
+ free(path);
96
+ return (Term)kind;
97
+ }
98
+
99
+ // A path's real path: absolute, links resolved, "/" between its parts.
100
+ Term files_real_run(Env e, Term* f, IoWork* w) {
101
+ u64 n = 0;
102
+ char* path = io_cstr(e, f[0], &n);
103
+ if (io_nul(path, n)) {
104
+ free(path);
105
+ return io_fail(e, EINVAL, NULL);
106
+ }
107
+ #ifdef _WIN32
108
+ HANDLE h = CreateFileA(path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
109
+ NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
110
+ free(path);
111
+ if (h == INVALID_HANDLE_VALUE) {
112
+ return io_fail(e, files_errno(GetLastError()), NULL);
113
+ }
114
+ char buf[32768];
115
+ DWORD k = GetFinalPathNameByHandleA(h, buf, sizeof buf, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
116
+ DWORD why = GetLastError();
117
+ CloseHandle(h);
118
+ if (k == 0 || k >= sizeof buf) {
119
+ return io_fail(e, k == 0 ? files_errno(why) : ENAMETOOLONG, NULL);
120
+ }
121
+ // \\?\C:\a is C:\a, and \\?\UNC\host\a is \\host\a.
122
+ char* real = buf;
123
+ if (strncmp(buf, "\\\\?\\UNC\\", 8) == 0) {
124
+ real = buf + 6;
125
+ real[0] = '\\';
126
+ } else if (strncmp(buf, "\\\\?\\", 4) == 0) {
127
+ real = buf + 4;
128
+ }
129
+ for (char* c = real; *c != 0; c += 1) {
130
+ *c = *c == '\\' ? '/' : *c;
131
+ }
132
+ return io_done(e, io_str(e, real, strlen(real)));
133
+ #else
134
+ char* real = realpath(path, NULL);
135
+ int code = errno;
136
+ free(path);
137
+ if (real == NULL) {
138
+ return io_fail(e, code, NULL);
139
+ }
140
+ Term r = io_done(e, io_str(e, real, strlen(real)));
141
+ free(real);
142
+ return r;
143
+ #endif
144
+ }
145
+
146
+ static void __attribute__((constructor)) files_use(void) {
147
+ io_eff(CID(list.raw), files_list_run);
148
+ io_eff(CID(kind), files_kind_run);
149
+ io_eff(CID(real), files_real_run);
150
+ }
@@ -0,0 +1,40 @@
1
+ // Files
2
+ // =====
3
+ // The JS twins of files.c.
4
+
5
+ function files_list(path) {
6
+ if (path.includes("\0")) return io_fail(22);
7
+ try {
8
+ return io_done(
9
+ require("fs")
10
+ .readdirSync(path)
11
+ .map((name) => name + "\0")
12
+ .join(""),
13
+ );
14
+ } catch (e) {
15
+ return io_fail(io_code(e));
16
+ }
17
+ }
18
+
19
+ function files_kind(path) {
20
+ if (path.includes("\0")) return 0;
21
+ try {
22
+ const st = require("fs").statSync(path);
23
+ return st.isFile() ? 1 : st.isDirectory() ? 2 : 3;
24
+ } catch {
25
+ return 0;
26
+ }
27
+ }
28
+
29
+ function files_real(path) {
30
+ if (path.includes("\0")) return io_fail(22);
31
+ try {
32
+ return io_done(require("fs").realpathSync.native(path).replaceAll("\\", "/"));
33
+ } catch (e) {
34
+ return io_fail(io_code(e));
35
+ }
36
+ }
37
+
38
+ io_eff(CID(list.raw), files_list);
39
+ io_eff(CID(kind), files_kind);
40
+ io_eff(CID(real), files_real);