@chenglou/freerange 0.0.1
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/fr.ts +33 -0
- package/package.json +39 -0
- package/src/analyze.ts +16 -0
- package/src/audit.ts +468 -0
- package/src/domain/number.ts +444 -0
- package/src/domain/value.ts +445 -0
- package/src/engine/analyze.ts +745 -0
- package/src/engine/outcome.ts +137 -0
- package/src/engine/state.ts +155 -0
- package/src/engine/transfer.ts +1723 -0
- package/src/index.ts +23 -0
- package/src/ir/finite-inputs.ts +51 -0
- package/src/ir/function-usage.ts +50 -0
- package/src/ir/ids.ts +10 -0
- package/src/ir/instructions.ts +164 -0
- package/src/ir/program.ts +446 -0
- package/src/lower/accept.ts +119 -0
- package/src/lower/context.ts +250 -0
- package/src/lower/expression.ts +1742 -0
- package/src/lower/literals.ts +84 -0
- package/src/lower/module.ts +748 -0
- package/src/lower/platform.ts +81 -0
- package/src/lower/program.ts +310 -0
- package/src/lower/statements.ts +553 -0
- package/src/lower/static-intrinsics.ts +87 -0
- package/src/project.ts +500 -0
- package/src/report/format-requirement.ts +110 -0
- package/src/report/index.ts +1132 -0
- package/src/requirements/infer.ts +334 -0
- package/src/requirements/model.ts +77 -0
- package/src/typescript/check.ts +56 -0
- package/src/typescript/diagnostics.ts +69 -0
- package/src/typescript/project.ts +101 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {analyzeCheckedSource} from './analyze.ts'
|
|
2
|
+
import {createFileAudit, type FileAudit} from './audit.ts'
|
|
3
|
+
import {createReport, type AnalysisReport} from './report/index.ts'
|
|
4
|
+
import {checkFile, checkSource} from './typescript/check.ts'
|
|
5
|
+
|
|
6
|
+
export function analyzeFile(file: string, baseDirectory?: string): AnalysisReport {
|
|
7
|
+
const {program, analysis} = analyzeCheckedSource(checkFile(file), baseDirectory)
|
|
8
|
+
return createReport(program, analysis)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function analyzeSource(file: string, source: string): AnalysisReport {
|
|
12
|
+
const {program, analysis} = analyzeCheckedSource(checkSource(file, source))
|
|
13
|
+
return createReport(program, analysis)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function auditSource(file: string, source: string): FileAudit {
|
|
17
|
+
return createFileAudit(analyzeCheckedSource(checkSource(file, source)))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {formatReport} from './report/index.ts'
|
|
21
|
+
export {formatFileAuditUnit, refactorGuide, refactorGuides} from './audit.ts'
|
|
22
|
+
export type {AuditCoverage, AuditReason, AuditReference, FileAudit, RefactorGuide, RefactorGuideID} from './audit.ts'
|
|
23
|
+
export type {AnalysisReport} from './report/index.ts'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {SiteID} from './ids.ts'
|
|
2
|
+
import type {DeclaredKind, FunctionIR} from './program.ts'
|
|
3
|
+
import type {NumericExpression} from '../requirements/model.ts'
|
|
4
|
+
|
|
5
|
+
export type FiniteInput = {
|
|
6
|
+
parameter: number
|
|
7
|
+
properties: string[]
|
|
8
|
+
site: SiteID
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Plain number parameters and number leaves inside fixed records share one boundary rule.
|
|
12
|
+
// Conditional values and collections keep their existing, shape-specific assumptions.
|
|
13
|
+
export function finiteInputPaths(declared: DeclaredKind): string[][] {
|
|
14
|
+
switch (declared.kind) {
|
|
15
|
+
case 'number': return declared.interval == null ? [[]] : []
|
|
16
|
+
case 'record': {
|
|
17
|
+
const paths: string[][] = []
|
|
18
|
+
for (const property of declared.properties) {
|
|
19
|
+
for (const path of finiteInputPaths(property.declared)) {
|
|
20
|
+
paths.push([property.name, ...path])
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return paths
|
|
24
|
+
}
|
|
25
|
+
case 'array':
|
|
26
|
+
case 'boolean':
|
|
27
|
+
case 'nullish':
|
|
28
|
+
case 'opaque':
|
|
29
|
+
case 'taggedUnion':
|
|
30
|
+
case 'tuple': return []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function finiteInputs(fn: FunctionIR): FiniteInput[] {
|
|
35
|
+
const inputs: FiniteInput[] = []
|
|
36
|
+
for (let parameter = 0; parameter < fn.parameters.length; parameter++) {
|
|
37
|
+
const current = fn.parameters[parameter]!
|
|
38
|
+
for (const properties of finiteInputPaths(current.type)) {
|
|
39
|
+
inputs.push({parameter, properties, site: current.site})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return inputs
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function finiteInputExpression(input: FiniteInput): NumericExpression {
|
|
46
|
+
let expression: NumericExpression = {kind: 'parameter', index: input.parameter}
|
|
47
|
+
for (const property of input.properties) {
|
|
48
|
+
expression = {kind: 'property', base: expression, name: property}
|
|
49
|
+
}
|
|
50
|
+
return expression
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {FunctionID, ModuleBindingID} from './ids.ts'
|
|
2
|
+
import type {ProgramIR} from './program.ts'
|
|
3
|
+
|
|
4
|
+
export type FunctionUsage = {
|
|
5
|
+
callees: FunctionID[]
|
|
6
|
+
moduleBindings: ModuleBindingID[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function functionUsage(program: ProgramIR): FunctionUsage[] {
|
|
10
|
+
return program.functions.map(fn => {
|
|
11
|
+
const callees = new Set<FunctionID>()
|
|
12
|
+
const moduleBindings = new Set<ModuleBindingID>()
|
|
13
|
+
if (fn.kind === 'lowered') {
|
|
14
|
+
for (const block of fn.blocks) {
|
|
15
|
+
for (const instruction of block.instructions) {
|
|
16
|
+
if (instruction.kind === 'call') callees.add(instruction.function)
|
|
17
|
+
if (instruction.kind === 'moduleRead') moduleBindings.add(instruction.binding)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {callees: [...callees], moduleBindings: [...moduleBindings]}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function transitiveModuleBindings(
|
|
26
|
+
usage: FunctionUsage[],
|
|
27
|
+
direct: ReadonlyArray<ReadonlySet<ModuleBindingID>> = usage.map(fn => new Set(fn.moduleBindings)),
|
|
28
|
+
): Set<ModuleBindingID>[] {
|
|
29
|
+
const callers: FunctionID[][] = usage.map(() => [])
|
|
30
|
+
for (let caller = 0; caller < usage.length; caller++) {
|
|
31
|
+
for (const callee of usage[caller]!.callees) callers[callee]!.push(caller)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bindings = direct.map(items => new Set(items))
|
|
35
|
+
const queue: Array<{functionID: FunctionID; binding: ModuleBindingID}> = []
|
|
36
|
+
for (let functionID = 0; functionID < bindings.length; functionID++) {
|
|
37
|
+
for (const binding of bindings[functionID]!) queue.push({functionID, binding})
|
|
38
|
+
}
|
|
39
|
+
let index = 0
|
|
40
|
+
while (index < queue.length) {
|
|
41
|
+
const {functionID, binding} = queue[index++]!
|
|
42
|
+
for (const caller of callers[functionID]!) {
|
|
43
|
+
if (!bindings[caller]!.has(binding)) {
|
|
44
|
+
bindings[caller]!.add(binding)
|
|
45
|
+
queue.push({functionID: caller, binding})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return bindings
|
|
50
|
+
}
|
package/src/ir/ids.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ValueID = number
|
|
2
|
+
export type BlockID = number
|
|
3
|
+
export type FunctionID = number
|
|
4
|
+
// Identity of one lowered operation. Dense, program-scoped, assigned in lowering order;
|
|
5
|
+
// indexes ProgramIR.sites. Requirement and outcome records reference operations by SiteID
|
|
6
|
+
// (integer equality, array indexing), never by comparing spans or message strings.
|
|
7
|
+
export type SiteID = number
|
|
8
|
+
// Index into ProgramIR.moduleBindings and SharedState.modules. Dense, assigned by the
|
|
9
|
+
// whole-file scan in declaration order.
|
|
10
|
+
export type ModuleBindingID = number
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type {BlockID, FunctionID, ModuleBindingID, SiteID, ValueID} from './ids.ts'
|
|
2
|
+
import type {UnsupportedReason} from './program.ts'
|
|
3
|
+
|
|
4
|
+
type InstructionBase = {
|
|
5
|
+
result: ValueID
|
|
6
|
+
site: SiteID
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ComparisonOperator = 'lessThan' | 'lessThanOrEqual' | 'greaterThan' | 'greaterThanOrEqual' | 'equal' | 'notEqual'
|
|
10
|
+
export type ArithmeticOperator = 'add' | 'subtract' | 'multiply' | 'divide' | 'remainder'
|
|
11
|
+
|
|
12
|
+
type ObjectPropertyIR = {
|
|
13
|
+
name: string
|
|
14
|
+
value: ValueID
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type InstructionIR =
|
|
18
|
+
| (InstructionBase & {kind: 'constant'; value: number})
|
|
19
|
+
| (InstructionBase & {kind: 'nullishConstant'; sentinel: 'null' | 'undefined'})
|
|
20
|
+
// A value the analysis carries without claims — a string literal, a template string.
|
|
21
|
+
| (InstructionBase & {kind: 'opaqueConstant'; content?: string})
|
|
22
|
+
// A boolean the analysis knows nothing about — comparing two opaque values.
|
|
23
|
+
| (InstructionBase & {kind: 'unknownBoolean'})
|
|
24
|
+
| (InstructionBase & {kind: 'arrayLiteral'; elements: ValueID[]; form: 'tuple' | 'array'})
|
|
25
|
+
| (InstructionBase & {kind: 'arrayLength'; array: ValueID})
|
|
26
|
+
// An element read. Bare reads honestly carry possible undefined regardless of the
|
|
27
|
+
// project's TypeScript options; bareUnchecked marks the case where those options hide
|
|
28
|
+
// undefined and Freerange should explain a later kind mismatch. Asserted reads create
|
|
29
|
+
// an assumption when bounds are unknown.
|
|
30
|
+
| (InstructionBase & {
|
|
31
|
+
kind: 'arrayIndex'
|
|
32
|
+
array: ValueID
|
|
33
|
+
index: ValueID
|
|
34
|
+
mode: 'bare' | 'bareUnchecked' | 'asserted'
|
|
35
|
+
})
|
|
36
|
+
// `value === null` and friends. sentinel 'nullish' is the loose form (== null, and the
|
|
37
|
+
// ?? test), which covers both sentinels; negated flips the polarity (!==, !=).
|
|
38
|
+
// route.type === 'lightbox': consumes the tagged-union value directly (the tag read
|
|
39
|
+
// never becomes a property instruction), and branch refinement keeps only the matching
|
|
40
|
+
// variants on the true side, the rest on the false side.
|
|
41
|
+
| (InstructionBase & {kind: 'tagCheck'; union: ValueID; tagValue: string | boolean; negated: boolean})
|
|
42
|
+
| (InstructionBase & {kind: 'nullishCheck'; value: ValueID; sentinel: 'null' | 'undefined' | 'nullish'; negated: boolean})
|
|
43
|
+
| (InstructionBase & {kind: 'booleanConstant'; value: boolean})
|
|
44
|
+
// Read a module binding's slot. Evaluates to the slot's current value; stops the path
|
|
45
|
+
// when the slot holds nothing usable (uninitialized, imported, or an untracked kind).
|
|
46
|
+
| (InstructionBase & {kind: 'moduleRead'; binding: ModuleBindingID})
|
|
47
|
+
// Assign a module binding's slot. A binding is one storage location, so the write
|
|
48
|
+
// replaces the slot's value. The instruction's result is the assigned value.
|
|
49
|
+
| (InstructionBase & {kind: 'moduleWrite'; binding: ModuleBindingID; value: ValueID})
|
|
50
|
+
// Emitted where the initializer skipped a top-level statement: the binding's slot resets
|
|
51
|
+
// to what its category allows (declared-kind unknown, or uninitialized for untracked
|
|
52
|
+
// bindings), so later top-level statements cannot compute from a stale pre-skip value.
|
|
53
|
+
| (InstructionBase & {kind: 'moduleHavoc'; binding: ModuleBindingID})
|
|
54
|
+
| (InstructionBase & {
|
|
55
|
+
kind: 'binary'
|
|
56
|
+
operator: ArithmeticOperator
|
|
57
|
+
left: ValueID
|
|
58
|
+
right: ValueID
|
|
59
|
+
})
|
|
60
|
+
| (InstructionBase & {kind: 'compare'; operator: ComparisonOperator; left: ValueID; right: ValueID})
|
|
61
|
+
| (InstructionBase & {kind: 'floor'; value: ValueID})
|
|
62
|
+
// A read of a platform catalog entry, e.g. document.documentElement.clientWidth. Each
|
|
63
|
+
// evaluation produces a fresh finite non-NaN value within the recorded range — platform
|
|
64
|
+
// state is mutable, so two reads are never assumed equal.
|
|
65
|
+
| (InstructionBase & {kind: 'platformValue'; lower: number; upper: number; integer: boolean})
|
|
66
|
+
| (InstructionBase & {kind: 'absolute'; value: ValueID})
|
|
67
|
+
// Math.ceil / Math.round / Math.trunc / Math.sqrt. Kept apart from 'floor', which
|
|
68
|
+
// additionally lives in the requirement expression language; these can join it when a
|
|
69
|
+
// rounded divisor shows the need.
|
|
70
|
+
| (InstructionBase & {kind: 'mathUnary'; operator: 'ceil' | 'round' | 'trunc' | 'sqrt'; value: ValueID})
|
|
71
|
+
// A string's .length is a nonnegative integer. Carrying the string lets repeated reads
|
|
72
|
+
// of the same immutable value keep their identity.
|
|
73
|
+
| (InstructionBase & {kind: 'stringLength'; value: ValueID})
|
|
74
|
+
// parseFloat / parseInt / Number(x): an honest NaN source — any number including NaN
|
|
75
|
+
// and the infinities; parseInt's result is an integer when it is a number at all.
|
|
76
|
+
// Arguments are lowered by the caller and not carried.
|
|
77
|
+
| (InstructionBase & {kind: 'parsedNumber'; integer: boolean})
|
|
78
|
+
// Number.isInteger(x) / Number.isFinite(x) / Number.isNaN(x): a boolean over one number
|
|
79
|
+
// operand, with branch refinement like nullishCheck — the true branch of isInteger knows
|
|
80
|
+
// the value is an integer (and finite, and not NaN), the false branch of isFinite prunes
|
|
81
|
+
// when the value was already provably finite, and the false branch of isNaN launders a
|
|
82
|
+
// possibly-NaN value clean.
|
|
83
|
+
| (InstructionBase & {
|
|
84
|
+
kind: 'numberCheck'
|
|
85
|
+
predicate: 'integer' | 'finite' | 'nan'
|
|
86
|
+
value: ValueID
|
|
87
|
+
// Generated entry checks establish function-boundary contracts but are not body reads.
|
|
88
|
+
purpose?: 'finiteInput'
|
|
89
|
+
})
|
|
90
|
+
// Boolean negation, from `!x` on a boolean operand.
|
|
91
|
+
| (InstructionBase & {kind: 'not'; value: ValueID})
|
|
92
|
+
// Static requirements narrow the function body and become caller preconditions.
|
|
93
|
+
// Interior assertions are observational; their indexes address FunctionIR.assertions.
|
|
94
|
+
| (InstructionBase & {kind: 'staticRequire'; value: ValueID; purpose?: 'finiteInput'})
|
|
95
|
+
| (InstructionBase & {kind: 'staticAssert'; value: ValueID; assertion: number})
|
|
96
|
+
| (InstructionBase & {kind: 'minimum' | 'maximum'; values: ValueID[]})
|
|
97
|
+
| (InstructionBase & {kind: 'call'; function: FunctionID; arguments: ValueID[]})
|
|
98
|
+
// tag is set when the literal's contextual type is a tagged union and the literal names
|
|
99
|
+
// its tag with a string literal — the engine then builds a single-variant union, so
|
|
100
|
+
// branches building different variants join per tag instead of dropping properties.
|
|
101
|
+
| (InstructionBase & {kind: 'object'; properties: ObjectPropertyIR[]; tag?: {property: string}})
|
|
102
|
+
| (InstructionBase & {kind: 'property'; object: ValueID; property: string})
|
|
103
|
+
|
|
104
|
+
// Every ValueID operand an instruction reads, enumerated next to the type so a new kind or
|
|
105
|
+
// a new operand field on an existing kind changes in the same file and the same diff view.
|
|
106
|
+
// Completeness is soundness-bearing for report assumption trimming: an unlisted operand
|
|
107
|
+
// could make the report claim that a parameter property was never read.
|
|
108
|
+
export function forEachOperand(instruction: InstructionIR, visit: (operand: ValueID) => void): void {
|
|
109
|
+
switch (instruction.kind) {
|
|
110
|
+
case 'constant':
|
|
111
|
+
case 'nullishConstant':
|
|
112
|
+
case 'opaqueConstant':
|
|
113
|
+
case 'unknownBoolean':
|
|
114
|
+
case 'parsedNumber':
|
|
115
|
+
case 'booleanConstant':
|
|
116
|
+
case 'moduleRead':
|
|
117
|
+
case 'moduleHavoc':
|
|
118
|
+
case 'platformValue':
|
|
119
|
+
return
|
|
120
|
+
case 'stringLength': visit(instruction.value); return
|
|
121
|
+
case 'moduleWrite': visit(instruction.value); return
|
|
122
|
+
case 'binary': visit(instruction.left); visit(instruction.right); return
|
|
123
|
+
case 'compare': visit(instruction.left); visit(instruction.right); return
|
|
124
|
+
case 'floor':
|
|
125
|
+
case 'absolute':
|
|
126
|
+
case 'mathUnary':
|
|
127
|
+
case 'numberCheck':
|
|
128
|
+
case 'not':
|
|
129
|
+
case 'staticRequire':
|
|
130
|
+
case 'staticAssert': visit(instruction.value); return
|
|
131
|
+
case 'nullishCheck': visit(instruction.value); return
|
|
132
|
+
case 'tagCheck': visit(instruction.union); return
|
|
133
|
+
case 'arrayLiteral': for (const element of instruction.elements) visit(element); return
|
|
134
|
+
case 'arrayLength': visit(instruction.array); return
|
|
135
|
+
case 'arrayIndex': visit(instruction.array); visit(instruction.index); return
|
|
136
|
+
case 'minimum':
|
|
137
|
+
case 'maximum': for (const id of instruction.values) visit(id); return
|
|
138
|
+
case 'call': for (const id of instruction.arguments) visit(id); return
|
|
139
|
+
case 'object': for (const property of instruction.properties) visit(property.value); return
|
|
140
|
+
case 'property': visit(instruction.object); return
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type EdgeIR = {
|
|
145
|
+
block: BlockID
|
|
146
|
+
arguments: ValueID[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type TerminatorIR =
|
|
150
|
+
| {kind: 'return'; value: ValueID | null; site: SiteID}
|
|
151
|
+
| {kind: 'jump'; target: EdgeIR; site: SiteID}
|
|
152
|
+
| {kind: 'branch'; condition: ValueID; whenTrue: EdgeIR; whenFalse: EdgeIR; site: SiteID}
|
|
153
|
+
// The evaluation must record a stop here instead of returning. Only the file-wide
|
|
154
|
+
// rejections (eval, type-check suppression) emit one today, as the terminator of the
|
|
155
|
+
// replacement initializer; ordinary functions discard their whole body when lowering
|
|
156
|
+
// stops, and the real initializer skips statements instead.
|
|
157
|
+
| {kind: 'stop'; site: SiteID; reason: UnsupportedReason}
|
|
158
|
+
// A throw statement: the path ends and contributes nothing — no return value, no stop,
|
|
159
|
+
// no successor. Sound without modeling exceptions because the subset has no catch: a
|
|
160
|
+
// thrown path cannot be observed by any analyzed continuation, in this function or any
|
|
161
|
+
// caller. The thrown expression is deliberately NOT lowered (nothing after it runs);
|
|
162
|
+
// the acceptance pre-pass still vets it for any/assertions, and the eval scan is
|
|
163
|
+
// file-wide.
|
|
164
|
+
| {kind: 'thrown'; site: SiteID}
|