bend-lint 0.1.1 → 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.
@@ -2,12 +2,52 @@
2
2
  import Base
3
3
  import ../files/main.bend as Files
4
4
  import ../json/main.bend as Json
5
- import bend-lint-api@0.1.1.0/shared/lists.bend as Lists
6
- import bend-lint-api@0.1.1.0/shared/names.bend as Names
5
+ import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
6
+ import bend-lint-api@0.2.0.0/shared/names.bend as Names
7
+ import bend-lint-api@0.2.0.0/shared/sets.bend as Sets
7
8
  import ../text/main.bend as Text
8
9
  import ./directives.bend as Directives
9
10
  import ./fixes.bend as Fixes
10
- import bend-lint-api@0.1.1.0/linter/lint.bend as Lint
11
+ import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
12
+
13
+ # Errors
14
+ # ======
15
+
16
+ # What a JSON run prints when an error ends it: `ok` false, and the error: what it says, and the file it is about, if
17
+ # one (seam.ts prints the same for the errors it finds).
18
+ def Error.json(+path: String, text: String) -> Json.Json:
19
+ Json.JObject{
20
+ [
21
+ Json.JField{"ok", Json.JFalse{}},
22
+ Json.JField{
23
+ "errors",
24
+ Json.JArray{
25
+ [
26
+ Json.JObject{
27
+ Lists.push(
28
+ Json.JField,
29
+ Lists.when(
30
+ Json.JField,
31
+ Bool.not(String.is_empty(path)),
32
+ Json.JField{"path", Json.JString{path}}
33
+ ),
34
+ [Json.JField{"message", Json.JString{text}}]
35
+ )
36
+ }
37
+ ]
38
+ }
39
+ }
40
+ ]
41
+ }
42
+
43
+ # Ends the run with `status`, as `text` says on stderr, about the file at `path` ("" for none); a JSON run (`json`)
44
+ # first prints the error (see Error.json).
45
+ def Error.end(-A: Type, json: Bool, status: U32, path: String, +text: String) -> IO(A):
46
+ match json:
47
+ case True{}:
48
+ IO.bind(Unit, A, IO.print(Json.show(Error.json(path, text))), u => IO.die(A, status, text))
49
+ case False{}:
50
+ IO.die(A, status, text)
11
51
 
12
52
  # Config
13
53
  # ======
@@ -261,59 +301,91 @@ def Parsed.of(folder: String, json: Maybe<&2, Json.Json>) -> Result<&2, &2, Stri
261
301
  Fail{"not a JSON object"}
262
302
 
263
303
  # The config at `path`, if each of its entries is a file (`missing` lists those that are not); else the run ends.
264
- def Config.entered(missing: List<&2, String>, path: String, config: Project) -> IO(Project):
304
+ def Config.entered(
305
+ json: Bool,
306
+ missing: List<&2, String>,
307
+ +path: String,
308
+ config: Project
309
+ ) -> IO(Project):
265
310
  match missing:
266
311
  case Nil{}:
267
312
  IO.pure(Project, config)
268
313
  case Con{entry, rest}:
269
- IO.die(Project, 2, "bend-lint: " ++ path ++ ": the entry " ++ entry ++ " is not a file")
314
+ Error.end(
315
+ Project,
316
+ json,
317
+ 2,
318
+ path,
319
+ "bend-lint: " ++ path ++ ": the entry " ++ entry ++ " is not a file"
320
+ )
270
321
 
271
- def Config.parsed(path: String, parsed: Result<&2, &2, String, Project>) -> IO(Project):
322
+ def Config.parsed(
323
+ json: Bool,
324
+ +path: String,
325
+ parsed: Result<&2, &2, String, Project>
326
+ ) -> IO(Project):
272
327
  match parsed:
273
328
  case Done{Project{+entries, imports, rules}}:
274
329
  IO.bind(
275
330
  List<&2, String>,
276
331
  Project,
277
332
  Files.missing(entries, []),
278
- missing => Config.entered(missing, path, Project{entries, imports, rules})
333
+ missing => Config.entered(json, missing, path, Project{entries, imports, rules})
279
334
  )
280
335
  case Fail{why}:
281
- IO.die(Project, 2, "bend-lint: " ++ path ++ ": " ++ why)
336
+ Error.end(Project, json, 2, path, "bend-lint: " ++ path ++ ": " ++ why)
282
337
 
283
- def Config.read(+folder: String, text: Result<&2, &2, String, String>) -> IO(Project):
338
+ def Config.read(json: Bool, +folder: String, text: Result<&2, &2, String, String>) -> IO(Project):
284
339
  match text:
285
340
  case Done{t}:
286
- Config.parsed(folder ++ "/bend-lint.json", Parsed.of(folder, Json.read(t)))
341
+ Config.parsed(json, folder ++ "/bend-lint.json", Parsed.of(folder, Json.read(t)))
287
342
  case Fail{why}:
288
- IO.die(Project, 2, "bend-lint: reading a config failed: " ++ why)
343
+ Error.end(
344
+ Project,
345
+ json,
346
+ 2,
347
+ folder ++ "/bend-lint.json",
348
+ "bend-lint: reading a config failed: " ++ why
349
+ )
289
350
 
290
- def Config.opened(folder: String, opened: Result<&1, &1, U32 & String, File>) -> IO(Project):
351
+ def Config.opened(
352
+ json: Bool,
353
+ +folder: String,
354
+ opened: Result<&1, &1, U32 & String, File>
355
+ ) -> IO(Project):
291
356
  match opened:
