bend-lint 0.0.0-stage → 0.1.0
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 +23 -0
- package/README.md +126 -2
- package/package.json +16 -4
- package/src/files/effs/files.c +150 -0
- package/src/files/effs/files.js +40 -0
- package/src/files/main.bend +764 -0
- package/src/json/main.bend +422 -0
- package/src/linter/cli.bend +3157 -0
- package/src/linter/directives.bend +1111 -0
- package/src/linter/fixes.bend +775 -0
- package/src/linter/seam.ts +2060 -0
- package/src/shared/bend2.ts +187 -0
- package/src/shared/result.ts +57 -0
- package/src/shared/utils.ts +105 -0
- package/src/text/main.bend +398 -0
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
|
-
#
|
|
1
|
+
# bend-lint
|
|
2
2
|
|
|
3
|
-
|
|
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.0.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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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/4113803537dd07a7c45101476d9e895fcde06703",
|
|
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);
|