@prestack/thread 0.1.20 → 0.1.22
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/CHANGELOG.md +193 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.d.ts +2 -0
- package/dist/core/descend.js +18 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +68 -0
- package/dist/core/rows.d.ts +5 -0
- package/dist/core/rows.js +45 -0
- package/dist/core/tree.d.ts +1 -0
- package/dist/core/tree.js +7 -1
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +103 -22
- package/dist/derivation/generators/release.js +74 -26
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.d.ts +3 -9
- package/dist/integrations/cli.js +96 -36
- package/dist/integrations/declared.d.ts +8 -0
- package/dist/integrations/declared.js +22 -0
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +4 -1
- package/dist/integrations/target.js +20 -6
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/generators/publish.js +2 -1
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +234 -24
- package/dist/operations/generators/compile.d.ts +21 -0
- package/dist/operations/generators/compile.js +151 -51
- package/dist/operations/generators/permits.d.ts +2 -1
- package/dist/operations/generators/permits.js +52 -47
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1173
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -459
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -615
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -656
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -232
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -224
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
package/dist/core/descend.ts
DELETED
|
@@ -1,1173 +0,0 @@
|
|
|
1
|
-
import { starts } from '../integrations/cli.ts'
|
|
2
|
-
import { attached, beyond, existsSync, here as answering, kind, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, type Kind } from '../integrations/disk.ts'
|
|
3
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
4
|
-
|
|
5
|
-
import ts from 'typescript'
|
|
6
|
-
|
|
7
|
-
import type { Finding } from './finding.ts'
|
|
8
|
-
import { SHAPED, ignored } from './init.ts'
|
|
9
|
-
import { ORGANISATION, installed } from './scope.ts'
|
|
10
|
-
import { findTree } from './tree.ts'
|
|
11
|
-
|
|
12
|
-
const BUILT: ReadonlySet<string> = new Set(
|
|
13
|
-
ignored()
|
|
14
|
-
.split('\n')
|
|
15
|
-
.map((one) => one.trim())
|
|
16
|
-
.filter((one) => one.endsWith('/'))
|
|
17
|
-
.map((one) => one.replace(/\/+$/, '')),
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
const TREE_AT = ((): ReturnType<typeof findTree> | undefined => {
|
|
21
|
-
try {
|
|
22
|
-
return findTree()
|
|
23
|
-
} catch {
|
|
24
|
-
return undefined
|
|
25
|
-
}
|
|
26
|
-
})()
|
|
27
|
-
|
|
28
|
-
const NOWHERE = '/this-app-has-no-tree'
|
|
29
|
-
|
|
30
|
-
function rooted(): void {
|
|
31
|
-
if (TREE_AT === undefined) findTree()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const ROOT = TREE_AT?.at ?? NOWHERE
|
|
35
|
-
export const TREE = `${TREE_AT?.name ?? NOWHERE}/`
|
|
36
|
-
export const THREAD_AT = `${TREE_AT?.name ?? NOWHERE}/thread.ts`
|
|
37
|
-
|
|
38
|
-
let ignores: readonly string[] | undefined
|
|
39
|
-
let readIgnores = false
|
|
40
|
-
|
|
41
|
-
function ownIgnores(): readonly string[] | undefined {
|
|
42
|
-
if (readIgnores) return ignores
|
|
43
|
-
readIgnores = true
|
|
44
|
-
if (!attached() && starts('git', ['rev-parse', '--is-inside-work-tree'], { at: ROOT }).ok) {
|
|
45
|
-
return ignores
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
ignores = readFileSync(join(ROOT, '.gitignore'), 'utf8')
|
|
49
|
-
.split('\n')
|
|
50
|
-
.map((one) => one.trim())
|
|
51
|
-
.filter((one) => one !== '' && !one.startsWith('#') && !one.startsWith('!'))
|
|
52
|
-
.map((one) => one.replace(/^\/+|\/+$/g, ''))
|
|
53
|
-
.filter((one) => one !== '' && !one.includes('*'))
|
|
54
|
-
} catch {
|
|
55
|
-
ignores = []
|
|
56
|
-
}
|
|
57
|
-
return ignores
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const asked = new Map<string, boolean>()
|
|
61
|
-
|
|
62
|
-
export function unkept(at: string): boolean {
|
|
63
|
-
const held = asked.get(at)
|
|
64
|
-
if (held !== undefined) return held
|
|
65
|
-
const own = ownIgnores()
|
|
66
|
-
const found =
|
|
67
|
-
own === undefined
|
|
68
|
-
? starts('git', ['check-ignore', '-q', '--', at], { at: ROOT }).ok
|
|
69
|
-
: own.some((one) => at === one || at.startsWith(`${one}/`) || at.split('/').includes(one))
|
|
70
|
-
asked.set(at, found)
|
|
71
|
-
return found
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const LISTS = ['declares', 'derives', 'seeds'] as const
|
|
75
|
-
|
|
76
|
-
export const ROW_LISTS = [...LISTS, 'holds'] as const
|
|
77
|
-
|
|
78
|
-
const SOURCE = /\.(ts|tsx|mts|cts)$/
|
|
79
|
-
const RUNNER = /^(?:pnpm|npm|yarn) (?:run )?([\w:.-]+)/
|
|
80
|
-
|
|
81
|
-
export interface Row {
|
|
82
|
-
readonly at?: string
|
|
83
|
-
readonly id?: string
|
|
84
|
-
readonly describes?: string
|
|
85
|
-
readonly by?: string
|
|
86
|
-
readonly runs?: string
|
|
87
|
-
|
|
88
|
-
readonly requires?: readonly string[]
|
|
89
|
-
readonly takes?: readonly string[]
|
|
90
|
-
|
|
91
|
-
readonly type: string
|
|
92
|
-
readonly shape?: ts.Type
|
|
93
|
-
readonly fields: ReadonlyMap<string, ts.Expression>
|
|
94
|
-
readonly where: string
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface Pass {
|
|
98
|
-
|
|
99
|
-
readonly threaded: ReadonlyMap<
|
|
100
|
-
string,
|
|
101
|
-
readonly { readonly list: 'declares' | 'derives' | 'seeds'; readonly by: string; readonly row: Row }[]
|
|
102
|
-
>
|
|
103
|
-
readonly mentioned: ReadonlyMap<string, readonly string[]>
|
|
104
|
-
readonly derived: ReadonlyMap<string, Row>
|
|
105
|
-
|
|
106
|
-
readonly stated: ReadonlyMap<string, Row>
|
|
107
|
-
readonly coined: ReadonlyMap<string, { readonly said: readonly string[]; readonly under: string; readonly from: readonly string[] }>
|
|
108
|
-
readonly broken: readonly Finding[]
|
|
109
|
-
readonly findings: readonly Finding[]
|
|
110
|
-
readonly declarations: number
|
|
111
|
-
readonly rows: number
|
|
112
|
-
readonly sources: readonly string[]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export type { Finding }
|
|
116
|
-
|
|
117
|
-
export function under(pass: Pass, id: string, root: string, seen: Set<string> = new Set()): boolean {
|
|
118
|
-
if (id === root) return true
|
|
119
|
-
if (seen.has(id)) return false
|
|
120
|
-
seen.add(id)
|
|
121
|
-
return (pass.coined.get(id)?.from ?? []).some((one) => under(pass, one, root, seen))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function here(path: string): string {
|
|
125
|
-
return relative(ROOT, path).split('\\').join('/')
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const CANONICAL = 'threads/'
|
|
129
|
-
|
|
130
|
-
const BESIDE = /^\.\//
|
|
131
|
-
|
|
132
|
-
function settle(at: string, where: string): string {
|
|
133
|
-
if (BESIDE.test(at)) return here(resolve(ROOT, dirname(where), at.slice(2)))
|
|
134
|
-
if (at.startsWith(CANONICAL)) return here(resolve(ROOT, TREE, at.slice(CANONICAL.length)))
|
|
135
|
-
return here(resolve(ROOT, at))
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function itself(at: string): string {
|
|
139
|
-
try {
|
|
140
|
-
return realpathSync(resolve(ROOT, at))
|
|
141
|
-
} catch {
|
|
142
|
-
return resolve(ROOT, at)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function walk(dir: string, out: string[]): string[] {
|
|
147
|
-
let entries: readonly string[]
|
|
148
|
-
try {
|
|
149
|
-
entries = readdirSync(dir)
|
|
150
|
-
} catch {
|
|
151
|
-
return out
|
|
152
|
-
}
|
|
153
|
-
for (const name of [...entries].sort()) {
|
|
154
|
-
if (name === '.git' || BUILT.has(name)) continue
|
|
155
|
-
const path = join(dir, name)
|
|
156
|
-
let held
|
|
157
|
-
try {
|
|
158
|
-
held = statSync(path)
|
|
159
|
-
} catch {
|
|
160
|
-
continue
|
|
161
|
-
}
|
|
162
|
-
if (held.isDirectory()) walk(path, out)
|
|
163
|
-
else out.push(here(path))
|
|
164
|
-
}
|
|
165
|
-
return out
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let scanned: readonly string[] | undefined
|
|
169
|
-
|
|
170
|
-
export function sources(): readonly string[] {
|
|
171
|
-
rooted()
|
|
172
|
-
scanned ??= walk(ROOT, []).sort()
|
|
173
|
-
return scanned
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function imported(program: ts.Program): readonly string[] {
|
|
177
|
-
const under = `node_modules/${ORGANISATION}/`
|
|
178
|
-
return program
|
|
179
|
-
.getSourceFiles()
|
|
180
|
-
.map((one) => here(one.fileName))
|
|
181
|
-
.filter((one) => one.includes(under))
|
|
182
|
-
.sort()
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const FALLBACK: ts.CompilerOptions = {
|
|
186
|
-
target: ts.ScriptTarget.Latest,
|
|
187
|
-
module: ts.ModuleKind.ESNext,
|
|
188
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
189
|
-
jsx: ts.JsxEmit.ReactJSX,
|
|
190
|
-
strict: true,
|
|
191
|
-
allowImportingTsExtensions: true,
|
|
192
|
-
allowJs: true,
|
|
193
|
-
resolveJsonModule: true,
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const CONFIGS = ['tsconfig.json', 'tsconfig.base.json'] as const
|
|
197
|
-
|
|
198
|
-
const INVARIANT: ts.CompilerOptions = {
|
|
199
|
-
noEmit: true,
|
|
200
|
-
noUnusedLocals: true,
|
|
201
|
-
skipLibCheck: true,
|
|
202
|
-
allowImportingTsExtensions: true,
|
|
203
|
-
allowJs: true,
|
|
204
|
-
jsx: ts.JsxEmit.ReactJSX,
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const NEVER: readonly string[] = ['node_modules', 'bower_components', 'jspm_packages']
|
|
208
|
-
|
|
209
|
-
interface Wanted {
|
|
210
|
-
readonly under: string
|
|
211
|
-
readonly ending: string
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function bounds(from: string, one: string): Wanted {
|
|
215
|
-
const star = one.indexOf('*')
|
|
216
|
-
if (star === -1) return { under: resolve(from, one), ending: '' }
|
|
217
|
-
const head = one.slice(0, star)
|
|
218
|
-
const cut = head.lastIndexOf('/')
|
|
219
|
-
const tail = one.slice(star)
|
|
220
|
-
const dot = tail.lastIndexOf('.')
|
|
221
|
-
const end = dot === -1 ? '' : tail.slice(dot)
|
|
222
|
-
return {
|
|
223
|
-
under: resolve(from, cut === -1 ? '' : head.slice(0, cut)),
|
|
224
|
-
ending: end.includes('/') || end.includes('*') ? '' : end,
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function wanted(at: string, only: readonly Wanted[]): boolean {
|
|
229
|
-
if (only.length === 0) return true
|
|
230
|
-
return only.some((one) => (at === one.under || at.startsWith(`${one.under}/`)) && at.endsWith(one.ending))
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function listing(at: string, take: readonly string[], skip: readonly string[], only: readonly Wanted[], deep: number): readonly string[] {
|
|
234
|
-
const found: string[] = []
|
|
235
|
-
if (deep < 0 || !existsSync(at)) return found
|
|
236
|
-
for (const one of readdirSync(at, { withFileTypes: true })) {
|
|
237
|
-
const full = join(at, one.name)
|
|
238
|
-
if (skip.some((each) => full === each || full.startsWith(`${each}/`))) continue
|
|
239
|
-
if (one.isDirectory()) {
|
|
240
|
-
if (NEVER.includes(one.name)) continue
|
|
241
|
-
found.push(...listing(full, take, skip, only, deep - 1))
|
|
242
|
-
} else if (take.some((each) => one.name.endsWith(each)) && wanted(full, only)) {
|
|
243
|
-
found.push(full)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return found
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function parsing(from: string): ts.ParseConfigHost {
|
|
250
|
-
return {
|
|
251
|
-
useCaseSensitiveFileNames: true,
|
|
252
|
-
fileExists: (at) => !beyond(at) && existsSync(at),
|
|
253
|
-
readFile: (at) => {
|
|
254
|
-
try {
|
|
255
|
-
return readFileSync(at)
|
|
256
|
-
} catch {
|
|
257
|
-
return undefined
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
|
-
readDirectory: (at, take, skip, only, deep) =>
|
|
261
|
-
listing(
|
|
262
|
-
at,
|
|
263
|
-
take,
|
|
264
|
-
(skip ?? []).map((one) => resolve(from, one.endsWith('/**') ? one.slice(0, -3) : one)),
|
|
265
|
-
only.map((one) => bounds(from, one)),
|
|
266
|
-
deep ?? Number.MAX_SAFE_INTEGER,
|
|
267
|
-
),
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function configured(): { readonly options: ts.CompilerOptions; readonly from: string; readonly trouble?: string } {
|
|
272
|
-
for (const name of CONFIGS) {
|
|
273
|
-
const at = join(ROOT, name)
|
|
274
|
-
if (!existsSync(at)) continue
|
|
275
|
-
|
|
276
|
-
const host = parsing(ROOT)
|
|
277
|
-
const held = ts.readConfigFile(at, host.readFile)
|
|
278
|
-
if (held.error !== undefined) {
|
|
279
|
-
return {
|
|
280
|
-
options: { ...FALLBACK, ...INVARIANT },
|
|
281
|
-
from: '',
|
|
282
|
-
trouble: `${name} is there and cannot be read: ${ts.flattenDiagnosticMessageText(held.error.messageText, ' ')}`,
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const parsed = ts.parseJsonConfigFileContent(held.config as object, host, dirname(at))
|
|
287
|
-
const complained = parsed.errors[0]
|
|
288
|
-
if (complained !== undefined) {
|
|
289
|
-
return {
|
|
290
|
-
options: { ...FALLBACK, ...INVARIANT },
|
|
291
|
-
from: '',
|
|
292
|
-
trouble: `${name} is there and states no options this gate can use: ${ts.flattenDiagnosticMessageText(complained.messageText, ' ')}`,
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return { options: { ...parsed.options, ...INVARIANT }, from: name }
|
|
297
|
-
}
|
|
298
|
-
return { options: { ...FALLBACK, ...INVARIANT }, from: '' }
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const libraries = new Map<string, ReturnType<ts.CompilerHost['getSourceFile']>>()
|
|
302
|
-
|
|
303
|
-
const DEPENDENCIES = '/node_' + 'modules/'
|
|
304
|
-
|
|
305
|
-
export function hosting(options: ts.CompilerOptions): ts.CompilerHost {
|
|
306
|
-
const host = ts.createCompilerHost(options, true)
|
|
307
|
-
const lib = host.getDefaultLibLocation?.()
|
|
308
|
-
const settings = JSON.stringify(options)
|
|
309
|
-
const toolchain = (at: string): boolean => lib !== undefined && (at === lib || at.startsWith(`${lib}/`))
|
|
310
|
-
const seen = (at: string): Kind => (beyond(at) ? 'none' : kind(at))
|
|
311
|
-
return {
|
|
312
|
-
...host,
|
|
313
|
-
getCurrentDirectory: () => ROOT,
|
|
314
|
-
fileExists: (at) => (toolchain(at) ? host.fileExists(at) : seen(at) === 'file'),
|
|
315
|
-
directoryExists: (at) => (toolchain(at) ? (host.directoryExists?.(at) ?? false) : seen(at) === 'folder'),
|
|
316
|
-
readFile: (at) => (toolchain(at) ? host.readFile(at) : seen(at) === 'file' ? readFileSync(at) : undefined),
|
|
317
|
-
getSourceFile: (at, version) => {
|
|
318
|
-
const lasting = toolchain(at) ? true : at.includes(DEPENDENCIES)
|
|
319
|
-
const key = settings + at + JSON.stringify(version)
|
|
320
|
-
if (lasting && libraries.has(key)) return libraries.get(key)
|
|
321
|
-
const made = toolchain(at)
|
|
322
|
-
? host.getSourceFile(at, version)
|
|
323
|
-
: seen(at) === 'file'
|
|
324
|
-
? ts.createSourceFile(at, readFileSync(at), version, true)
|
|
325
|
-
: undefined
|
|
326
|
-
if (lasting) libraries.set(key, made)
|
|
327
|
-
return made
|
|
328
|
-
},
|
|
329
|
-
getDirectories: (at) =>
|
|
330
|
-
seen(at) === 'folder' ? readdirSync(at, { withFileTypes: true }).filter((one) => one.isDirectory()).map((one) => one.name) : [],
|
|
331
|
-
realpath: (at) => (toolchain(at) ? (host.realpath?.(at) ?? at) : realpathSync(at)),
|
|
332
|
-
writeFile: (at, said) => {
|
|
333
|
-
writeFileSync(at, said)
|
|
334
|
-
},
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
let built: { program: ts.Program; checker: ts.TypeChecker } | undefined
|
|
339
|
-
|
|
340
|
-
function compiled(): { program: ts.Program; checker: ts.TypeChecker } {
|
|
341
|
-
rooted()
|
|
342
|
-
if (built !== undefined) return built
|
|
343
|
-
const roots = [THREAD_AT, ...sources()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one))
|
|
344
|
-
const options = configured().options
|
|
345
|
-
const program = ts.createProgram([...new Set(roots)], options, hosting(options))
|
|
346
|
-
built = { program, checker: program.getTypeChecker() }
|
|
347
|
-
return built
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export function uncompiled(): readonly Finding[] {
|
|
351
|
-
const { program } = compiled()
|
|
352
|
-
const found: Finding[] = []
|
|
353
|
-
const INSTALLED = 'node_' + 'modules'
|
|
354
|
-
for (const file of program.getSourceFiles()) {
|
|
355
|
-
if (file.isDeclarationFile) continue
|
|
356
|
-
const at = here(file.fileName)
|
|
357
|
-
if (at.startsWith('..') || at.split('/').includes(INSTALLED)) continue
|
|
358
|
-
for (const one of [...program.getSyntacticDiagnostics(file), ...program.getSemanticDiagnostics(file)]) {
|
|
359
|
-
const start = one.start === undefined ? undefined : file.getLineAndCharacterOfPosition(one.start)
|
|
360
|
-
found.push({
|
|
361
|
-
file: at,
|
|
362
|
-
at: start === undefined ? undefined : { line: start.line + 1, column: start.character + 1 },
|
|
363
|
-
detail: `will not compile — TS${String(one.code)}: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
|
|
364
|
-
instead:
|
|
365
|
-
'fix the type error. Nothing in this tree is built, served or gated while a file it names does not compile, and every other surface here reads the same program',
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return found
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function threadedType(checker: ts.TypeChecker, program: ts.Program): ts.Type | undefined {
|
|
373
|
-
const root = program.getSourceFile(resolve(ROOT, THREAD_AT))
|
|
374
|
-
if (root === undefined) return undefined
|
|
375
|
-
for (const statement of root.statements) {
|
|
376
|
-
if (!ts.isImportDeclaration(statement)) continue
|
|
377
|
-
const bindings = statement.importClause?.namedBindings
|
|
378
|
-
if (bindings === undefined || !ts.isNamedImports(bindings)) continue
|
|
379
|
-
for (const one of bindings.elements) {
|
|
380
|
-
const symbol = checker.getSymbolAtLocation(one.name)
|
|
381
|
-
if (symbol === undefined) continue
|
|
382
|
-
const real = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol
|
|
383
|
-
if (real.name === 'Threaded') return checker.getDeclaredTypeOfSymbol(real)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return undefined
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const source = new Map<string, string | undefined>()
|
|
390
|
-
|
|
391
|
-
export function read(at: string): string | undefined {
|
|
392
|
-
if (source.has(at)) return source.get(at)
|
|
393
|
-
let text: string | undefined
|
|
394
|
-
try {
|
|
395
|
-
text = readFileSync(resolve(ROOT, at), 'utf8')
|
|
396
|
-
} catch {
|
|
397
|
-
text = undefined
|
|
398
|
-
}
|
|
399
|
-
source.set(at, text)
|
|
400
|
-
return text
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const shapes = new WeakMap<object, ReadonlyMap<string, readonly string[]>>()
|
|
404
|
-
|
|
405
|
-
export function shapesNamed(): ReadonlyMap<string, readonly string[]> {
|
|
406
|
-
const held = compiled()
|
|
407
|
-
const known = shapes.get(held)
|
|
408
|
-
if (known !== undefined) return known
|
|
409
|
-
const { program, checker } = held
|
|
410
|
-
const shape = threadedType(checker, program)
|
|
411
|
-
const found = new Map<string, string[]>()
|
|
412
|
-
if (shape === undefined) return found
|
|
413
|
-
for (const at of [...sources(), ...imported(program)]) {
|
|
414
|
-
if (!SOURCE.test(at)) continue
|
|
415
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
416
|
-
if (file === undefined) continue
|
|
417
|
-
for (const statement of file.statements) {
|
|
418
|
-
if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
|
|
419
|
-
if ((statement.typeParameters ?? []).length > 0) continue
|
|
420
|
-
const symbol = checker.getSymbolAtLocation(statement.name)
|
|
421
|
-
if (symbol === undefined) continue
|
|
422
|
-
const shown = (statement.modifiers ?? []).some((one) => one.kind === ts.SyntaxKind.ExportKeyword)
|
|
423
|
-
if (!shown && statement.name.text !== 'Threaded') continue
|
|
424
|
-
const held = ts.isInterfaceDeclaration(statement)
|
|
425
|
-
? checker.getDeclaredTypeOfSymbol(symbol)
|
|
426
|
-
: checker.getTypeAtLocation(statement.name)
|
|
427
|
-
const named = statement.name.text
|
|
428
|
-
const shadows = named === 'Threaded'
|
|
429
|
-
const kind = elementOf(checker, held)
|
|
430
|
-
if (!shadows && (kind.flags & ts.TypeFlags.Never) !== 0) continue
|
|
431
|
-
if (!shadows && !checker.isTypeAssignableTo(kind, shape)) continue
|
|
432
|
-
const where = found.get(named) ?? []
|
|
433
|
-
const real = itself(at)
|
|
434
|
-
if (where.some((one) => itself(one) === real)) continue
|
|
435
|
-
found.set(named, [...where, at])
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
shapes.set(held, found)
|
|
439
|
-
return found
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export function threading(): ReadonlySet<string> {
|
|
443
|
-
return new Set(['Threaded', ...shapesNamed().keys()])
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export function shapeFields(
|
|
447
|
-
named: string,
|
|
448
|
-
inside?: string,
|
|
449
|
-
): readonly { readonly name: string; readonly required: boolean; readonly list: boolean; readonly map: boolean; readonly worded: boolean; readonly guide: string; readonly record: readonly string[] }[] {
|
|
450
|
-
const { program, checker } = compiled()
|
|
451
|
-
for (const at of [...sources(), ...imported(program)]) {
|
|
452
|
-
if (!SOURCE.test(at)) continue
|
|
453
|
-
if (inside !== undefined && !at.split('\\').join('/').endsWith(inside)) continue
|
|
454
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
455
|
-
if (file === undefined) continue
|
|
456
|
-
for (const statement of file.statements) {
|
|
457
|
-
if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
|
|
458
|
-
if (statement.name.text !== named) continue
|
|
459
|
-
const symbol = checker.getSymbolAtLocation(statement.name)
|
|
460
|
-
if (symbol === undefined) continue
|
|
461
|
-
const held = ts.isInterfaceDeclaration(statement)
|
|
462
|
-
? checker.getDeclaredTypeOfSymbol(symbol)
|
|
463
|
-
: checker.getTypeAtLocation(statement.name)
|
|
464
|
-
const mapped = (kind: ts.Type): boolean =>
|
|
465
|
-
(kind.isUnion() ? kind.types : [kind]).some(
|
|
466
|
-
(each) =>
|
|
467
|
-
(each.flags & ts.TypeFlags.StringLike) === 0 &&
|
|
468
|
-
checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined &&
|
|
469
|
-
checker.getIndexTypeOfType(each, ts.IndexKind.String) !== undefined,
|
|
470
|
-
)
|
|
471
|
-
const listed = (kind: ts.Type): boolean =>
|
|
472
|
-
(kind.isUnion() ? kind.types : [kind]).some(
|
|
473
|
-
(each) =>
|
|
474
|
-
(each.flags & ts.TypeFlags.StringLike) === 0 &&
|
|
475
|
-
checker.getIndexTypeOfType(each, ts.IndexKind.Number) !== undefined,
|
|
476
|
-
)
|
|
477
|
-
return checker.getPropertiesOfType(held).map((one) => {
|
|
478
|
-
const kind = checker.getTypeOfSymbolAtLocation(one, statement)
|
|
479
|
-
return {
|
|
480
|
-
name: one.getName(),
|
|
481
|
-
required: (one.flags & ts.SymbolFlags.Optional) === 0,
|
|
482
|
-
list: listed(kind),
|
|
483
|
-
map: mapped(kind),
|
|
484
|
-
worded: (kind.isUnion() ? kind.types : [kind]).some((each) => (each.flags & ts.TypeFlags.StringLike) !== 0),
|
|
485
|
-
guide: ts.displayPartsToString(one.getDocumentationComment(checker)).trim(),
|
|
486
|
-
record: recordOf(checker, kind),
|
|
487
|
-
}
|
|
488
|
-
})
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return []
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
export interface Field {
|
|
495
|
-
readonly name: string
|
|
496
|
-
readonly required: boolean
|
|
497
|
-
readonly list: boolean
|
|
498
|
-
readonly map: boolean
|
|
499
|
-
readonly boolean: boolean
|
|
500
|
-
readonly worded: boolean
|
|
501
|
-
readonly guide: string
|
|
502
|
-
readonly record: readonly string[]
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function elementAt(at: string, list: string, within?: number): { readonly type: ts.Type; readonly node: ts.Node } | undefined {
|
|
506
|
-
const { program, checker } = compiled()
|
|
507
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
508
|
-
if (file === undefined) return undefined
|
|
509
|
-
const threaded = threadedType(checker, program)
|
|
510
|
-
|
|
511
|
-
let held: { readonly type: ts.Type; readonly node: ts.Node } | undefined
|
|
512
|
-
|
|
513
|
-
const consider = (node: ts.ArrayLiteralExpression): void => {
|
|
514
|
-
if (held !== undefined) return
|
|
515
|
-
if (within !== undefined && node.getStart() !== within) return
|
|
516
|
-
const wanted = checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
|
|
517
|
-
const kinds = (wanted.isUnionOrIntersection() ? wanted.types : [wanted])
|
|
518
|
-
.filter((one) => checker.getIndexTypeOfType(one, ts.IndexKind.Number) !== undefined)
|
|
519
|
-
.map((one) => elementOf(checker, one))
|
|
520
|
-
const type =
|
|
521
|
-
kinds.find((one) => threaded !== undefined && checker.isTypeAssignableTo(one, threaded)) ??
|
|
522
|
-
(kinds.length === 1 ? kinds[0] : undefined)
|
|
523
|
-
if (type !== undefined) held = { type, node }
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const visit = (node: ts.Node): void => {
|
|
527
|
-
if (held !== undefined) return
|
|
528
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === list) {
|
|
529
|
-
const initial = node.initializer
|
|
530
|
-
if (initial !== undefined && ts.isArrayLiteralExpression(initial)) consider(initial)
|
|
531
|
-
}
|
|
532
|
-
if (ts.isPropertyAssignment(node)) {
|
|
533
|
-
const key = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : undefined
|
|
534
|
-
if (key === list && ts.isArrayLiteralExpression(node.initializer)) consider(node.initializer)
|
|
535
|
-
}
|
|
536
|
-
ts.forEachChild(node, visit)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
visit(file)
|
|
540
|
-
return held
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
export function listShape(at: string, list: string): string | undefined {
|
|
544
|
-
const held = elementAt(at, list)
|
|
545
|
-
return held === undefined ? undefined : compiled().checker.typeToString(held.type)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
export function listFields(at: string, list: string, within?: number): readonly Field[] {
|
|
549
|
-
const held = elementAt(at, list, within)
|
|
550
|
-
if (held === undefined) return []
|
|
551
|
-
const { checker } = compiled()
|
|
552
|
-
const bare = (kind: ts.Type): readonly ts.Type[] =>
|
|
553
|
-
(kind.isUnion() ? kind.types : [kind]).filter((each) => (each.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) === 0)
|
|
554
|
-
const mapped = (kind: ts.Type): boolean =>
|
|
555
|
-
bare(kind).some(
|
|
556
|
-
(each) =>
|
|
557
|
-
(each.flags & ts.TypeFlags.StringLike) === 0 &&
|
|
558
|
-
checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined &&
|
|
559
|
-
checker.getIndexTypeOfType(each, ts.IndexKind.String) !== undefined,
|
|
560
|
-
)
|
|
561
|
-
const listed = (kind: ts.Type): boolean =>
|
|
562
|
-
bare(kind).some(
|
|
563
|
-
(each) => (each.flags & ts.TypeFlags.StringLike) === 0 && checker.getIndexTypeOfType(each, ts.IndexKind.Number) !== undefined,
|
|
564
|
-
)
|
|
565
|
-
const flagged = (kind: ts.Type): boolean => {
|
|
566
|
-
const kinds = bare(kind)
|
|
567
|
-
return kinds.length > 0 && kinds.every((each) => (each.flags & ts.TypeFlags.BooleanLike) !== 0)
|
|
568
|
-
}
|
|
569
|
-
const worded = (kind: ts.Type): boolean => bare(kind).some((each) => (each.flags & ts.TypeFlags.StringLike) !== 0)
|
|
570
|
-
return checker.getPropertiesOfType(held.type).map((one) => {
|
|
571
|
-
const kind = checker.getTypeOfSymbolAtLocation(one, held.node)
|
|
572
|
-
return {
|
|
573
|
-
name: one.getName(),
|
|
574
|
-
required: (one.flags & ts.SymbolFlags.Optional) === 0,
|
|
575
|
-
list: listed(kind),
|
|
576
|
-
map: mapped(kind),
|
|
577
|
-
boolean: flagged(kind),
|
|
578
|
-
worded: worded(kind),
|
|
579
|
-
guide: ts.displayPartsToString(one.getDocumentationComment(checker)).trim(),
|
|
580
|
-
record: recordOf(checker, kind),
|
|
581
|
-
}
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function elementOf(checker: ts.TypeChecker, type: ts.Type): ts.Type {
|
|
586
|
-
return checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function recordOf(checker: ts.TypeChecker, type: ts.Type): readonly string[] {
|
|
590
|
-
for (const each of type.isUnion() ? type.types : [type]) {
|
|
591
|
-
if ((each.flags & ts.TypeFlags.StringLike) !== 0 || checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined) continue
|
|
592
|
-
const element = elementOf(checker, each)
|
|
593
|
-
if ((element.flags & ts.TypeFlags.Object) === 0) return []
|
|
594
|
-
const fields = checker.getPropertiesOfType(element)
|
|
595
|
-
const worded = fields.every((one) => {
|
|
596
|
-
const kind = checker.getNonNullableType(checker.getTypeOfSymbol(one))
|
|
597
|
-
return (kind.isUnion() ? kind.types : [kind]).every((part) => (part.flags & ts.TypeFlags.StringLike) !== 0)
|
|
598
|
-
})
|
|
599
|
-
return worded ? fields.map((one) => one.getName()) : []
|
|
600
|
-
}
|
|
601
|
-
return []
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
export function rowShape(at: string, list: string): string | undefined {
|
|
605
|
-
const { program, checker } = compiled()
|
|
606
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
607
|
-
const threaded = threadedType(checker, program)
|
|
608
|
-
if (file === undefined || threaded === undefined) return undefined
|
|
609
|
-
|
|
610
|
-
let held: ts.Type | undefined
|
|
611
|
-
|
|
612
|
-
const consider = (node: ts.ArrayLiteralExpression): void => {
|
|
613
|
-
if (held !== undefined) return
|
|
614
|
-
const wanted = checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
|
|
615
|
-
const each = (wanted.isUnionOrIntersection() ? wanted.types : [wanted]).map((one) => elementOf(checker, one))
|
|
616
|
-
held = each.find((one) => checker.isTypeAssignableTo(one, threaded))
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const visit = (node: ts.Node): void => {
|
|
620
|
-
if (held !== undefined) return
|
|
621
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === list) {
|
|
622
|
-
const initial = node.initializer
|
|
623
|
-
if (initial !== undefined && ts.isArrayLiteralExpression(initial)) consider(initial)
|
|
624
|
-
}
|
|
625
|
-
if (ts.isPropertyAssignment(node)) {
|
|
626
|
-
const key = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : undefined
|
|
627
|
-
if (key === list && ts.isArrayLiteralExpression(node.initializer)) consider(node.initializer)
|
|
628
|
-
}
|
|
629
|
-
ts.forEachChild(node, visit)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
visit(file)
|
|
633
|
-
return held === undefined ? undefined : checker.typeToString(held)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function literal(node: ts.Expression | undefined): string | undefined {
|
|
637
|
-
if (node === undefined) return undefined
|
|
638
|
-
return ts.isStringLiteralLike(node) ? node.text : undefined
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function fieldsOf(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
|
|
642
|
-
const found = new Map<string, ts.Expression>()
|
|
643
|
-
for (const property of node.properties) {
|
|
644
|
-
if (!ts.isPropertyAssignment(property)) continue
|
|
645
|
-
const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name) ? property.name.text : undefined
|
|
646
|
-
if (key !== undefined) found.set(key, property.initializer)
|
|
647
|
-
}
|
|
648
|
-
return found
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function wordsOf(node: ts.Expression | undefined): readonly string[] {
|
|
652
|
-
if (node === undefined || !ts.isArrayLiteralExpression(node)) return []
|
|
653
|
-
return node.elements.map((one) => literal(one)).filter((one): one is string => one !== undefined)
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function rowOf(node: ts.ObjectLiteralExpression, type: string, where: string, shape?: ts.Type): Row {
|
|
657
|
-
const fields = fieldsOf(node)
|
|
658
|
-
const named = literal(fields.get('at'))
|
|
659
|
-
const at = named === undefined ? undefined : settle(named, where)
|
|
660
|
-
const id = literal(fields.get('id'))
|
|
661
|
-
const describes = literal(fields.get('describes'))
|
|
662
|
-
const by = literal(fields.get('by'))
|
|
663
|
-
const runs = literal(fields.get('runs'))
|
|
664
|
-
const requires = wordsOf(fields.get('requires'))
|
|
665
|
-
const takes = wordsOf(fields.get('takes'))
|
|
666
|
-
return {
|
|
667
|
-
...(at === undefined ? {} : { at }),
|
|
668
|
-
...(id === undefined ? {} : { id }),
|
|
669
|
-
...(describes === undefined ? {} : { describes }),
|
|
670
|
-
...(by === undefined ? {} : { by }),
|
|
671
|
-
...(runs === undefined ? {} : { runs }),
|
|
672
|
-
...(requires.length === 0 ? {} : { requires }),
|
|
673
|
-
...(takes.length === 0 ? {} : { takes }),
|
|
674
|
-
...(shape === undefined ? {} : { shape }),
|
|
675
|
-
type,
|
|
676
|
-
fields,
|
|
677
|
-
where,
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function unwrap(node: ts.Expression | undefined): ts.Expression | undefined {
|
|
682
|
-
let held = node
|
|
683
|
-
for (;;) {
|
|
684
|
-
if (held === undefined) return undefined
|
|
685
|
-
if (ts.isParenthesizedExpression(held) || ts.isSatisfiesExpression(held) || ts.isAsExpression(held)) {
|
|
686
|
-
held = held.expression
|
|
687
|
-
continue
|
|
688
|
-
}
|
|
689
|
-
return held
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
interface Held {
|
|
694
|
-
readonly name: string
|
|
695
|
-
readonly type: string
|
|
696
|
-
readonly shape: ts.Type
|
|
697
|
-
readonly node: ts.ObjectLiteralExpression
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
export function nodesIn(at: string): readonly Held[] {
|
|
701
|
-
const { program, checker } = compiled()
|
|
702
|
-
const threaded = threadedType(checker, program)
|
|
703
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
704
|
-
if (file === undefined || threaded === undefined) return []
|
|
705
|
-
|
|
706
|
-
const found: Held[] = []
|
|
707
|
-
|
|
708
|
-
const take = (name: string, where: ts.Node, initial: ts.Expression | undefined): void => {
|
|
709
|
-
const held = unwrap(initial)
|
|
710
|
-
if (held === undefined) return
|
|
711
|
-
|
|
712
|
-
const recognised = (one: ts.Type): boolean => (one.flags & ts.TypeFlags.Any) === 0 && checker.isTypeAssignableTo(one, threaded)
|
|
713
|
-
const declared = checker.getTypeAtLocation(where)
|
|
714
|
-
const each = (declared.isUnionOrIntersection() ? declared.types : [declared]).map((one) => elementOf(checker, one))
|
|
715
|
-
const stated = each.find((one) => recognised(one))
|
|
716
|
-
|
|
717
|
-
const parts = ts.isArrayLiteralExpression(held) ? held.elements : [held]
|
|
718
|
-
for (const one of parts) {
|
|
719
|
-
const object = unwrap(one)
|
|
720
|
-
if (object === undefined || !ts.isObjectLiteralExpression(object)) continue
|
|
721
|
-
const mine = checker.getWidenedType(checker.getTypeAtLocation(object))
|
|
722
|
-
const known = stated ?? (recognised(mine) ? mine : undefined)
|
|
723
|
-
if (known === undefined) continue
|
|
724
|
-
found.push({ name, type: checker.typeToString(known), shape: known, node: object })
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const step = (statements: readonly ts.Statement[]): void => {
|
|
729
|
-
for (const statement of statements) {
|
|
730
|
-
if (ts.isModuleDeclaration(statement) && statement.body !== undefined && ts.isModuleBlock(statement.body)) {
|
|
731
|
-
step(statement.body.statements)
|
|
732
|
-
continue
|
|
733
|
-
}
|
|
734
|
-
if (ts.isExportAssignment(statement)) {
|
|
735
|
-
take('default', statement.expression, statement.expression)
|
|
736
|
-
continue
|
|
737
|
-
}
|
|
738
|
-
if (!ts.isVariableStatement(statement)) continue
|
|
739
|
-
for (const one of statement.declarationList.declarations) {
|
|
740
|
-
if (!ts.isIdentifier(one.name) || one.initializer === undefined) continue
|
|
741
|
-
take(one.name.text, one.name, one.initializer)
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
step(file.statements)
|
|
747
|
-
return found
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
let commands: ReadonlySet<string> | undefined
|
|
751
|
-
|
|
752
|
-
function scripts(): ReadonlySet<string> {
|
|
753
|
-
if (commands !== undefined) return commands
|
|
754
|
-
const found = new Set<string>()
|
|
755
|
-
try {
|
|
756
|
-
const held = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as { scripts?: Record<string, unknown> }
|
|
757
|
-
for (const name of Object.keys(held.scripts ?? {})) found.add(name)
|
|
758
|
-
} catch {
|
|
759
|
-
found.clear()
|
|
760
|
-
}
|
|
761
|
-
commands = found
|
|
762
|
-
return found
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function mentionsIn(at: string): readonly string[] {
|
|
766
|
-
const found: string[] = []
|
|
767
|
-
const step = (node: ts.ObjectLiteralExpression): void => {
|
|
768
|
-
const fields = fieldsOf(node)
|
|
769
|
-
for (const list of LISTS) {
|
|
770
|
-
const value = fields.get(list)
|
|
771
|
-
if (value === undefined || !ts.isArrayLiteralExpression(value)) continue
|
|
772
|
-
for (const one of value.elements) {
|
|
773
|
-
if (!ts.isObjectLiteralExpression(one)) continue
|
|
774
|
-
const held = literal(fieldsOf(one).get('at'))
|
|
775
|
-
if (held !== undefined) found.push(settle(held, at))
|
|
776
|
-
step(one)
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
for (const one of nodesIn(at)) step(one.node)
|
|
781
|
-
return found
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
let passed: Pass | undefined
|
|
785
|
-
|
|
786
|
-
export function reached(): readonly string[] {
|
|
787
|
-
return [THREAD_AT, ...descend().threaded.keys()]
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
let world: object = answering()
|
|
791
|
-
|
|
792
|
-
const worlds = new WeakMap<object, () => void>()
|
|
793
|
-
|
|
794
|
-
function kept(): () => void {
|
|
795
|
-
const was = { passed, built, scanned, commands, ignores, readIgnores, source: new Map(source), asked: new Map(asked) }
|
|
796
|
-
return () => {
|
|
797
|
-
passed = was.passed
|
|
798
|
-
built = was.built
|
|
799
|
-
scanned = was.scanned
|
|
800
|
-
commands = was.commands
|
|
801
|
-
ignores = was.ignores
|
|
802
|
-
readIgnores = was.readIgnores
|
|
803
|
-
source.clear()
|
|
804
|
-
for (const [at, text] of was.source) source.set(at, text)
|
|
805
|
-
asked.clear()
|
|
806
|
-
for (const [at, ignore] of was.asked) asked.set(at, ignore)
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
export function forget(): void {
|
|
811
|
-
const now = answering()
|
|
812
|
-
if (now === world) libraries.clear()
|
|
813
|
-
else {
|
|
814
|
-
worlds.set(world, kept())
|
|
815
|
-
world = now
|
|
816
|
-
const back = worlds.get(now)
|
|
817
|
-
if (back !== undefined) {
|
|
818
|
-
back()
|
|
819
|
-
return
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
passed = undefined
|
|
823
|
-
built = undefined
|
|
824
|
-
scanned = undefined
|
|
825
|
-
commands = undefined
|
|
826
|
-
source.clear()
|
|
827
|
-
asked.clear()
|
|
828
|
-
ignores = undefined
|
|
829
|
-
readIgnores = false
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
export function descend(): Pass {
|
|
833
|
-
rooted()
|
|
834
|
-
if (passed !== undefined) return passed
|
|
835
|
-
|
|
836
|
-
const { program, checker } = compiled()
|
|
837
|
-
const threaded = new Map<string, { list: 'declares' | 'derives' | 'seeds'; by: string; row: Row }[]>()
|
|
838
|
-
const mentioned = new Map<string, string[]>()
|
|
839
|
-
const derived = new Map<string, Row>()
|
|
840
|
-
const stated = new Map<string, Row>()
|
|
841
|
-
const coined = new Map<string, { said: string[]; under: string; from: string[] }>()
|
|
842
|
-
const broken: Finding[] = []
|
|
843
|
-
const findings: Finding[] = []
|
|
844
|
-
const opened = new Set<string>()
|
|
845
|
-
const shaped = new Map<string, { type: string; shape?: ts.Type; by: string }>()
|
|
846
|
-
const made = new Map<string, { by: Set<string>; rows: string[]; where: string }>()
|
|
847
|
-
const absent = new Map<string, { at: number; by: Set<string>; rows: number }>()
|
|
848
|
-
const demanded: { row: Row; word: string; list: string }[] = []
|
|
849
|
-
const thread: { row: Row; command: string }[] = []
|
|
850
|
-
const offered = new Set<string>()
|
|
851
|
-
let declarations = 0
|
|
852
|
-
let rows = 0
|
|
853
|
-
|
|
854
|
-
const root = program.getSourceFile(resolve(ROOT, THREAD_AT))
|
|
855
|
-
if (root === undefined) {
|
|
856
|
-
broken.push({
|
|
857
|
-
file: THREAD_AT,
|
|
858
|
-
detail: 'the root of the tree cannot be read, so nothing below it is reached and every file here reads as unthreaded',
|
|
859
|
-
instead: 'write the root, or run `thread init`',
|
|
860
|
-
})
|
|
861
|
-
passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
|
|
862
|
-
return passed
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const shape = threadedType(checker, program)
|
|
866
|
-
if (shape === undefined) {
|
|
867
|
-
broken.push({
|
|
868
|
-
file: THREAD_AT,
|
|
869
|
-
detail: 'the root does not resolve `Threaded`, so nothing here can be recognised as a declaration and every file reads as unthreaded',
|
|
870
|
-
instead: `import \`Threaded\` from \`${SHAPED}\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
|
|
871
|
-
})
|
|
872
|
-
passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
|
|
873
|
-
return passed
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const said = (by: string): string => (by === '' ? 'the root' : by)
|
|
877
|
-
const names = (row: Row): string => row.id ?? row.at ?? 'a row stating neither an id nor an at'
|
|
878
|
-
|
|
879
|
-
function coin(id: string | undefined, describes: string | undefined, under: string, parent: string): void {
|
|
880
|
-
if (id === undefined) return
|
|
881
|
-
const held = coined.get(id) ?? { said: [], under, from: [] }
|
|
882
|
-
const meaning = describes ?? ''
|
|
883
|
-
if (!held.said.includes(meaning)) held.said.push(meaning)
|
|
884
|
-
if (parent !== '' && parent !== id && !held.from.includes(parent)) held.from.push(parent)
|
|
885
|
-
coined.set(id, held)
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function judge(row: Row, by: string, list: (typeof LISTS)[number], own: boolean): void {
|
|
889
|
-
const at = row.where
|
|
890
|
-
|
|
891
|
-
for (const field of ['by', 'runs'] as const) {
|
|
892
|
-
const command = row[field]
|
|
893
|
-
if (command === undefined) continue
|
|
894
|
-
if (command.startsWith('thread ')) {
|
|
895
|
-
if (field === 'by') offered.add(command)
|
|
896
|
-
else thread.push({ row, command })
|
|
897
|
-
continue
|
|
898
|
-
}
|
|
899
|
-
const held = RUNNER.exec(command)
|
|
900
|
-
if (held === null || scripts().has(held[1] ?? '')) continue
|
|
901
|
-
findings.push({
|
|
902
|
-
file: at,
|
|
903
|
-
detail: `\`${names(row)}\` states \`${field}: '${command}'\`, and package.json states no \`${held[1] ?? ''}\` script`,
|
|
904
|
-
instead: 'add the script, or fix the name. A row naming a command is a promise that the command exists',
|
|
905
|
-
})
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
for (const field of ['requires', 'takes'] as const) {
|
|
909
|
-
for (const word of wordsOf(row.fields.get(field))) demanded.push({ row, word, list: field })
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (list === 'derives' && row.at !== undefined && !own) {
|
|
913
|
-
const held = made.get(row.at) ?? { by: new Set<string>(), rows: [] as string[], where: at }
|
|
914
|
-
if (row.by !== undefined) held.by.add(row.by)
|
|
915
|
-
held.rows.push(names(row))
|
|
916
|
-
made.set(row.at, held)
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (row.at === THREAD_AT && !own) {
|
|
920
|
-
findings.push({
|
|
921
|
-
file: at,
|
|
922
|
-
detail: `\`${names(row)}\` declares \`${THREAD_AT}\`, which is the root`,
|
|
923
|
-
instead:
|
|
924
|
-
'delete the row. The root exists by fiat and is the one thing nothing declares — giving it a parent makes its origin a thing the tree answers two ways',
|
|
925
|
-
})
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (row.at === undefined) return
|
|
929
|
-
|
|
930
|
-
if (row.at.startsWith('../') || row.at.startsWith('/')) {
|
|
931
|
-
findings.push({
|
|
932
|
-
file: at,
|
|
933
|
-
detail: `\`${names(row)}\` names \`${row.at}\`, which is outside this repository`,
|
|
934
|
-
instead:
|
|
935
|
-
'name something inside the tree. A declaration reaches only what this repository holds, and a row pointing past its edge threads a file nobody here can read, change or delete',
|
|
936
|
-
})
|
|
937
|
-
return
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const target = resolve(ROOT, row.at)
|
|
941
|
-
if (!installed(row.at) && unkept(row.at)) {
|
|
942
|
-
findings.push({
|
|
943
|
-
file: at,
|
|
944
|
-
detail: `\`${names(row)}\` names \`${row.at}\`, which git ignores, so it is on this machine and in no clone of it`,
|
|
945
|
-
instead:
|
|
946
|
-
'name a file this repository carries, or stop ignoring it. A row naming an ignored path is answered here and nowhere else, so the descent passes on the disk that wrote it and breaks everywhere the work actually runs',
|
|
947
|
-
})
|
|
948
|
-
return
|
|
949
|
-
}
|
|
950
|
-
if (!existsSync(target)) {
|
|
951
|
-
|
|
952
|
-
const derived = list === 'derives' || row.by !== undefined
|
|
953
|
-
const said = absent.get(row.at)
|
|
954
|
-
if (said !== undefined) {
|
|
955
|
-
said.by.add(names(row))
|
|
956
|
-
said.rows += 1
|
|
957
|
-
return
|
|
958
|
-
}
|
|
959
|
-
absent.set(row.at, { at: findings.length, by: new Set([names(row)]), rows: 1 })
|
|
960
|
-
findings.push({
|
|
961
|
-
file: at,
|
|
962
|
-
detail: derived
|
|
963
|
-
? `\`${names(row)}\` ${list === 'derives' ? 'derives' : 'names'} \`${row.at}\`, and nothing is there yet`
|
|
964
|
-
: `\`${names(row)}\` names \`${row.at}\`, and nothing is there`,
|
|
965
|
-
instead: derived
|
|
966
|
-
? 'run the generator the row names in `by`. A derived file is absent until it is derived, so this is a warning'
|
|
967
|
-
: 'write the file, fix the path, or delete the row. A row naming nothing brings nothing into existence',
|
|
968
|
-
...(derived ? { severity: 'warning' as const } : {}),
|
|
969
|
-
})
|
|
970
|
-
} else if (statSync(target).isDirectory()) {
|
|
971
|
-
findings.push({
|
|
972
|
-
file: at,
|
|
973
|
-
detail: `\`${names(row)}\` names \`${row.at}\`, which is a directory`,
|
|
974
|
-
instead:
|
|
975
|
-
'name the file. A directory threads nothing beneath it, so every file under this one still reads as declared by nobody while the row itself looks answered',
|
|
976
|
-
})
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
const already = shaped.get(row.at)
|
|
980
|
-
if (already === undefined) {
|
|
981
|
-
shaped.set(row.at, { type: row.type, shape: row.shape, by })
|
|
982
|
-
return
|
|
983
|
-
}
|
|
984
|
-
if (row.shape === undefined || already.shape === undefined) return
|
|
985
|
-
if (checker.isTypeAssignableTo(row.shape, already.shape) || checker.isTypeAssignableTo(already.shape, row.shape)) return
|
|
986
|
-
findings.push({
|
|
987
|
-
file: at,
|
|
988
|
-
detail: `\`${row.at}\` is declared \`${row.type}\` by \`${said(by)}\` and \`${already.type}\` by \`${said(already.by)}\`, and neither shape satisfies the other`,
|
|
989
|
-
instead: 'one address has one shape. Decide what the child is and let both rows say it',
|
|
990
|
-
})
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function into(node: ts.ObjectLiteralExpression, type: string, at: string, under: string, parent: string, judged = false): void {
|
|
994
|
-
declarations += 1
|
|
995
|
-
const held = rowOf(node, type, at)
|
|
996
|
-
if (!judged) {
|
|
997
|
-
coin(held.id, held.describes, under, parent)
|
|
998
|
-
judge(held, parent, 'declares', true)
|
|
999
|
-
if (held.id !== undefined && !stated.has(held.id)) stated.set(held.id, held)
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
const by = held.id ?? at
|
|
1003
|
-
|
|
1004
|
-
for (const list of LISTS) {
|
|
1005
|
-
const stated = held.fields.get(list)
|
|
1006
|
-
if (stated === undefined) continue
|
|
1007
|
-
|
|
1008
|
-
const only = ts.isCallExpression(stated) && stated.arguments.length === 1 ? stated.arguments[0] : undefined
|
|
1009
|
-
const value = only !== undefined && ts.isArrayLiteralExpression(only) ? only : stated
|
|
1010
|
-
|
|
1011
|
-
if (!ts.isArrayLiteralExpression(value)) {
|
|
1012
|
-
findings.push({ file: at, detail: `\`${by}\` states a \`${list}\` that is not a list`, instead: `write \`${list}\` as a list of rows` })
|
|
1013
|
-
continue
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
for (const one of value.elements) {
|
|
1017
|
-
const declaration = ts.isIdentifier(one) ? checker.getSymbolAtLocation(one)?.valueDeclaration : undefined
|
|
1018
|
-
const initial = declaration !== undefined && ts.isVariableDeclaration(declaration) ? unwrap(declaration.initializer) : undefined
|
|
1019
|
-
const object = ts.isObjectLiteralExpression(one) ? one : initial !== undefined && ts.isObjectLiteralExpression(initial) ? initial : undefined
|
|
1020
|
-
if (object === undefined) {
|
|
1021
|
-
findings.push({
|
|
1022
|
-
file: at,
|
|
1023
|
-
detail: `\`${by}\` holds something in \`${list}\` that is not a row written out here`,
|
|
1024
|
-
instead: 'write the row inline. A row reached through a name is a row the descent would have to run code to read',
|
|
1025
|
-
})
|
|
1026
|
-
continue
|
|
1027
|
-
}
|
|
1028
|
-
rows += 1
|
|
1029
|
-
const wanted = checker.getContextualType(one)
|
|
1030
|
-
const kind = wanted === undefined ? undefined : elementOf(checker, wanted)
|
|
1031
|
-
const row = rowOf(object, kind === undefined ? type : checker.typeToString(kind), at, kind)
|
|
1032
|
-
coin(row.id, row.describes, under, by)
|
|
1033
|
-
judge(row, by, list, false)
|
|
1034
|
-
if (row.at !== undefined) {
|
|
1035
|
-
threaded.set(row.at, [...(threaded.get(row.at) ?? []), { list, by, row }])
|
|
1036
|
-
if (list === 'derives' && row.id !== undefined) derived.set(row.id, row)
|
|
1037
|
-
}
|
|
1038
|
-
if (LISTS.some((name) => row.fields.has(name))) into(object, row.type, at, under, by, true)
|
|
1039
|
-
if (row.at !== undefined) open(row.at, wanted === undefined ? undefined : elementOf(checker, wanted), under, row.id ?? by, by, list !== 'derives' || row.at.startsWith(TREE))
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function open(at: string, expected: ts.Type | undefined, under: string, parent: string, by: string, reads: boolean): void {
|
|
1045
|
-
const already = opened.has(at)
|
|
1046
|
-
opened.add(at)
|
|
1047
|
-
if (!SOURCE.test(at)) return
|
|
1048
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
1049
|
-
if (file === undefined) return
|
|
1050
|
-
if (!reads) return
|
|
1051
|
-
|
|
1052
|
-
if (!already) {
|
|
1053
|
-
for (const one of program.getSyntacticDiagnostics(file)) {
|
|
1054
|
-
broken.push({
|
|
1055
|
-
file: at,
|
|
1056
|
-
detail: `this file does not parse, so whatever it declares was never read and every child under it reads as absent: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
|
|
1057
|
-
instead: 'fix the syntax. A file the descent cannot read declares nothing, and nothing below it can be found missing',
|
|
1058
|
-
})
|
|
1059
|
-
break
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
for (const one of nodesIn(at)) {
|
|
1064
|
-
if (expected !== undefined && !checker.isTypeAssignableTo(one.shape, expected)) {
|
|
1065
|
-
findings.push({
|
|
1066
|
-
file: at,
|
|
1067
|
-
detail: `\`${one.name}\` here is \`${one.type}\`, and \`${said(by)}\` states its child is \`${checker.typeToString(expected)}\``,
|
|
1068
|
-
instead: `annotate it \`${checker.typeToString(expected)}\`, or a shape that satisfies it, or change the row. A parent states the shape of its child, and a child answering with another shape is a child of something else`,
|
|
1069
|
-
})
|
|
1070
|
-
continue
|
|
1071
|
-
}
|
|
1072
|
-
if (already) continue
|
|
1073
|
-
into(one.node, one.type, at, under, parent)
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
opened.add(THREAD_AT)
|
|
1078
|
-
for (const one of nodesIn(THREAD_AT)) into(one.node, one.type, THREAD_AT, '', '')
|
|
1079
|
-
|
|
1080
|
-
for (const at of [...opened].filter((one) => SOURCE.test(one))) {
|
|
1081
|
-
const file = program.getSourceFile(resolve(ROOT, at))
|
|
1082
|
-
if (file === undefined) continue
|
|
1083
|
-
for (const one of program.getSemanticDiagnostics(file)) {
|
|
1084
|
-
findings.push({
|
|
1085
|
-
file: at,
|
|
1086
|
-
detail: `the compiler refuses this declaration: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
|
|
1087
|
-
instead:
|
|
1088
|
-
'make the declaration the shape its own type states. A row the compiler will not accept is a row nothing downstream can read',
|
|
1089
|
-
})
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
for (const [what, held] of made) {
|
|
1094
|
-
if (held.by.size === 0) {
|
|
1095
|
-
findings.push({
|
|
1096
|
-
file: held.where,
|
|
1097
|
-
detail: `\`${what}\` is derived by ${held.rows.length === 1 ? `\`${held.rows[0] ?? ''}\`` : `${String(held.rows.length)} rows`}, and not one of them states a \`by\``,
|
|
1098
|
-
instead:
|
|
1099
|
-
'name the command that writes it on one of those rows. A derived file whose generator is unnamed cannot be compared against what it should be, so drift in it is invisible',
|
|
1100
|
-
})
|
|
1101
|
-
continue
|
|
1102
|
-
}
|
|
1103
|
-
if (held.by.size < 2) continue
|
|
1104
|
-
findings.push({
|
|
1105
|
-
file: held.where,
|
|
1106
|
-
detail: `\`${what}\` is derived by ${[...held.by].map((one) => `\`${one}\``).join(' and ')}`,
|
|
1107
|
-
instead: 'one file has one generator. Two commands writing one address means whichever runs last wins and the other is overwritten in silence',
|
|
1108
|
-
})
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
const answers = (command: string): boolean =>
|
|
1112
|
-
offered.has(command) || [...offered].some((one) => command.startsWith(`${one} --`))
|
|
1113
|
-
|
|
1114
|
-
for (const one of thread) {
|
|
1115
|
-
if (answers(one.command)) continue
|
|
1116
|
-
findings.push({
|
|
1117
|
-
file: one.row.where,
|
|
1118
|
-
detail: `\`${names(one.row)}\` states \`runs: '${one.command}'\`, and no row declares a \`by\` it is a mode of`,
|
|
1119
|
-
instead:
|
|
1120
|
-
'name a command some row brings into existence with a `by`, or a `--` mode of one. A row proving a byproduct proves what another row writes, so the writer has to exist',
|
|
1121
|
-
})
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
for (const one of demanded) {
|
|
1125
|
-
if (coined.has(one.word)) continue
|
|
1126
|
-
findings.push({
|
|
1127
|
-
file: one.row.where,
|
|
1128
|
-
detail: `\`${names(one.row)}\` ${one.list} \`${one.word}\`, and nothing in this tree coins that word`,
|
|
1129
|
-
instead: `declare something with \`id: '${one.word}'\`, or fix the name. A demand naming nothing is met by nothing, and nothing tells you so`,
|
|
1130
|
-
})
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
for (const [word, held] of coined) {
|
|
1134
|
-
const meanings = [...new Set(held.said.filter((one) => one !== ''))]
|
|
1135
|
-
if (meanings.length < 2) continue
|
|
1136
|
-
findings.push({
|
|
1137
|
-
file: THREAD_AT,
|
|
1138
|
-
detail: `\`${word}\` is coined ${String(meanings.length)} times with different meanings: ${meanings.map((one) => `"${one.slice(0, 60)}${one.length > 60 ? '…' : ''}"`).join(' and ')}`,
|
|
1139
|
-
instead: 'one word means one thing. Keep the row that coins it and make the others name what they actually are, or say the same sentence in both',
|
|
1140
|
-
})
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
for (const [name, where] of shapesNamed()) {
|
|
1144
|
-
if (where.length < 2) continue
|
|
1145
|
-
findings.push({
|
|
1146
|
-
file: where[1] ?? THREAD_AT,
|
|
1147
|
-
detail: `\`${name}\` is a shape the tree threads by, and it is declared in ${String(where.length)} places: ${where.join(', ')}`,
|
|
1148
|
-
instead: 'keep one and delete the rest. Whichever the descent reads first decides what every declaration is held to, so a second one silently rewrites the rules for the whole tree',
|
|
1149
|
-
})
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
for (const one of sources()) {
|
|
1153
|
-
if (!SOURCE.test(one)) continue
|
|
1154
|
-
for (const at of mentionsIn(one)) {
|
|
1155
|
-
const held = mentioned.get(at) ?? []
|
|
1156
|
-
if (!held.includes(one)) mentioned.set(at, [...held, one])
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
for (const [path, held] of absent) {
|
|
1161
|
-
if (held.rows < 2) continue
|
|
1162
|
-
const one = findings[held.at]
|
|
1163
|
-
if (one === undefined) continue
|
|
1164
|
-
|
|
1165
|
-
findings[held.at] = {
|
|
1166
|
-
...one,
|
|
1167
|
-
detail: `${String(held.rows)} rows derive \`${path}\`, and nothing is there yet — ${[...held.by].sort().join(', ')}`,
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
|
|
1172
|
-
return passed
|
|
1173
|
-
}
|