292
357
  case Done{file}:
293
358
  IO.bind(
294
359
  Result<&2, &2, String, String>,
295
360
  Project,
296
361
  Files.read(file),
297
- text => Config.read(folder, text)
362
+ text => Config.read(json, folder, text)
298
363
  )
299
364
  case Fail{Tuple{code, why}}:
300
- IO.die(Project, 2, "bend-lint: reading " ++ folder ++ "/bend-lint.json failed: " ++ why)
365
+ Error.end(
366
+ Project,
367
+ json,
368
+ 2,
369
+ folder ++ "/bend-lint.json",
370
+ "bend-lint: reading " ++ folder ++ "/bend-lint.json failed: " ++ why
371
+ )
301
372
 
302
- def Config.at(+folder: String) -> IO(Project):
373
+ def Config.at(json: Bool, +folder: String) -> IO(Project):
303
374
  IO.bind(
304
375
  Result<&1, &1, U32 & String, File>,
305
376
  Project,
306
377
  File.open(folder ++ "/bend-lint.json", "r"),
307
- opened => Config.opened(folder, opened)
378
+ opened => Config.opened(json, folder, opened)
308
379
  )
309
380
 
310
- # The run's config: the bend-lint.json the server found, in the folder it names in BEND_LINT_PROJECT.
311
- def Config.load() -> IO(Project):
381
+ # The run's config: the bend-lint.json the server found, in the folder it names in BEND_LINT_PROJECT. Its errors end
382
+ # the run, in JSON if `json`.
383
+ def Config.load(json: Bool) -> IO(Project):
312
384
  IO.bind(
313
385
  String,
314
386
  Project,
315
387
  IO.try(String, IO.get_env("BEND_LINT_PROJECT")),
316
- folder => Config.at(folder)
388
+ folder => Config.at(json, folder)
317
389
  )
318
390
 
319
391
  # Seam
