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.
@@ -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.1.0.0/shared/lists.bend as Lists
3
- import bend-lint-api@0.1.0.0/shared/names.bend as Names
4
- import bend-lint-api@0.1.0.0/shared/strings.bend as Strings
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.1.0.0/linter/lint.bend as Lint
6
+ import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
7
7
 
8
8
  # Scanning
9
9
  # ========
@@ -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.1.0.0/shared/lists.bend as Lists
5
- import bend-lint-api@0.1.0.0/shared/names.bend as Names
6
- import bend-lint-api@0.1.0.0/shared/strings.bend as Strings
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.1.0.0/linter/lint.bend as Lint
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(Pair.snd(Set(), Bool, Set.has(taken, Taken.key(path, alias, to))))
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
- Bool,
801
+ Result<&1, &1, U32 & String, File>,
737
802
  Unit,
738
- Files.write(path, Text.apply(Con{e, rest}, text)),
739
- w => write.done(path, w)
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):
@@ -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@${pkg.version}.0`;
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
- export const DEFAULTS = `bend-lint-rules@${pkg.version}.0`;
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 `bend-lint init` writes: a config that turns on the formatter, and the formatter's rule,
242
- // which uses bend-lint's own.
243
- const INIT_CONFIG = `{\n "rules": {\n "format": "warning"\n }\n}\n`;
244
- const INIT_FORMAT =
245
- "# format: bend-lint's formatter (layout/format). Its options go in bend-lint.json; see\n" +
246
- "# bend-lint's README.\n" +
247
- "import Base\n" +
248
- `import ${API}/linter/lint.bend as Lint\n` +
249
- `import ${DEFAULTS}/layout/format.bend as Format\n\n` +
250
- "def rule() -> Lint.Rule:\n" +
251
- " Format.rule()\n";
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 reads.
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; and
512
- // each text Bend reads kept.
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: { ...nodeFs, realpathSync: getRealPath, readFileSync: getKeptRead },
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
- tryGet(() => getRealPath(path)) ?? getPathWithForwardSlashes(nodePath.resolve(path));
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. A hit marks the
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(nodeFs.readFileSync(p, "utf8")) === d)) {
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
- // Makes bend-lint.json and bend-lint-rules/format.bend in `dir`, those that are not there; gives
1966
- // those it wrote, and those it kept.
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: the project that installs it in its node_modules gets what init
2004
- // makes, and bend-lint downloads its Bend now, not on its first run. It never fails the install.
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
- const project = getProject(process.cwd());
2023
- if (project === undefined) {
2024
- console.error(
2025
- "bend-lint: no " + CONFIG + " in this folder or above it; `bunx bend-lint init` makes one",
2026
- );
2027
- process.exit(2);
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
- const [checker, program, notes] = await Promise.resolve()
2031
- .then(() => getRunSetup(getRulesProgram(project, getRules(project))))
2032
- .catch((e: unknown) => {
2033
- console.error("bend-lint: " + getErrorMessageFromUnknown(e));
2034
- return process.exit(2);
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, ...args]]
2046
- : [program, args];
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);
@@ -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
  });
@@ -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.1.0.0/shared/lists.bend as Lists
5
- import bend-lint-api@0.1.0.0/shared/names.bend as Names
6
- import bend-lint-api@0.1.0.0/shared/strings.bend as Strings
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
  # =====