@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/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}