@@ -454,7 +526,9 @@ def Wire.receive(
454
526
  Socket.close(s)
455
527
  IO.die(List<&2, String>, 1, "bend-lint: the checker's answer is too long")
456
528
 
457
- def Wire.sent(r: Socket & Result<&1, &1, (U32 & String) & String, Unit>) -> IO(List<&2, String>):
529
+ def Wire.sent(
530
+ r: Socket & Result<&1, &1, (U32 & String) & List<&2, U32>, Unit>
531
+ ) -> IO(List<&2, String>):
458
532
  match r:
459
533
  case Tuple{s, Done{u}}:
460
534
  IO.bind(
@@ -497,7 +571,11 @@ def Wire.ask(request: List<&2, String>) -> IO(List<&2, List<&2, String>>):
497
571
  p: U32 <- Wire.port()
498
572
  sock: Socket <- IO.try(Socket, TCP.connect("127.0.0.1", p))
499
573
  s: Socket
500
- & Result<&1, &1, (U32 & String) & String, Unit> <- TCP.send(sock, Wire.line(request, ""))
574
+ & Result<
575
+ &1,
576
+ &1,
577
+ (U32 & String) & List<&2, U32>,
578
+ Unit> <- TCP.send(sock, Wire.line(request, ""))
501
579
  got: List<&2, String> <- Wire.sent(s)
502
580
  Wire.answered(Wire.records(List.reverse(&2, String, got), Reading{False{}, "", [], []}))
503
581
 
@@ -959,12 +1037,14 @@ def About.needs_every(about: About) -> Bool:
959
1037
  case _:
960
1038
  False{}
961
1039
 
1040
+ # Whether a rule of this setting runs: one the config does not name is off, as the runner can hold more rules than the
1041
+ # config turns on (see getBuiltRules in seam.ts).
962
1042
  def Setting.is_on(setting: Maybe<&2, Setting>) -> Bool:
963
1043
  match setting:
964
- case Some{RuleOff{}}:
965
- False{}
966
- case _:
1044
+ case Some{RuleOn{severity, options}}:
967
1045
  True{}
1046
+ case _:
1047
+ False{}
968
1048
 
969
1049
  def Setting.severity(setting: Maybe<&2, Setting>) -> Maybe<&2, Lint.Severity>:
970
1050
  match setting:
@@ -1202,7 +1282,7 @@ def About.settle.of(about: About, setting: Maybe<&2, Setting>) -> Names.Named<Se
1202
1282
  case About{id, needs, options, every} Some{RuleOn{severity, given}}:
1203
1283
  Names.Named{id, RuleOn{severity, Option.values(options, given, [])}}
1204
1284
  case About{id, needs, options, every} None{}:
1205
- Names.Named{id, RuleOn{None{}, Option.values(options, [], [])}}
1285
+ Names.Named{id, RuleOff{}}
1206
1286
 
1207
1287
  # Each rule's setting, its option values settled: off, or on with its severity, if any, and every option it declares.
1208
1288
  def About.settle(
@@ -1591,7 +1671,7 @@ def Found.keys(founds: List<&2, Lint.Found>, keys: Set()) -> Set():
1591
1671
  Found.keys(rest, Set.add(keys, Found.key(f)))
1592
1672
 
1593
1673
  def Found.is_among(keys: Set(), found: Lint.Found) -> Bool:
1594
- Pair.snd(Set(), Bool, Set.has(keys, Found.key(found)))
1674
+ Sets.has(keys, Found.key(found))
1595
1675
 
1596
1676
  def Found.keep(hit: Bool, found: Lint.Found, out: List<&2, Lint.Found>) -> List<&2, Lint.Found>:
1597
1677
  match hit:
@@ -1967,21 +2047,43 @@ def Object.fixes(
1967
2047
  case _ _:
1968
2048
  List.reverse(&2, Json.Json, out)
1969
2049
 
2050
+ # Where a rule of bend-lint's own, from the package bend-lint-rules, is explained: its file, whose header says what it
2051
+ # reports, what its fix does, and its options. Its id is its path in bend-lint's repo.
2052
+ def Docs.of(+rule: String) -> Maybe<&2, String>:
2053
+ Lists.when(
2054
+ String,
2055
+ String.starts_with(rule, "bend-lint-rules/"),
2056
+ "https://github.com/MattCozendey/bend-lint/blob/main/" ++ rule ++ ".bend"
2057
+ )
2058
+
1970
2059
  def Object.finding(resolved: Resolved, +places: Map<&2, Text.Position>) -> Json.Json:
1971
2060
  match resolved:
1972
2061
  case Resolved{
1973
- Lint.Found{rule, Lint.Finding{severity, Lint.Span{+path, beg, end}, message, fixes}},
2062
+ Lint.Found{+rule, Lint.Finding{severity, Lint.Span{+path, beg, end}, message, fixes}},
1974
2063
  edits
1975
2064
  }:
1976
2065
  Json.JObject{
1977
- [
1978
- Json.JField{"code", Json.JString{rule}},
1979
- Json.JField{"severity", Json.JString{Lint.Severity.show(severity)}},
1980
- Json.JField{"message", Json.JString{message}},
1981
- Json.JField{"path", Json.JString{path}},
1982
- Json.JField{"range", Object.range(places, path, beg, end)},
1983
- Json.JField{"fixes", Json.JArray{Object.fixes(fixes, edits, places, [])}}
1984
- ]
2066
+ Json.JField{
2067
+ "code",
2068
+ Json.JString{rule}
2069
+ }
2070
+ <> Lists.push(
2071
+ Json.JField,
2072
+ Maybe.map(
2073
+ &2,
2074
+ String,
2075
+ Json.JField,
2076
+ d => Json.JField{"docs", Json.JString{d}},
2077
+ Docs.of(rule)
2078
+ ),
2079
+ [
2080
+ Json.JField{"severity", Json.JString{Lint.Severity.show(severity)}},
2081
+ Json.JField{"message", Json.JString{message}},
2082
+ Json.JField{"path", Json.JString{path}},
2083
+ Json.JField{"range", Object.range(places, path, beg, end)},
2084
+ Json.JField{"fixes", Json.JArray{Object.fixes(fixes, edits, places, [])}}
2085
+ ]
2086
+ )
1985
2087
  }
1986
2088
 
1987
2089
  def Shown.one(json: Bool, resolved: Resolved, places: Map<&2, Text.Position>) -> Shown:
@@ -2042,25 +2144,28 @@ def Declarations.of(need: Bool, sources: List<&2, Lint.Source>) -> List<&2, Lint
2042
2144
  # ------
2043
2145
 
2044
2146
  # What linting named files gave: how many findings stay; those to show at the end, and those directives suppressed
2045
- # (both last first); whether a file fails the run; and how many fixes the run made.
2147
+ # (both last first); whether a file fails the run; how many fixes the run made; and why rules did not run on some
2148
+ # files, in order, as JSON (see Skipped).
2046
2149
  type Outcome is Data:
2047
2150
  Outcome{
2048
2151
  count: Nat,
2049
2152
  findings: List<&2, Shown>,
2050
2153
  suppressed: List<&2, Shown>,
2051
2154
  failed: Bool,
2052
- fixed: U32
2155
+ fixed: U32,
2156
+ skipped: List<&2, Json.Json>
2053
2157
  }
2054
2158
 
2055
2159
  def Outcome.add(a: Outcome, b: Outcome) -> Outcome:
2056
2160
  match a b:
2057
- case Outcome{n, f, s, e, x} Outcome{m, g, t, d, y}:
2161
+ case Outcome{n, f, s, e, x, w} Outcome{m, g, t, d, y, z}:
2058
2162
  Outcome{
2059
2163
  Nat.add(n, m),
2060
2164
  Lists.append(&2, Shown, g, f),
2061
2165
  Lists.append(&2, Shown, t, s),
2062
2166
  e || d,
2063
- (x + y: U32)
2167
+ (x + y: U32),
2168
+ Lists.append(&2, Json.Json, w, z)
2064
2169
  }
2065
2170
 
2066
2171
  def summary.fixed(fixing: Bool, fixed: U32) -> String:
@@ -2120,15 +2225,15 @@ def end.json(failed: Bool, summary: String) -> IO(Unit):
2120
2225
 
2121
2226
  def end(json: Bool, total: Outcome, show: Bool, fixing: Bool) -> IO(Unit):
2122
2227
  match json total:
2123
- case False{} Outcome{n, f, s, True{}, x}:
2228
+ case False{} Outcome{n, f, s, True{}, x, w}:
2124
2229
  do IO<Unit>:
2125
2230
  say(suppressed.lines(show, s))
2126
2231
  IO.die(Unit, 1, "bend-lint: FAIL")
2127
- case False{} Outcome{n, f, +s, False{}, x}:
2232
+ case False{} Outcome{n, f, +s, False{}, x, w}:
2128
2233
  do IO<Unit>:
2129
2234
  say(suppressed.lines(show, s))
2130
2235
  IO.print(summary(n, Lists.length(&2, Shown, s), summary.fixed(fixing, x)))
2131
- case True{} Outcome{n, f, +s, +failed, x}:
2236
+ case True{} Outcome{n, f, +s, +failed, x, w}:
2132
2237
  do IO<Unit>:
2133
2238
  IO.print(
2134
2239
  Json.show(
@@ -2136,7 +2241,8 @@ def end(json: Bool, total: Outcome, show: Bool, fixing: Bool) -> IO(Unit):
2136
2241
  [
2137
2242
  Json.JField{"ok", Bool.pick(Json.Json, failed, Json.JFalse{}, Json.JTrue{})},
2138
2243
  Json.JField{"findings", Json.JArray{Shown.values(f, [])}},
2139
- Json.JField{"suppressed", Json.JArray{Shown.values(s, [])}}
2244
+ Json.JField{"suppressed", Json.JArray{Shown.values(s, [])}},
2245
+ Json.JField{"skipped", Json.JArray{w}}
2140
2246
  ]
2141
2247
  }
2142
2248
  )
@@ -2147,14 +2253,15 @@ def end(json: Bool, total: Outcome, show: Bool, fixing: Bool) -> IO(Unit):
2147
2253
  # (first first; a line as a JSON string, an object as itself).
2148
2254
  def Outcome.json(total: Outcome) -> Json.Json:
2149
2255
  match total:
2150
- case Outcome{n, f, s, failed, x}:
2256
+ case Outcome{n, f, s, failed, x, w}:
2151
2257
  Json.JArray{
2152
2258
  [
2153
2259
  Json.JNumber{Nat.show(n)},
2154
2260
  Json.JNumber{U32.show(x)},
2155
2261
  Bool.pick(Json.Json, failed, Json.JTrue{}, Json.JFalse{}),
2156
2262
  Json.JArray{Shown.values(f, [])},
2157
- Json.JArray{Shown.values(s, [])}
2263
+ Json.JArray{Shown.values(s, [])},
2264
+ Json.JArray{w}
2158
2265
  ]
2159
2266
  }
2160
2267
 
@@ -2172,11 +2279,12 @@ def Outcome.of(
2172
2279
  n: Maybe<&2, U32>,
2173
2280
  x: Maybe<&2, U32>,
2174
2281
  f: List<&2, Json.Json>,
2175
- s: List<&2, Json.Json>
2282
+ s: List<&2, Json.Json>,
2283
+ w: List<&2, Json.Json>
2176
2284
  ) -> Maybe<&2, Outcome>:
2177
2285
  match n x:
2178
2286
  case Some{n} Some{x}:
2179
- Some{Outcome{U32.to_nat(n), Shown.back(f, []), Shown.back(s, []), failed, x}}
2287
+ Some{Outcome{U32.to_nat(n), Shown.back(f, []), Shown.back(s, []), failed, x, w}}
2180
2288
  case _ _:
2181
2289
  None{}
2182
2290
 
@@ -2187,20 +2295,26 @@ def Outcome.read(answer: Maybe<&2, Json.Json>) -> Maybe<&2, Outcome>:
2187
2295
  Json.JArray{
2188
2296
  Con{
2189
2297
  Json.JNumber{n},
2190
- Con{Json.JNumber{x}, Con{Json.JTrue{}, Con{Json.JArray{f}, Con{Json.JArray{s}, Nil{}}}}}
2298
+ Con{
2299
+ Json.JNumber{x},
2300
+ Con{Json.JTrue{}, Con{Json.JArray{f}, Con{Json.JArray{s}, Con{Json.JArray{w}, Nil{}}}}}
2301
+ }
2191
2302
  }
2192
2303
  }
2193
2304
  }:
2194
- Outcome.of(True{}, U32.read(n), U32.read(x), f, s)
2305
+ Outcome.of(True{}, U32.read(n), U32.read(x), f, s, w)
2195
2306
  case Some{
2196
2307
  Json.JArray{
2197
2308
  Con{
2198
2309
  Json.JNumber{n},
2199
- Con{Json.JNumber{x}, Con{Json.JFalse{}, Con{Json.JArray{f}, Con{Json.JArray{s}, Nil{}}}}}
2310
+ Con{
2311
+ Json.JNumber{x},
2312
+ Con{Json.JFalse{}, Con{Json.JArray{f}, Con{Json.JArray{s}, Con{Json.JArray{w}, Nil{}}}}}
2313
+ }
2200
2314
  }
2201
2315
  }
2202
2316
  }:
2203
- Outcome.of(False{}, U32.read(n), U32.read(x), f, s)
2317
+ Outcome.of(False{}, U32.read(n), U32.read(x), f, s, w)
2204
2318
  case _:
2205
2319
  None{}
2206
2320
 
@@ -2248,7 +2362,8 @@ def outcome(report: Report, +json: Bool, +fixing: Bool, program: Lint.Program) -
2248
2362
  List.reverse(&2, Shown, Shown.kept(json, List.take(&2, Shown, shown, n))),
2249
2363
  List.reverse(&2, Shown, List.drop(&2, Shown, shown, n)),
2250
2364
  Lists.any(~Lint.Found, ~Found.is_error, founds),
2251
- fixed
2365
+ fixed,
2366
+ []
2252
2367
  }
2253
2368
 
2254
2369
  # CLI
@@ -2405,19 +2520,25 @@ def linted(imports: Bool, path: String, sources: List<&2, Lint.Source>) -> List<
2405
2520
  []
2406
2521
  )
2407
2522
 
2408
- # Says, if the named file's check failed, which rules on did not run on it, as they need its facts (`skipped`), if any.
2409
- def unchecked(failed: Bool, skipped: List<&2, String>, path: String) -> IO(Unit):
2410
- match failed skipped:
2523
+ # Why rules did not run on a file: Bend does not load it, so none ran (`Unloaded`); its check fails, so those that
2524
+ # need its facts did not (`Unchecked`); or an entry that reads it fails its check, so it was linted without it, and the
2525
+ # rules whose findings every entry must find report nothing on it (`Unentered`).
2526
+ type Skip is Data:
2527
+ Unloaded{}
2528
+ Unchecked{rules: List<&2, String>}
2529
+ Unentered{entry: String, rules: List<&2, String>}
2530
+
2531
+ # Why rules did not run on the file at `path`.
2532
+ type Skipped is Data:
2533
+ Skipped{path: String, skip: Skip}
2534
+
2535
+ # The skip, if the named file's check failed (`failed`), of the rules on that need its facts (`facts`), if any.
2536
+ def unchecked(failed: Bool, facts: List<&2, String>, path: String) -> List<&2, Skipped>:
2537
+ match failed facts:
2411
2538
  case True{} Con{id, rest}:
2412
- IO.print_err(
2413
- "bend-lint: Bend's check of "
2414
- ++ path
2415
- ++ " fails, so the rules that need its facts did not run on it: "
2416
- ++ String.join(Con{id, rest}, ", ")
2417
- ++ "; `bend <file> --check-only` says why"
2418
- )
2539
+ [Skipped{path, Unchecked{Con{id, rest}}}]
2419
2540
  case _ _:
2420
- IO.pure(Unit, Unit{})
2541
+ []
2421
2542
 
2422
2543
  # What a note on a failed entry adds of the rules on whose findings every entry must find (`every`): they report
2423
2544
  # nothing.
@@ -2429,30 +2550,99 @@ def silenced(every: List<&2, String>) -> String:
2429
2550
  ", and the rules whose findings every entry must find report nothing on it: "
2430
2551
  ++ String.join(Con{id, rest}, ", ")
2431
2552
 
2432
- # Says, for each entry of `failing` that reads the named file and failed its check, that the file was linted without
2433
- # it, and which rules report nothing on it (`every`). If the file's own check failed too (`failed`), `unchecked` says
2434
- # so instead.
2553
+ # The skips, for each entry of `failing` that reads the named file and failed its check, of the rules whose findings
2554
+ # every entry must find (`every`), onto `out` (last first). If the file's own check failed too (`failed`), `unchecked`
2555
+ # gives its skip instead.
2435
2556
  def unentered(
2436
2557
  failed: Bool,
2437
2558
  failing: List<&2, String>,
2438
2559
  +every: List<&2, String>,
2439
- +path: String
2440
- ) -> IO(Unit):
2560
+ +path: String,
2561
+ out: List<&2, Skipped>
2562
+ ) -> List<&2, Skipped>:
2441
2563
  match failed failing:
