bend-lint 0.1.0 → 0.2.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/README.md +23 -10
- package/package.json +4 -4
- package/src/files/main.bend +2 -2
- package/src/json/main.bend +2 -2
- package/src/linter/cli.bend +386 -118
- package/src/linter/defaults.json +1 -0
- package/src/linter/directives.bend +4 -4
- package/src/linter/fixes.bend +73 -8
- package/src/linter/seam.ts +215 -46
- package/src/shared/utils.ts +2 -0
- package/src/text/main.bend +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
["language/decreasing_call","language/forward_reference","language/reused_binder","language/unneeded_plus","layout/canon_import_path","layout/case","layout/directive_order","layout/format","layout/imports","no_unused/imports","performance/append_in_loop","performance/bang_after_fork","performance/dead_accumulator","performance/deep_call_in_fork","performance/drop_in_bang","performance/eager_branch","performance/gpu_fit","performance/list_index_loop","performance/loop_invariant_call","performance/missed_fork","performance/repeated_call","performance/template_value_field","performance/unused_result"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Base
|
|
2
|
-
import bend-lint-api@0.
|
|
3
|
-
import bend-lint-api@0.
|
|
4
|
-
import bend-lint-api@0.
|
|
2
|
+
import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
|
|
3
|
+
import bend-lint-api@0.2.0.0/shared/names.bend as Names
|
|
4
|
+
import bend-lint-api@0.2.0.0/shared/strings.bend as Strings
|
|
5
5
|
import ../text/main.bend as Text
|
|
6
|
-
import bend-lint-api@0.
|
|
6
|
+
import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
|
|
7
7
|
|
|
8
8
|
# Scanning
|
|
9
9
|
# ========
|
package/src/linter/fixes.bend
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# The fixes a run makes with `--fix-dangerously`: no fix is proven to keep the program until Bend's checker is Bend.
|
|
2
2
|
import Base
|
|
3
3
|
import ../files/main.bend as Files
|
|
4
|
-
import bend-lint-api@0.
|
|
5
|
-
import bend-lint-api@0.
|
|
6
|
-
import bend-lint-api@0.
|
|
4
|
+
import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
|
|
5
|
+
import bend-lint-api@0.2.0.0/shared/names.bend as Names
|
|
6
|
+
import bend-lint-api@0.2.0.0/shared/sets.bend as Sets
|
|
7
|
+
import bend-lint-api@0.2.0.0/shared/strings.bend as Strings
|
|
7
8
|
import ../text/main.bend as Text
|
|
8
|
-
import bend-lint-api@0.
|
|
9
|
+
import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
|
|
9
10
|
|
|
10
11
|
# Types
|
|
11
12
|
# =====
|
|
@@ -149,7 +150,7 @@ def Rename.is_allowed(
|
|
|
149
150
|
case True{} True{} _:
|
|
150
151
|
False{}
|
|
151
152
|
case True{} False{} Some{Lint.Span{path, beg, end}}:
|
|
152
|
-
Bool.not(
|
|
153
|
+
Bool.not(Sets.has(taken, Taken.key(path, alias, to)))
|
|
153
154
|
case True{} False{} None{}:
|
|
154
155
|
False{}
|
|
155
156
|
|
|
@@ -727,16 +728,80 @@ def write.done(path: String, written: Bool) -> IO(Unit):
|
|
|
727
728
|
case False{}:
|
|
728
729
|
IO.die(Unit, 2, "bend-lint: could not write " ++ path)
|
|
729
730
|
|
|
731
|
+
# Check both prefixes because the pinned String.eq uses the JavaScript stack for each character.
|
|
732
|
+
def write.read(
|
|
733
|
+
+path: String,
|
|
734
|
+
+text: String,
|
|
735
|
+
edits: List<&2, Text.Edit>,
|
|
736
|
+
read: File & Result<&1, &1, U32 & String, String>
|
|
737
|
+
) -> IO(Unit):
|
|
738
|
+
match read:
|
|
739
|
+
case Tuple{file, Done{+current}}:
|
|
740
|
+
do IO<Unit>:
|
|
741
|
+
File.close(file)
|
|
742
|
+
Bool.pick(
|
|
743
|
+
IO(Unit),
|
|
744
|
+
Bool.and(String.starts_with(text, current), String.starts_with(current, text)),
|
|
745
|
+
IO.pure(Unit, Unit{}),
|
|
746
|
+
IO.die(
|
|
747
|
+
Unit,
|
|
748
|
+
2,
|
|
749
|
+
"bend-lint: " ++ path ++ ": file changed during lint; run the command again"
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
IO.bind(Bool, Unit, Files.write(path, Text.apply(edits, text)), w => write.done(path, w))
|
|
753
|
+
case Tuple{file, Fail{Tuple{code, why}}}:
|
|
754
|
+
do IO<Unit>:
|
|
755
|
+
File.close(file)
|
|
756
|
+
IO.die(Unit, 2, "bend-lint: could not read " ++ path ++ ": " ++ why)
|
|
757
|
+
|
|
758
|
+
# Read one block to keep UTF-8 characters whole, with one extra byte to detect growth.
|
|
759
|
+
def write.sized(
|
|
760
|
+
path: String,
|
|
761
|
+
text: String,
|
|
762
|
+
edits: List<&2, Text.Edit>,
|
|
763
|
+
sized: File & Result<&1, &1, U32 & String, U32>
|
|
764
|
+
) -> IO(Unit):
|
|
765
|
+
match sized:
|
|
766
|
+
case Tuple{file, Done{+size}}:
|
|
767
|
+
IO.bind(
|
|
768
|
+
File & Result<&1, &1, U32 & String, String>,
|
|
769
|
+
Unit,
|
|
770
|
+
File.read(file, U32.max(size, (size + 1: U32))),
|
|
771
|
+
read => write.read(path, text, edits, read)
|
|
772
|
+
)
|
|
773
|
+
case Tuple{file, Fail{Tuple{code, why}}}:
|
|
774
|
+
do IO<Unit>:
|
|
775
|
+
File.close(file)
|
|
776
|
+
IO.die(Unit, 2, "bend-lint: could not read " ++ path ++ ": " ++ why)
|
|
777
|
+
|
|
778
|
+
def write.opened(
|
|
779
|
+
path: String,
|
|
780
|
+
text: String,
|
|
781
|
+
edits: List<&2, Text.Edit>,
|
|
782
|
+
opened: Result<&1, &1, U32 & String, File>
|
|
783
|
+
) -> IO(Unit):
|
|
784
|
+
match opened:
|
|
785
|
+
case Done{file}:
|
|
786
|
+
IO.bind(
|
|
787
|
+
File & Result<&1, &1, U32 & String, U32>,
|
|
788
|
+
Unit,
|
|
789
|
+
File.size(file),
|
|
790
|
+
sized => write.sized(path, text, edits, sized)
|
|
791
|
+
)
|
|
792
|
+
case Fail{Tuple{code, why}}:
|
|
793
|
+
IO.die(Unit, 2, "bend-lint: could not read " ++ path ++ ": " ++ why)
|
|
794
|
+
|
|
730
795
|
def write.edited(+path: String, text: String, edits: List<&2, Text.Edit>) -> IO(Unit):
|
|
731
796
|
match edits:
|
|
732
797
|
case Nil{}:
|
|
733
798
|
IO.pure(Unit, Unit{})
|
|
734
799
|
case Con{e, rest}:
|
|
735
800
|
IO.bind(
|
|
736
|
-
|
|
801
|
+
Result<&1, &1, U32 & String, File>,
|
|
737
802
|
Unit,
|
|
738
|
-
|
|
739
|
-
|
|
803
|
+
File.open(path, "r"),
|
|
804
|
+
opened => write.opened(path, text, Con{e, rest}, opened)
|
|
740
805
|
)
|
|
741
806
|
|
|
742
807
|
def write.all(sources: List<&2, Lint.Source>, +groups: Map<&2, List<&2, Text.Edit>>) -> IO(Unit):
|
package/src/linter/seam.ts
CHANGED
|
@@ -223,13 +223,23 @@ export const BUILD = url.fileURLToPath(new URL("build/", import.meta.url));
|
|
|
223
223
|
const CONFIG = "bend-lint.json";
|
|
224
224
|
const RULES = "bend-lint-rules";
|
|
225
225
|
|
|
226
|
+
// bend-lint's version, as its BendHub packages have it: with `.0` added.
|
|
227
|
+
const HUB_VERSION = `${pkg.version}.0`;
|
|
228
|
+
|
|
226
229
|
// The rule API, as rules import it from BendHub: src/linter/lint.bend and the files it imports, at
|
|
227
230
|
// their paths from src/, in the package bend-lint-api, at bend-lint's version (see ship.ts).
|
|
228
|
-
export const API = `bend-lint-api@${
|
|
231
|
+
export const API = `bend-lint-api@${HUB_VERSION}`;
|
|
229
232
|
|
|
230
233
|
// bend-lint's own rules, as projects get them from BendHub, at bend-lint's version: each at its
|
|
231
234
|
// path from bend-lint-rules/ (see ship.ts).
|
|
232
|
-
|
|
235
|
+
const DEFAULTS_NAME = "bend-lint-rules";
|
|
236
|
+
export const DEFAULTS = `${DEFAULTS_NAME}@${HUB_VERSION}`;
|
|
237
|
+
|
|
238
|
+
// The ids of bend-lint's own rules in DEFAULTS, as the npm package lists them (see ship.ts); none
|
|
239
|
+
// in this repo, which runs them from bend-lint-rules/.
|
|
240
|
+
const DEFAULT_IDS = tryGetJson(url.fileURLToPath(new URL("defaults.json", import.meta.url))) as
|
|
241
|
+
| string[]
|
|
242
|
+
| undefined;
|
|
233
243
|
|
|
234
244
|
// A path Bend imports: up, then names (letters, digits, _ and -).
|
|
235
245
|
const PLAIN = /^(\.\.\/)*([A-Za-z_][\w-]*\/)*[A-Za-z_][\w-]*\.bend$/;
|
|
@@ -238,17 +248,20 @@ const PLAIN = /^(\.\.\/)*([A-Za-z_][\w-]*\/)*[A-Za-z_][\w-]*\.bend$/;
|
|
|
238
248
|
const PER_PROJECT =
|
|
239
249
|
"install bend-lint in this project (bun add -D bend-lint): Bend imports files only by relative paths of plain names, so bend-lint cannot reach your rules from elsewhere";
|
|
240
250
|
|
|
241
|
-
// What
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
251
|
+
// What a run that prepares says first (see `prepare`).
|
|
252
|
+
const PREPARING =
|
|
253
|
+
"bend-lint: preparing Bend and the build of the rules; a build takes about a minute";
|
|
254
|
+
|
|
255
|
+
// What `bend-lint init` writes: a config that turns on bend-lint's formatter.
|
|
256
|
+
const INIT_CONFIG =
|
|
257
|
+
JSON.stringify(
|
|
258
|
+
{
|
|
259
|
+
packages: { [DEFAULTS_NAME]: HUB_VERSION },
|
|
260
|
+
rules: { [DEFAULTS_NAME + "/layout/format"]: "warning" },
|
|
261
|
+
},
|
|
262
|
+
null,
|
|
263
|
+
2,
|
|
264
|
+
) + "\n";
|
|
252
265
|
|
|
253
266
|
// An import by a relative path: of a .bend file, or, quoted, of an effect's.
|
|
254
267
|
export const IMPORT = /^\s*import\s+"?(\.\.?\/[^\s"]+)/gm;
|
|
@@ -398,6 +411,14 @@ ${TRACED.map(
|
|
|
398
411
|
// The text of each file bend.ts read while the check that runs read it, by the path it read.
|
|
399
412
|
const READS = new Map<string, string>();
|
|
400
413
|
|
|
414
|
+
// The texts that `--overlays` gives on stdin, by their files' keys (see `getOverlayKey`): a check
|
|
415
|
+
// reads each in place of its file, which need not be on disk.
|
|
416
|
+
const OVERLAYS = new Map<string, string>();
|
|
417
|
+
|
|
418
|
+
// What a run's report counts (see `--report`): whether the run built its rules; its checks, from
|
|
419
|
+
// the cache and not; and how long Bend's checker took.
|
|
420
|
+
const COUNTS = { built: false, cached: 0, checked: 0, checkerMs: 0 };
|
|
421
|
+
|
|
401
422
|
// How many syntax nodes the server made: the next one's number.
|
|
402
423
|
const MADE = { nodes: 0 };
|
|
403
424
|
|
|
@@ -481,6 +502,24 @@ const getPatchedCopy = (dir: URL): URL => {
|
|
|
481
502
|
const getRealPath = (p: nodeFs.PathLike): string =>
|
|
482
503
|
getPathWithForwardSlashes(nodeFs.realpathSync(p));
|
|
483
504
|
|
|
505
|
+
// The path of a file as Bend names it once it loads it: its folder's real path, with `/`, then its
|
|
506
|
+
// name; or, if its folder is not on disk, its absolute path.
|
|
507
|
+
const getOverlayKey = (p: string): string => {
|
|
508
|
+
const absolute = nodePath.resolve(p);
|
|
509
|
+
const folder = nodePath.dirname(absolute);
|
|
510
|
+
return (
|
|
511
|
+
(tryGet(() => getRealPath(folder)) ?? getPathWithForwardSlashes(folder)).replace(/\/$/, "") +
|
|
512
|
+
"/" +
|
|
513
|
+
nodePath.basename(absolute)
|
|
514
|
+
);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const getOverlay = (p: nodeFs.PathOrFileDescriptor): string | undefined =>
|
|
518
|
+
OVERLAYS.size === 0 || typeof p === "number" ? undefined : OVERLAYS.get(getOverlayKey(String(p)));
|
|
519
|
+
|
|
520
|
+
// The text of the file at `p`, as the run sees it: its overlay, or what is on disk.
|
|
521
|
+
const getViewText = (p: string): string => getOverlay(p) ?? nodeFs.readFileSync(p, "utf8");
|
|
522
|
+
|
|
484
523
|
// path.posix.resolve, where a drive letter is a root.
|
|
485
524
|
const getResolvedPosixPath = (...ps: string[]): string => {
|
|
486
525
|
const all = [process.cwd(), ...ps].map(getPathWithForwardSlashes);
|
|
@@ -498,9 +537,10 @@ const getRelativePosixPath = (from: string, to: string): string =>
|
|
|
498
537
|
getPathWithForwardSlashes(to).replace(DRIVE, ""),
|
|
499
538
|
);
|
|
500
539
|
|
|
501
|
-
// Node's readFileSync, which keeps in READS each text it
|
|
540
|
+
// Node's readFileSync, which reads a file's overlay in its place, and keeps in READS each text it
|
|
541
|
+
// reads.
|
|
502
542
|
const getKeptRead = ((p: nodeFs.PathOrFileDescriptor, options?: unknown) => {
|
|
503
|
-
const read = nodeFs.readFileSync(p, options as BufferEncoding);
|
|
543
|
+
const read = getOverlay(p) ?? nodeFs.readFileSync(p, options as BufferEncoding);
|
|
504
544
|
if (typeof read === "string") {
|
|
505
545
|
READS.set(String(p), read);
|
|
506
546
|
}
|
|
@@ -508,10 +548,17 @@ const getKeptRead = ((p: nodeFs.PathOrFileDescriptor, options?: unknown) => {
|
|
|
508
548
|
}) as typeof nodeFs.readFileSync;
|
|
509
549
|
|
|
510
550
|
// What the server Shares with its copy of bend.ts: Node's fs and path, with each path they give
|
|
511
|
-
// real and with `/`, as Bend resolves imports with path.posix, where a drive letter is a root;
|
|
512
|
-
//
|
|
551
|
+
// real and with `/`, as Bend resolves imports with path.posix, where a drive letter is a root; each
|
|
552
|
+
// text Bend reads kept; and each file with an overlay there, as its key.
|
|
513
553
|
const getShared = (): Shared => ({
|
|
514
|
-
fs: {
|
|
554
|
+
fs: {
|
|
555
|
+
...nodeFs,
|
|
556
|
+
existsSync: (p: nodeFs.PathLike): boolean =>
|
|
557
|
+
getOverlay(String(p)) !== undefined || nodeFs.existsSync(p),
|
|
558
|
+
realpathSync: (p: nodeFs.PathLike): string =>
|
|
559
|
+
getOverlay(String(p)) === undefined ? getRealPath(p) : getOverlayKey(String(p)),
|
|
560
|
+
readFileSync: getKeptRead,
|
|
561
|
+
},
|
|
515
562
|
path: {
|
|
516
563
|
...nodePath,
|
|
517
564
|
join: (...ps: string[]): string => getPathWithForwardSlashes(nodePath.join(...ps)),
|
|
@@ -1462,9 +1509,12 @@ const getUntemplated = (x: unknown, bok: Book): unknown => {
|
|
|
1462
1509
|
);
|
|
1463
1510
|
};
|
|
1464
1511
|
|
|
1465
|
-
// The path Bend loads a file at, as `seen` keeps it: real, with
|
|
1512
|
+
// The path Bend loads a file at, as `seen` keeps it: real, with `/`; a file with an overlay, at its
|
|
1513
|
+
// key.
|
|
1466
1514
|
const getLoadPath = (path: string): string =>
|
|
1467
|
-
|
|
1515
|
+
getOverlay(path) === undefined
|
|
1516
|
+
? (tryGet(() => getRealPath(path)) ?? getPathWithForwardSlashes(nodePath.resolve(path)))
|
|
1517
|
+
: getOverlayKey(path);
|
|
1468
1518
|
|
|
1469
1519
|
// Checks the file at `path`, as `check <path> <facts|syntax> <paths...>` asks. Its answer: `check`
|
|
1470
1520
|
// and the check's number, here 0 (see `getNumberedAnswer`), or `failed` and each file it read;
|
|
@@ -1472,6 +1522,7 @@ const getLoadPath = (path: string): string =>
|
|
|
1472
1522
|
// `getTreeRecords`). A passed check gives the facts of the files at `facts.paths` (all, if none).
|
|
1473
1523
|
// The hooks are global, so checks must not overlap.
|
|
1474
1524
|
const getCheck = async (checker: Checker, path: string, facts: Wanted): Promise<Made> => {
|
|
1525
|
+
const start = performance.now();
|
|
1475
1526
|
const { Bend, base } = await checker.getLoaded();
|
|
1476
1527
|
const book = getSeededBook(Bend, base);
|
|
1477
1528
|
const seen = new Map<string, string | null>([[Bend.BASE_BEND, ""]]);
|
|
@@ -1522,6 +1573,7 @@ const getCheck = async (checker: Checker, path: string, facts: Wanted): Promise<
|
|
|
1522
1573
|
),
|
|
1523
1574
|
);
|
|
1524
1575
|
const read = [...READS];
|
|
1576
|
+
COUNTS.checkerMs += performance.now() - start;
|
|
1525
1577
|
return passed
|
|
1526
1578
|
? { answer: [getRecord(["check", "0"]), ...program].join(""), read, held }
|
|
1527
1579
|
: {
|
|
@@ -1600,12 +1652,12 @@ const getCachePath = ({ version }: Checker, path: string, facts: Wanted): string
|
|
|
1600
1652
|
) + ".json",
|
|
1601
1653
|
);
|
|
1602
1654
|
|
|
1603
|
-
// The cached answer at `file`, if each file it read still has the text it read
|
|
1604
|
-
// entry used now.
|
|
1655
|
+
// The cached answer at `file`, if each file it read still has the text it read (its overlay's, if
|
|
1656
|
+
// it has one). A hit marks the entry used now.
|
|
1605
1657
|
const getCachedEntry = (file: string): Entry | undefined =>
|
|
1606
1658
|
tryGet(() => {
|
|
1607
1659
|
const entry = JSON.parse(nodeFs.readFileSync(file, "utf8")) as Entry;
|
|
1608
|
-
if (!entry.files.every(([p, d]) => getSha256Hex(
|
|
1660
|
+
if (!entry.files.every(([p, d]) => getSha256Hex(getViewText(p)) === d)) {
|
|
1609
1661
|
return undefined;
|
|
1610
1662
|
}
|
|
1611
1663
|
nodeFs.utimesSync(file, new Date(), new Date());
|
|
@@ -1702,6 +1754,7 @@ export const getBuiltProgram = async (source: string, dir: URL): Promise<string>
|
|
|
1702
1754
|
}
|
|
1703
1755
|
return exe;
|
|
1704
1756
|
}
|
|
1757
|
+
COUNTS.built = true;
|
|
1705
1758
|
nodeFs.mkdirSync(PROGRAMS_DIR, { recursive: true });
|
|
1706
1759
|
const part = nodeFs.mkdtempSync(nodePath.join(PROGRAMS_DIR, ".part-"));
|
|
1707
1760
|
try {
|
|
@@ -1731,6 +1784,7 @@ export const getBuiltProgram = async (source: string, dir: URL): Promise<string>
|
|
|
1731
1784
|
const getCheckAnswer = async (checker: Checker, path: string, facts: Wanted): Promise<string> => {
|
|
1732
1785
|
const file = getCachePath(checker, path, facts);
|
|
1733
1786
|
let entry = getCachedEntry(file);
|
|
1787
|
+
COUNTS[entry === undefined ? "checked" : "cached"] += 1;
|
|
1734
1788
|
if (entry === undefined) {
|
|
1735
1789
|
const made = await getCheck(checker, path, facts);
|
|
1736
1790
|
if (made.held === undefined) {
|
|
@@ -1891,6 +1945,56 @@ const getObjectEntries = (value: unknown): Array<[string, unknown]> | undefined
|
|
|
1891
1945
|
? Object.entries(value)
|
|
1892
1946
|
: undefined;
|
|
1893
1947
|
|
|
1948
|
+
// Ends the run with status 2, as `message` says on stderr, about `file` if one; a JSON run first
|
|
1949
|
+
// prints the error on stdout, as the runner does (Error.json in cli.bend).
|
|
1950
|
+
const endWithError = (json: boolean, message: string, file?: string): never => {
|
|
1951
|
+
const text = "bend-lint: " + message;
|
|
1952
|
+
if (json) {
|
|
1953
|
+
const about = file === undefined ? {} : { path: getPathWithForwardSlashes(file) };
|
|
1954
|
+
console.log(JSON.stringify({ ok: false, errors: [{ ...about, message: text }] }));
|
|
1955
|
+
}
|
|
1956
|
+
console.error(text);
|
|
1957
|
+
return process.exit(2);
|
|
1958
|
+
};
|
|
1959
|
+
|
|
1960
|
+
// What `--report` writes as a run ends, for a bug report: the versions, the backend, the run's
|
|
1961
|
+
// build and caches, the times (in ms) of its setup, of Bend's checker and of the whole run, and its
|
|
1962
|
+
// exit code. No text of a source.
|
|
1963
|
+
const getReport = (
|
|
1964
|
+
program: string,
|
|
1965
|
+
notes: string[],
|
|
1966
|
+
times: { setupMs: number; runMs: number },
|
|
1967
|
+
code: number,
|
|
1968
|
+
): object => ({
|
|
1969
|
+
bendLint: pkg.version,
|
|
1970
|
+
notes,
|
|
1971
|
+
runtime:
|
|
1972
|
+
process.versions.bun === undefined ? "node " + process.version : "bun " + process.versions.bun,
|
|
1973
|
+
platform: process.platform + " " + process.arch,
|
|
1974
|
+
backend: program.endsWith(".js") ? "javascript" : "native",
|
|
1975
|
+
program: getPathWithForwardSlashes(program),
|
|
1976
|
+
cache: {
|
|
1977
|
+
dir: getPathWithForwardSlashes(url.fileURLToPath(SERVER_CACHE_DIR)),
|
|
1978
|
+
built: COUNTS.built,
|
|
1979
|
+
checks: { cached: COUNTS.cached, checked: COUNTS.checked },
|
|
1980
|
+
},
|
|
1981
|
+
timings: {
|
|
1982
|
+
setupMs: Math.round(times.setupMs),
|
|
1983
|
+
checkerMs: Math.round(COUNTS.checkerMs),
|
|
1984
|
+
runMs: Math.round(times.runMs),
|
|
1985
|
+
},
|
|
1986
|
+
exitCode: code,
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
// Keeps the overlays of `text`, which `--overlays` reads on stdin: a JSON object of paths to texts.
|
|
1990
|
+
const readOverlays = (text: string): void => {
|
|
1991
|
+
const entries = getObjectEntries(tryGet(() => JSON.parse(text) as unknown));
|
|
1992
|
+
if (entries === undefined || entries.some(([, t]) => typeof t !== "string")) {
|
|
1993
|
+
throw new Error("--overlays reads a JSON object of paths to texts on stdin");
|
|
1994
|
+
}
|
|
1995
|
+
entries.forEach(([p, t]) => OVERLAYS.set(getOverlayKey(p), t as string));
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1894
1998
|
// What the bend-lint.json in `project` says of its rules: those of "rules" but those "off", and
|
|
1895
1999
|
// "packages". Bend checks the packages' names and versions; the runner, the rest of the config.
|
|
1896
2000
|
export const getRules = (project: string): Rules => {
|
|
@@ -1962,13 +2066,28 @@ export const getRulesProgram = (project: string, { ids, packages }: Rules): stri
|
|
|
1962
2066
|
return source;
|
|
1963
2067
|
};
|
|
1964
2068
|
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
2069
|
+
// The rules a run of a config of `rules` builds: those it turns on; but if each is one of
|
|
2070
|
+
// bend-lint's own (DEFAULT_IDS) from DEFAULTS, all of bend-lint's own. That program is the same for
|
|
2071
|
+
// each such config, so one build serves them all, made once (see `prepare`): the runner runs only
|
|
2072
|
+
// the rules its config turns on.
|
|
2073
|
+
const getBuiltRules = (rules: Rules): Rules => {
|
|
2074
|
+
const isDefault = (id: string): boolean =>
|
|
2075
|
+
id.startsWith(DEFAULTS_NAME + "/") &&
|
|
2076
|
+
(DEFAULT_IDS?.includes(id.slice(DEFAULTS_NAME.length + 1)) ?? false);
|
|
2077
|
+
return DEFAULT_IDS !== undefined &&
|
|
2078
|
+
rules.packages.get(DEFAULTS_NAME) === HUB_VERSION &&
|
|
2079
|
+
rules.ids.every(isDefault)
|
|
2080
|
+
? { ids: DEFAULT_IDS.map((id) => DEFAULTS_NAME + "/" + id), packages: rules.packages }
|
|
2081
|
+
: rules;
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
// The program of the rules a run in `project` builds.
|
|
2085
|
+
const getProjectProgram = (project: string): string =>
|
|
2086
|
+
getRulesProgram(project, getBuiltRules(getRules(project)));
|
|
2087
|
+
|
|
2088
|
+
// Makes bend-lint.json in `dir`, if it is not there; gives the files it wrote, and those it kept.
|
|
1967
2089
|
const getInitFiles = (dir: string): { wrote: string[]; kept: string[] } => {
|
|
1968
|
-
const files: Array<[string, string]> = [
|
|
1969
|
-
[nodePath.join(dir, CONFIG), INIT_CONFIG],
|
|
1970
|
-
[nodePath.join(dir, RULES, "format.bend"), INIT_FORMAT],
|
|
1971
|
-
];
|
|
2090
|
+
const files: Array<[string, string]> = [[nodePath.join(dir, CONFIG), INIT_CONFIG]];
|
|
1972
2091
|
const kept = files.filter(([file]) => nodeFs.existsSync(file)).map(([file]) => file);
|
|
1973
2092
|
const wrote = files
|
|
1974
2093
|
.filter(([file]) => !kept.includes(file))
|
|
@@ -2000,8 +2119,9 @@ if (import.meta.main) {
|
|
|
2000
2119
|
}
|
|
2001
2120
|
process.exit(0);
|
|
2002
2121
|
}
|
|
2003
|
-
// An install of bend-lint:
|
|
2004
|
-
//
|
|
2122
|
+
// An install of bend-lint: bend-lint downloads its Bend now, not on its first run; and the project
|
|
2123
|
+
// that installs it in its node_modules gets what init makes, and the build of its rules (see
|
|
2124
|
+
// `prepare`). It never fails the install.
|
|
2005
2125
|
if (args[0] === "postinstall") {
|
|
2006
2126
|
try {
|
|
2007
2127
|
const project = process.env.INIT_CWD ?? "";
|
|
@@ -2010,30 +2130,77 @@ if (import.meta.main) {
|
|
|
2010
2130
|
modules === undefined
|
|
2011
2131
|
? ".."
|
|
2012
2132
|
: nodePath.relative(modules, url.fileURLToPath(new URL("../../", import.meta.url)));
|
|
2133
|
+
unwrap(await tryGetBend2SourceUrl());
|
|
2013
2134
|
if (!relative.startsWith("..") && !nodePath.isAbsolute(relative)) {
|
|
2014
2135
|
getInitFiles(project).wrote.forEach((file) => console.log("bend-lint: wrote " + file));
|
|
2136
|
+
console.log(PREPARING);
|
|
2137
|
+
await getRunSetup(getProjectProgram(project));
|
|
2015
2138
|
}
|
|
2016
|
-
unwrap(await tryGetBend2SourceUrl());
|
|
2017
2139
|
} catch (e) {
|
|
2018
2140
|
console.error("bend-lint: " + getErrorMessageFromUnknown(e) + "; its first run tries again");
|
|
2019
2141
|
}
|
|
2020
2142
|
process.exit(0);
|
|
2021
2143
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2144
|
+
// The runner never sees `--overlays`: it gets the files that have one in BEND_LINT_OVERLAYS, and
|
|
2145
|
+
// each argument that names one as its key.
|
|
2146
|
+
const started = performance.now();
|
|
2147
|
+
const json = args.includes("--json");
|
|
2148
|
+
// `--report <file>` writes the run's report there (see `getReport`); the runner never sees it.
|
|
2149
|
+
const reportAt = args.indexOf("--report");
|
|
2150
|
+
const report = reportAt === -1 ? undefined : args[reportAt + 1];
|
|
2151
|
+
if (reportAt !== -1 && (report === undefined || report.startsWith("--"))) {
|
|
2152
|
+
endWithError(json, "--report names the file to write the report to");
|
|
2153
|
+
}
|
|
2154
|
+
if (args.includes("--overlays")) {
|
|
2155
|
+
try {
|
|
2156
|
+
readOverlays(nodeFs.readFileSync(0, "utf8"));
|
|
2157
|
+
} catch (e) {
|
|
2158
|
+
endWithError(json, getErrorMessageFromUnknown(e));
|
|
2159
|
+
}
|
|
2028
2160
|
}
|
|
2161
|
+
const runArgs = args
|
|
2162
|
+
.filter(
|
|
2163
|
+
(a, n) => a !== "--overlays" && (reportAt === -1 || (n !== reportAt && n !== reportAt + 1)),
|
|
2164
|
+
)
|
|
2165
|
+
.map((a) => (getOverlay(a) === undefined ? a : getOverlayKey(a)));
|
|
2166
|
+
const project =
|
|
2167
|
+
getProject(process.cwd()) ??
|
|
2168
|
+
endWithError(
|
|
2169
|
+
json,
|
|
2170
|
+
"no " + CONFIG + " in this folder or above it; `bunx bend-lint init` makes one",
|
|
2171
|
+
);
|
|
2029
2172
|
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2173
|
+
// `bend-lint prepare` makes what a run needs, Bend and the build of its rules, and lints nothing:
|
|
2174
|
+
// an editor runs it at start, so that its first check builds nothing.
|
|
2175
|
+
const preparing = args[0] === "prepare";
|
|
2176
|
+
if (preparing) {
|
|
2177
|
+
console.error(PREPARING);
|
|
2178
|
+
}
|
|
2179
|
+
const named = await Promise.resolve()
|
|
2180
|
+
.then(() => getProjectProgram(project))
|
|
2181
|
+
.catch((e: unknown) =>
|
|
2182
|
+
endWithError(json, getErrorMessageFromUnknown(e), nodePath.join(project, CONFIG)),
|
|
2183
|
+
);
|
|
2184
|
+
const [checker, program, notes] = await getRunSetup(named).catch((e: unknown) =>
|
|
2185
|
+
endWithError(json, getErrorMessageFromUnknown(e)),
|
|
2186
|
+
);
|
|
2187
|
+
const setupMs = performance.now() - started;
|
|
2036
2188
|
notes.forEach((note) => console.error(note));
|
|
2189
|
+
// Writes the report, if the run asks for one, as it ends with `code`.
|
|
2190
|
+
const writeReport = (code: number): void => {
|
|
2191
|
+
if (report !== undefined) {
|
|
2192
|
+
const times = { setupMs, runMs: performance.now() - started };
|
|
2193
|
+
writeFileAtomically(
|
|
2194
|
+
report,
|
|
2195
|
+
JSON.stringify(getReport(program, notes, times, code), null, 2) + "\n",
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
if (preparing) {
|
|
2200
|
+
console.error("bend-lint: prepared " + program);
|
|
2201
|
+
writeReport(0);
|
|
2202
|
+
process.exit(0);
|
|
2203
|
+
}
|
|
2037
2204
|
|
|
2038
2205
|
const inTurn = getSerialRunner();
|
|
2039
2206
|
const server = net.createServer((socket) => void serve(checker, inTurn, socket));
|
|
@@ -2042,18 +2209,20 @@ if (import.meta.main) {
|
|
|
2042
2209
|
|
|
2043
2210
|
// A JS build of the program runs on this runtime; a native one, as itself.
|
|
2044
2211
|
const [command, commandArgs] = /\.m?js$/.test(program)
|
|
2045
|
-
? [process.execPath, [program, ...
|
|
2046
|
-
: [program,
|
|
2212
|
+
? [process.execPath, [program, ...runArgs]]
|
|
2213
|
+
: [program, runArgs];
|
|
2047
2214
|
const child = spawn(command, commandArgs, {
|
|
2048
2215
|
env: {
|
|
2049
2216
|
...process.env,
|
|
2050
2217
|
BEND_LINT_PORT: String((server.address() as net.AddressInfo).port),
|
|
2051
2218
|
BEND_LINT_PROJECT: getPathWithForwardSlashes(project),
|
|
2219
|
+
BEND_LINT_OVERLAYS: [...OVERLAYS.keys()].join("\n"),
|
|
2052
2220
|
},
|
|
2053
2221
|
stdio: "inherit",
|
|
2054
2222
|
});
|
|
2055
2223
|
const [code] = (await once(child, "exit")) as [number | null];
|
|
2056
2224
|
server.close();
|
|
2225
|
+
writeReport(code ?? 1);
|
|
2057
2226
|
prune(CHECKS_DIR);
|
|
2058
2227
|
prune(PROGRAMS_DIR);
|
|
2059
2228
|
process.exit(code ?? 1);
|
package/src/shared/utils.ts
CHANGED
|
@@ -23,10 +23,12 @@ export const getCommandOutcome = async (
|
|
|
23
23
|
cmd: string[],
|
|
24
24
|
cwd: string,
|
|
25
25
|
env: Record<string, string> = {},
|
|
26
|
+
input?: string,
|
|
26
27
|
): Promise<Outcome> => {
|
|
27
28
|
const child = Bun.spawn(cmd, {
|
|
28
29
|
cwd,
|
|
29
30
|
env: { ...process.env, ...env },
|
|
31
|
+
stdin: input === undefined ? "ignore" : new TextEncoder().encode(input),
|
|
30
32
|
stdout: "pipe",
|
|
31
33
|
stderr: "pipe",
|
|
32
34
|
});
|
package/src/text/main.bend
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Edits to texts, and places in them: the edits to make, spans that must not touch, and the lines and columns of
|
|
2
2
|
# offsets.
|
|
3
3
|
import Base
|
|
4
|
-
import bend-lint-api@0.
|
|
5
|
-
import bend-lint-api@0.
|
|
6
|
-
import bend-lint-api@0.
|
|
4
|
+
import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
|
|
5
|
+
import bend-lint-api@0.2.0.0/shared/names.bend as Names
|
|
6
|
+
import bend-lint-api@0.2.0.0/shared/strings.bend as Strings
|
|
7
7
|
|
|
8
8
|
# Edits
|
|
9
9
|
# =====
|