2442
2564
  case False{} Con{entry, rest}:
2443
- do IO<Unit>:
2444
- IO.print_err(
2445
- "bend-lint: Bend's check of the entry "
2446
- ++ entry
2447
- ++ " fails, so "
2448
- ++ path
2449
- ++ " was linted without it"
2450
- ++ silenced(every)
2451
- ++ "; `bend <file> --check-only` says why"
2452
- )
2453
- unentered(False{}, rest, every, path)
2565
+ unentered(False{}, rest, every, path, Skipped{path, Unentered{entry, every}} <> out)
2454
2566
  case _ _:
2455
- IO.pure(Unit, Unit{})
2567
+ out
2568
+
2569
+ def Skipped.text(skipped: Skipped) -> String:
2570
+ match skipped:
2571
+ case Skipped{path, Unloaded{}}:
2572
+ "bend-lint: Bend does not load "
2573
+ ++ path
2574
+ ++ ", so no rule ran on it; `bend <file> --check-only` says why"
2575
+ case Skipped{path, Unchecked{rules}}:
2576
+ "bend-lint: Bend's check of "
2577
+ ++ path
2578
+ ++ " fails, so the rules that need its facts did not run on it: "
2579
+ ++ String.join(rules, ", ")
2580
+ ++ "; `bend <file> --check-only` says why"
2581
+ case Skipped{path, Unentered{entry, rules}}:
2582
+ "bend-lint: Bend's check of the entry "
2583
+ ++ entry
2584
+ ++ " fails, so "
2585
+ ++ path
2586
+ ++ " was linted without it"
2587
+ ++ silenced(rules)
2588
+ ++ "; `bend <file> --check-only` says why"
2589
+
2590
+ def Skipped.strings(texts: List<&2, String>, out: List<&2, Json.Json>) -> Json.Json:
2591
+ match texts:
2592
+ case Nil{}:
2593
+ Json.JArray{List.reverse(&2, Json.Json, out)}
2594
+ case Con{t, rest}:
2595
+ Skipped.strings(rest, Json.JString{t} <> out)
2596
+
2597
+ # A skip as a JSON run gives it: the file, why (`reason`), the rules it is about, the entry, if Unentered, and what the
2598
+ # text run says.
2599
+ def Skipped.json(+skipped: Skipped) -> Json.Json:
2600
+ match skipped:
2601
+ case Skipped{+path, Unloaded{}}:
2602
+ Json.JObject{
2603
+ [
2604
+ Json.JField{"path", Json.JString{path}},
2605
+ Json.JField{"reason", Json.JString{"unloaded"}},
2606
+ Json.JField{"message", Json.JString{Skipped.text(skipped)}}
2607
+ ]
2608
+ }
2609
+ case Skipped{+path, Unchecked{+rules}}:
2610
+ Json.JObject{
2611
+ [
2612
+ Json.JField{"path", Json.JString{path}},
2613
+ Json.JField{"reason", Json.JString{"unchecked"}},
2614
+ Json.JField{"rules", Skipped.strings(rules, [])},
2615
+ Json.JField{"message", Json.JString{Skipped.text(skipped)}}
2616
+ ]
2617
+ }
2618
+ case Skipped{+path, Unentered{+entry, +rules}}:
2619
+ Json.JObject{
2620
+ [
2621
+ Json.JField{"path", Json.JString{path}},
2622
+ Json.JField{"reason", Json.JString{"unentered"}},
2623
+ Json.JField{"entry", Json.JString{entry}},
2624
+ Json.JField{"rules", Skipped.strings(rules, [])},
2625
+ Json.JField{"message", Json.JString{Skipped.text(skipped)}}
2626
+ ]
2627
+ }
2628
+
2629
+ # Says each of `skips` on stderr; gives them, in order, as JSON.
2630
+ def Skipped.said(skips: List<&2, Skipped>, out: List<&2, Json.Json>) -> IO(List<&2, Json.Json>):
2631
+ match skips:
2632
+ case Nil{}:
2633
+ IO.pure(List<&2, Json.Json>, List.reverse(&2, Json.Json, out))
2634
+ case Con{+s, rest}:
2635
+ IO.bind(
2636
+ Unit,
2637
+ List<&2, Json.Json>,
2638
+ IO.print_err(Skipped.text(s)),
2639
+ u => Skipped.said(rest, Skipped.json(s) <> out)
2640
+ )
2641
+
2642
+ def Outcome.skip(outcome: Outcome, skipped: List<&2, Json.Json>) -> Outcome:
2643
+ match outcome:
2644
+ case Outcome{n, f, s, e, x, w}:
2645
+ Outcome{n, f, s, e, x, Lists.append(&2, Json.Json, skipped, w)}
2456
2646
 
2457
2647
  def checked(
2458
2648
  ~rules: List<&1, Rule>,
@@ -2466,9 +2656,24 @@ def checked(
2466
2656
  match checks:
2467
2657
  case Checks{Con{Lint.Program{+sources}, rest}, +failing, +failed, forget}:
2468
2658
  +on = About.on(About.all(rules, []), settings, [])
2659
+ skips = Lists.append(
2660
+ &2,
2661
+ Skipped,
2662
+ unchecked(failed, About.ids(List.filter(~About, ~About.needs_facts, on), []), path),
2663
+ List.reverse(
2664
+ &2,
2665
+ Skipped,
2666
+ unentered(
2667
+ failed,
2668
+ failing,
2669
+ About.ids(List.filter(~About, ~About.needs_every, on), []),
2670
+ path,
2671
+ []
2672
+ )
2673
+ )
2674
+ )
2469
2675
  do IO<Outcome>:
2470
- unchecked(failed, About.ids(List.filter(~About, ~About.needs_facts, on), []), path)
2471
- unentered(failed, failing, About.ids(List.filter(~About, ~About.needs_every, on), []), path)
2676
+ skipped: List<&2, Json.Json> <- Skipped.said(skips, [])
2472
2677
  o: Outcome <- lint(
2473
2678
  ~rules,
2474
2679
  linted(imports, path, sources),
@@ -2479,15 +2684,11 @@ def checked(
2479
2684
  json,
2480
2685
  fixing
2481
2686
  )
2482
- forgotten(forget, o)
2687
+ forgotten(forget, Outcome.skip(o, skipped))
2483
2688
  case Checks{Nil{}, failing, failed, forget}:
2484
2689
  do IO<Outcome>:
2485
- IO.print_err(
2486
- "bend-lint: Bend does not load "
2487
- ++ path
2488
- ++ ", so no rule ran on it; `bend <file> --check-only` says why"
2489
- )
2490
- return Outcome{0n, [], [], False{}, 0}
2690
+ skipped: List<&2, Json.Json> <- Skipped.said([Skipped{path, Unloaded{}}], [])
2691
+ return Outcome{0n, [], [], False{}, 0, skipped}
2491
2692
 
2492
2693
  # The named file's own check, as the checks to lint it in, after the entries that read it and failed (`failing`). If
2493
2694
  # it fails, the rules that need no facts run on what it parsed, if it parsed the files.
@@ -2601,10 +2802,10 @@ def Check.of(imports: Bool, entries: List<&2, String>, +path: String, +facts: Bo
2601
2802
  c => IO.pure(Checks, Check.own([], c))
2602
2803
  )
2603
2804
 
2604
- def Options.checked(problem: Maybe<&2, String>) -> IO(Unit):
2805
+ def Options.checked(json: Bool, problem: Maybe<&2, String>) -> IO(Unit):
2605
2806
  match problem:
2606
2807
  case Some{why}:
2607
- IO.die(Unit, 2, "bend-lint config: " ++ why)
2808
+ Error.end(Unit, json, 2, "", "bend-lint config: " ++ why)
2608
2809
  case None{}:
2609
2810
  IO.pure(Unit, Unit{})
2610
2811
 
@@ -2626,6 +2827,11 @@ type Options is Data:
2626
2827
  jobs: Jobs
2627
2828
  }
2628
2829
 
2830
+ def Options.is_json(options: Options) -> Bool:
2831
+ match options:
2832
+ case Options{inputs, fixing, shown, json, worker, jobs}:
2833
+ json
2834
+
2629
2835
  # Whether a run with the fix flag `fixing` makes its fixes: `--fix-dangerously` does. `--fix` makes none until Bend's
2630
2836
  # checker in Bend can prove that a fix keeps the program, and says so (`Options.note`).
2631
2837
  def Options.fixes(fixing: Maybe<&2, Bool>) -> Bool:
@@ -2641,10 +2847,10 @@ def Options.note(fixing: Maybe<&2, Bool>) -> IO(Unit):
2641
2847
  IO.pure(Unit, Unit{})
2642
2848
 
2643
2849
  # Stops the run if it lints the imports of more than one file: a run with imports has one root.
2644
- def Options.single(imports: Bool, many: Bool) -> IO(Unit):
2850
+ def Options.single(json: Bool, imports: Bool, many: Bool) -> IO(Unit):
2645
2851
  match imports many:
2646
2852
  case True{} True{}:
2647
- IO.die(Unit, 2, "bend-lint: a config with \"imports\" lints one file at a time")
2853
+ Error.end(Unit, json, 2, "", "bend-lint: a config with \"imports\" lints one file at a time")
2648
2854
  case _ _:
2649
2855
  IO.pure(Unit, Unit{})
2650
2856
 
@@ -2678,11 +2884,11 @@ def configured(
2678
2884
  project: Project
2679
2885
  ) -> IO(Outcome):
2680
2886
  match options project:
2681
- case Options{inputs, fixing, shown, json, worker, jobs} Project{entries, from, +settings}:
2887
+ case Options{inputs, fixing, shown, +json, worker, jobs} Project{entries, from, +settings}:
2682
2888
  +on = Maybe.default(&2, Bool, from, False{})
2683
2889
  do IO<Outcome>:
2684
- Options.checked(About.problem(About.all(rules, []), settings))
2685
- Options.single(on, many)
2890
+ Options.checked(json, About.problem(About.all(rules, []), settings))
2891
+ Options.single(json, on, many)
2686
2892
  settled(
2687
2893
  ~rules,
2688
2894
  path,
@@ -2788,11 +2994,11 @@ def Options.read(args: List<String>, options: Maybe<&2, Options>, jobs: Bool) ->
2788
2994
  String.eq(a, "--jobs")
2789
2995
  )
2790
2996
 
2791
- def File.lint(~rules: List<&1, Rule>, path: String, options: Options, many: Bool) -> IO(Outcome):
2997
+ def File.lint(~rules: List<&1, Rule>, path: String, +options: Options, many: Bool) -> IO(Outcome):
2792
2998
  IO.bind(
2793
2999
  Project,
2794
3000
  Outcome,
2795
- Config.load(),
3001
+ Config.load(Options.is_json(options)),
2796
3002
  project => configured(~rules, path, options, many, project)
2797
3003
  )
2798
3004
 
@@ -2823,27 +3029,84 @@ def Paths.lint(
2823
3029
  o => Paths.lint(~rules, rest, options, many, Outcome.add(total, o))
2824
3030
  )
2825
3031
 
2826
- def files.of(found: Result<&2, &2, String, List<&2, String>>) -> IO(List<&2, String>):
3032
+ # The paths the server holds an overlay for (`--overlays`, see seam.ts), one a line in BEND_LINT_OVERLAYS: none, if it
3033
+ # gives none.
3034
+ def Overlays.of(given: Result<&1, &1, U32 & String, String>) -> List<&2, String>:
3035
+ match given:
3036
+ case Done{text}:
3037
+ List.filter(~String, ~(n => Bool.not(String.is_empty(n))), String.split(text, '
3038
+ '))
3039
+ case Fail{why}:
3040
+ []
3041
+
3042
+ # Stops a run that makes fixes (`fixes`) to files with overlays: a fix writes the file on disk, not the text it linted.
3043
+ def Overlays.fixing(json: Bool, fixes: Bool, overlays: List<&2, String>) -> IO(Unit):
3044
+ match fixes overlays:
3045
+ case True{} Con{o, rest}:
3046
+ Error.end(
3047
+ Unit,
3048
+ json,
3049
+ 2,
3050
+ "",
3051
+ "bend-lint: --fix-dangerously does not go with --overlays: a fix writes the file on disk"
3052
+ )
3053
+ case _ _:
3054
+ IO.pure(Unit, Unit{})
3055
+
3056
+ def files.of(
3057
+ json: Bool,
3058
+ kept: List<&2, String>,
3059
+ found: Result<&2, &2, String, List<&2, String>>
3060
+ ) -> IO(List<&2, String>):
2827
3061
  match found:
2828
3062
  case Done{paths}:
2829
- IO.pure(List<&2, String>, paths)
3063
+ IO.pure(List<&2, String>, Lists.append(&2, String, kept, paths))
2830
3064
  case Fail{why}:
2831
- IO.die(List<&2, String>, 2, "bend-lint: " ++ why)
2832
-
2833
- # The files the inputs name; with none, the .bend files under the current folder (see Files.listed). An input that
2834
- # names none ends the run with a usage error.
2835
- def files(inputs: List<&2, String>) -> IO(List<&2, String>):
3065
+ Error.end(List<&2, String>, json, 2, "", "bend-lint: " ++ why)
3066
+
3067
+ # The inputs that name a file with an overlay (`overlays`), onto `kept`, and the others, onto `rest` (both last first).
3068
+ def files.sort(
3069
+ inputs: List<&2, String>,
3070
+ +overlays: List<&2, String>,
3071
+ kept: List<&2, String>,
3072
+ rest: List<&2, String>
3073
+ ) -> List<&2, String>
3074
+ & List<&2, String>:
2836
3075
  match inputs:
2837
3076
  case Nil{}:
2838
- Files.listed("*.bend")
2839
- case Con{first, more}:
3077
+ (kept, rest)
3078
+ case Con{+input, more}:
3079
+ +hit = List.contains(~String, ~String.eq, overlays, input)
3080
+ files.sort(
3081
+ more,
3082
+ overlays,
3083
+ Lists.push(String, Lists.when(String, hit, input), kept),
3084
+ Lists.push(String, Lists.when(String, Bool.not(hit), input), rest)
3085
+ )
3086
+
3087
+ # The files of `sorted` (see files.sort): those with an overlay, which need not be on disk, then those the others name.
3088
+ def files.named(json: Bool, sorted: List<&2, String> & List<&2, String>) -> IO(List<&2, String>):
3089
+ match sorted:
3090
+ case Tuple{kept, Nil{}}:
3091
+ IO.pure(List<&2, String>, List.reverse(&2, String, kept))
3092
+ case Tuple{kept, Con{r, rest}}:
2840
3093
  IO.bind(
2841
3094
  Result<&2, &2, String, List<&2, String>>,
2842
3095
  List<&2, String>,
2843
- Files.named(Con{first, more}),
2844
- found => files.of(found)
3096
+ Files.named(List.reverse(&2, String, Con{r, rest})),
3097
+ found => files.of(json, List.reverse(&2, String, kept), found)
2845
3098
  )
2846
3099
 
3100
+ # The files the inputs name; with none, the .bend files under the current folder (see Files.listed). An input that
3101
+ # names none ends the run with a usage error (in JSON if `json`); one that names a file with an overlay (in `overlays`) names it, on disk
3102
+ # or not.
3103
+ def files(json: Bool, inputs: List<&2, String>, overlays: List<&2, String>) -> IO(List<&2, String>):
3104
+ match inputs:
3105
+ case Nil{}:
3106
+ Files.listed("*.bend")
3107
+ case Con{first, more}:
3108
+ files.named(json, files.sort(Con{first, more}, overlays, [], []))
3109
+
2847
3110
  # Workers
2848
3111
  # -------
2849
3112
 
@@ -2947,8 +3210,10 @@ def Worker.flag(on: Bool, flag: String) -> List<String>:
2947
3210
  def Worker.args(options: Options, path: String, threads: String) -> List<String>:
2948
3211
  match options:
2949
3212
  case Options{inputs, fixing, shown, json, worker, jobs}:
2950
- "--threads"
3213
+ "+RTS"
3214
+ <> "--threads"
2951
3215
  <> threads
3216
+ <> "-RTS"
2952
3217
  <> "--worker"
2953
3218
  <> List.append(
2954
3219
  &1,
@@ -3051,7 +3316,7 @@ def Workers.run(
3051
3316
  paths,
3052
3317
  []
3053
3318
  )
3054
- total: Outcome <- Workers.collect(workers, Outcome{0n, [], [], False{}, 0})
3319
+ total: Outcome <- Workers.collect(workers, Outcome{0n, [], [], False{}, 0, []})
3055
3320
  ended(options, total)
3056
3321
 
3057
3322
  # Lints `paths` in workers of `program`, as `options` say.
@@ -3106,7 +3371,7 @@ def Paths.go(
3106
3371
  paths,
3107
3372
  options,
3108
3373
  Options.is_many(options, paths),
3109
- Outcome{0n, [], [], False{}, 0}
3374
+ Outcome{0n, [], [], False{}, 0, []}
3110
3375
  )
3111
3376
 
3112
3377
  # Lints `paths` as `options` say: in workers, if the run can (see Workers), else here.
@@ -3120,13 +3385,16 @@ def Paths.run(
3120
3385
 
3121
3386
  def checking(~rules: List<&1, Rule>, program: String, options: Maybe<&2, Options>) -> IO(Unit):
3122
3387
  match options:
3123
- case Some{Options{+inputs, +fixing, shown, json, worker, jobs}}:
3388
+ case Some{Options{+inputs, +fixing, shown, +json, worker, jobs}}:
3124
3389
  do IO<Unit>:
3125
3390
  Options.note(fixing)
3391
+ given: Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LINT_OVERLAYS")
3392
+ +overlays: List<&2, String> = Overlays.of(given)
3393
+ Overlays.fixing(json, Options.fixes(fixing), overlays)
3126
3394
  IO.bind(
3127
3395
  List<&2, String>,
3128
3396
  Unit,
3129
- files(inputs),
3397
+ files(json, inputs, overlays),
3130
3398
  +paths => Paths.run(
3131
3399
  ~rules,
3132
3400
  program,
@@ -3138,7 +3406,7 @@ def checking(~rules: List<&1, Rule>, program: String, options: Maybe<&2, Options
3138
3406
  IO.die(
3139
3407
  Unit,
3140
3408
  2,
3141
- "usage: bend-lint [<file or glob>...] [--fix | --fix-dangerously] [--show-suppressed] [--json] [--jobs <number or max>]"
3409
+ "usage: bend-lint [<file or glob>...] [--fix | --fix-dangerously] [--show-suppressed] [--json] [--jobs <number or max>] [--overlays] [--report <file>]"
3142
3410
  )
3143
3411
 
3144
3412
  # Runs the command line `args`: the program as invoked, then its arguments.