@chenglou/freerange 0.0.1 → 0.0.3
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 +12 -0
- package/README.md +237 -67
- package/dist/fr.js +7821 -0
- package/fr.ts +0 -2
- package/package.json +6 -3
- package/src/audit.ts +2 -1
- package/src/domain/value-identity.ts +56 -0
- package/src/engine/analyze.ts +19 -12
- package/src/engine/state.ts +23 -11
- package/src/engine/transfer.ts +78 -29
- package/src/ir/instructions.ts +7 -1
- package/src/ir/program.ts +7 -0
- package/src/lower/accept.ts +6 -2
- package/src/lower/context.ts +11 -5
- package/src/lower/expression.ts +61 -33
- package/src/lower/function-unit.ts +64 -0
- package/src/lower/module.ts +20 -4
- package/src/lower/platform.ts +3 -0
- package/src/lower/program.ts +62 -34
- package/src/lower/statements.ts +3 -8
- package/src/lower/static-intrinsics.ts +9 -8
- package/src/project.ts +2 -2
- package/src/report/index.ts +2 -0
- package/src/requirements/infer.ts +76 -25
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.3 - 2026-07-28
|
|
6
|
+
|
|
7
|
+
- Analyze arrow functions and function expressions assigned directly to top-level `const` names.
|
|
8
|
+
- Preserve type narrowing through `&&` and `||` expressions.
|
|
9
|
+
- Analyze each `Math.random()` call as a fresh number from zero up to, but not including, one.
|
|
10
|
+
- Avoid quadratic work when tracking values through deep chains of same-file function calls.
|
|
11
|
+
- Consolidate analysis limits and practical refactoring examples in the README.
|
|
12
|
+
|
|
13
|
+
## 0.0.2 - 2026-07-21
|
|
14
|
+
|
|
15
|
+
- Add node support (#2)
|
|
16
|
+
|
|
5
17
|
## 0.0.1 - 2026-07-20
|
|
6
18
|
|
|
7
19
|
Initial public release of `@chenglou/freerange`.
|
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@ Freerange shows you the range of every `number` in your TypeScript codebase, let
|
|
|
7
7
|
- **Fast**. Uses a negligible fraction of TypeScript's analysis time.
|
|
8
8
|
- **Robust**. Adversarially tested by agents against thousands of edge cases.
|
|
9
9
|
|
|
10
|
-
Freerange is deliberately designed for
|
|
10
|
+
Freerange is deliberately designed to cater to a useful (and growing) subset of TypeScript, and gives concrete guidance for moving important calculations into that subset, so that your code and math can meet in the middle to unlock the most proof power without much ergonomics drawbacks. AI agents are especially well-suited to refactor such code, and we highly recommend you asking them to do so. However, if you/they do find an unsupported TS feature truly valuable, please file an issue!
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
bun install --dev @chenglou/freerange
|
|
15
|
+
bun install --dev @chenglou/freerange # npm install works too of course
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## API
|
|
@@ -44,7 +44,7 @@ function gridItemWidth(containerWidth: number) {
|
|
|
44
44
|
gridItemWidth(200)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`bun fr` outputs:
|
|
47
|
+
`bun fr` (or `npx fr`) outputs:
|
|
48
48
|
|
|
49
49
|
```zsh
|
|
50
50
|
index.ts:9:1 - error [inferred-requirement]: call to gridItemWidth violates its nonzero divisor requirement (division at index.ts:6:10)
|
|
@@ -117,45 +117,247 @@ Every plain `number` parameter, including a number field in a fixed-shape object
|
|
|
117
117
|
|
|
118
118
|
## Writing Analyzable TypeScript
|
|
119
119
|
|
|
120
|
-
Freerange
|
|
121
|
-
|
|
120
|
+
Freerange deliberately analyzes a restricted part of TypeScript. When code leaves that scope, `fr --audit` says so instead of guessing what the code does. Freerange currently supports:
|
|
121
|
+
|
|
122
|
+
- Named, synchronous top-level functions. Both `function size(...) {}` and direct `const size = (...) => ...` declarations work. Freerange follows calls between functions in the same file
|
|
122
123
|
- Numbers, booleans, strings, nullable values, plain objects, tagged unions, dense arrays, and fixed tuples
|
|
123
124
|
- `if`/`else`, ternaries, non-fallthrough `switch`, `&&`, `||`, `!`, `??`, `for`, `while`, and `for...of` loops
|
|
124
125
|
- Arithmetic, comparisons, object field and array reads, selected `Math` operations, and `Number.isInteger`, `Number.isFinite`, and `Number.isNaN`
|
|
125
126
|
|
|
126
127
|
Freerange could theoretically support a much larger subset of TS, and did before its public release. Those patterns often made numeric inference and proofs much harder and slower, however, and some questions are undecidable in general. Now that AI agents write code, we strongly recommend asking agents to refactor important calculations into shapes that Freerange analyzes well, guided by `fr --audit`. Code that is easy to analyze tends to resemble functional programming: immutable data, explicit inputs and outputs, and clean, direct control flow.
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
Use precise TypeScript types. Avoid `any`, casts, and suppression comments, and parse external data before passing it to a numeric helper. A file containing `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, or `eval` is rejected because its declared types cannot be trusted.
|
|
130
|
+
|
|
131
|
+
Before changing an input rule, decide what the application should do. If `columnCount` must be a positive integer, either require that with leading `console.assert` calls or normalize it with `Math.max(1, Math.floor(columnCount))`. Only normalize when the application wants that runtime behavior. Audit code: `[encode-input-rule]`.
|
|
132
|
+
|
|
133
|
+
### Numeric Analysis
|
|
134
|
+
|
|
135
|
+
For each `number`, Freerange remembers its lowest and highest possible values, whether it is an integer, whether it may be `NaN` or infinite, and at most one exact value that has been ruled out. It reasons about JavaScript floating-point numbers rather than ideal real numbers and does not use a general-purpose theorem prover.
|
|
136
|
+
|
|
137
|
+
#### No common-subexpression elimination
|
|
138
|
+
|
|
139
|
+
Freerange does not replace repeated calculations with one stored result, even when their source code is identical. This function checks one subtraction, then divides by a newly evaluated subtraction:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export function progressBar(value: number, start: number, end: number): number {
|
|
143
|
+
if (end - start === 0) return 0
|
|
144
|
+
return (value - start) / (end - start)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Calculate the subtraction once when the check and division must use the same result:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export function progressBar(value: number, start: number, end: number): number {
|
|
152
|
+
const span = end - start
|
|
153
|
+
if (span === 0) return 0
|
|
154
|
+
return (value - start) / span
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Freerange recognizes aliases, repeated reads of the same immutable field or array position, and the same argument passed to multiple parameters. A newly evaluated calculation or function call is a new value. Audit code: `[guard-derived-value]`.
|
|
159
|
+
|
|
160
|
+
#### Ranges use inclusive endpoints
|
|
161
|
+
|
|
162
|
+
Freerange stores every range using its lowest and highest included values. JavaScript numbers are discrete, so the neighboring representable number can often express a strict endpoint: `Math.random()` is stored as `0..0.9999999999999999` and reported as `at least 0 and less than 1`. This needs no rewrite.
|
|
163
|
+
|
|
164
|
+
#### Branches merge into one continuous range
|
|
165
|
+
|
|
166
|
+
Freerange combines both branch results into one continuous range. Here `width` becomes `240..480`, which includes `300` even though neither branch returns it:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
export function previewRatio(compact: boolean): number {
|
|
170
|
+
const width = compact ? 240 : 480
|
|
171
|
+
return 100 / (width - 300)
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Keep a calculation inside each branch when it depends on the separate alternatives:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
export function previewRatio(compact: boolean): number {
|
|
179
|
+
if (compact) return 100 / (240 - 300)
|
|
180
|
+
return 100 / (480 - 300)
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A broad but safe range may need no rewrite.
|
|
185
|
+
|
|
186
|
+
#### A number remembers at most one excluded value
|
|
187
|
+
|
|
188
|
+
After `code !== 240` and `code !== 300`, Freerange may remember only the later exclusion. When an operation depends on a particular exclusion, check the value that the operation uses:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const divisor = code - 240
|
|
192
|
+
if (divisor === 0) return 0
|
|
193
|
+
return 100 / divisor
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Freerange does not retain arbitrary sets such as "every number except 240 and 300."
|
|
197
|
+
|
|
198
|
+
#### No transitive reasoning between comparisons
|
|
199
|
+
|
|
200
|
+
Freerange does not combine `left <= middle` and `middle <= right` to prove `left <= right`:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
if (left > middle) throw new Error('out of order')
|
|
204
|
+
if (middle > right) throw new Error('out of order')
|
|
205
|
+
console.assert(left <= right) // unproven
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
When one value is built from another, keep the relationship visible in that calculation:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const gap = Math.max(0, requestedGap)
|
|
212
|
+
const left = navRight + gap
|
|
213
|
+
console.assert(navRight <= left)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
If the values arrive independently, Freerange may leave the relationship unproven.
|
|
217
|
+
|
|
218
|
+
#### No algebraic inversion of conditions
|
|
219
|
+
|
|
220
|
+
Freerange narrows the direct operands of a comparison but does not rearrange `width * 2 > 10` to derive `width > 5`. Check the value used by the later operation:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
export function previewScale(width: number): number {
|
|
224
|
+
if (width <= 5) return 1
|
|
225
|
+
return 100 / width
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### No algebraic normalization
|
|
230
|
+
|
|
231
|
+
Freerange does not rewrite expressions using associativity, commutativity, or distributivity. Those rules do not always preserve JavaScript floating-point results:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const amount = 9_007_199_254_740_992
|
|
235
|
+
const first = 3 + amount + 2 // 9007199254740998
|
|
236
|
+
const second = 1 + amount + 4 // 9007199254740996
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Choose the operation order whose floating-point behavior the application wants. For example, `frameWidth / (imageWidth / imageHeight)` introduces a ratio that can round to zero; `(frameWidth * imageHeight) / imageWidth` avoids that particular problem, although the two expressions can still round differently. Audit code: `[use-direct-operands]`.
|
|
240
|
+
|
|
241
|
+
#### Numeric truthiness is unsupported
|
|
242
|
+
|
|
243
|
+
Freerange does not guess what a number used as a condition means. Write `width === 0` instead of relying on a truthy or falsy number such as `width || 1`. Audit code: `[write-explicit-condition]`.
|
|
244
|
+
|
|
245
|
+
### Functions
|
|
246
|
+
|
|
247
|
+
Put important calculations in named synchronous functions. A React component, callback, or async function can call a plain helper:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
export function fittedImageHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {
|
|
251
|
+
return (frameWidth * Math.max(1, imageHeight)) / Math.max(1, imageWidth)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function ImageCard(props: {frameWidth: number; imageWidth: number; imageHeight: number}) {
|
|
255
|
+
const height = fittedImageHeight(props.frameWidth, props.imageWidth, props.imageHeight)
|
|
256
|
+
return <img style={{height}} />
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Literal default parameters and omitted optional parameters work in supported same-file calls. Object and calculated defaults do not. Passing more arguments than the implementation declares is unsupported.
|
|
261
|
+
|
|
262
|
+
#### A function return does not include how the value was calculated
|
|
135
263
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
264
|
+
Freerange evaluates supported same-file helpers using what the caller knows. It keeps what the helper may return, including numeric ranges and object fields, but not an equation such as "this result is exactly `end - start`":
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
function span(start: number, end: number): number {
|
|
268
|
+
return end - start
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function progressBar(value: number, start: number, end: number): number {
|
|
272
|
+
return (value - start) / span(start, end)
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
When the caller needs that exact calculation, calculate and check it in the caller, then pass the result to a helper. The same limitation applies to booleans: `true` from `isValidIndex(values, index)` does not tell the caller which checks made it true, so write those checks where they protect the array read.
|
|
277
|
+
|
|
278
|
+
#### Imported function bodies are not analyzed
|
|
279
|
+
|
|
280
|
+
Freerange does not follow imported functions. Keep a numeric rule that needs analysis in a supported helper with explicit inputs:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
export function labelWidthFromMeasurement(measuredWidth: number): number {
|
|
284
|
+
return Math.max(120, measuredWidth + 32)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
An unsupported caller can pass `measureText(text)` into this helper, allowing Freerange to verify the rule but not the imported measurement. Imported constants work when their initializer resolves to a numeric literal such as `export const GAP = 24`.
|
|
289
|
+
|
|
290
|
+
#### No higher-order function analysis
|
|
291
|
+
|
|
292
|
+
Freerange does not analyze callbacks passed to higher-order functions such as `reduce`, `map`, or `filter`. For a simple scalar aggregation, write the loop directly:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
export function totalWidth(widths: number[]): number {
|
|
296
|
+
let total = 0
|
|
297
|
+
for (let index = 0; index < widths.length; index += 1) {
|
|
298
|
+
total += widths[index]!
|
|
139
299
|
}
|
|
140
|
-
|
|
300
|
+
return total
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This is not a general replacement for `map` or `filter`: object and array writes remain unsupported, and callback arguments, effects, and result allocation may matter. Audit code: `[use-loop-for-aggregation]`.
|
|
305
|
+
|
|
306
|
+
### Objects, Arrays, and Changing State
|
|
307
|
+
|
|
308
|
+
Freerange reads plain objects, fixed tuples, dense arrays, and tagged unions declared in the project through at most eight nested levels. Give each union case a tag, use an exhaustive non-fallthrough `switch`, and keep deeply nested or unclassifiable data outside important numeric helpers.
|
|
309
|
+
|
|
310
|
+
Freerange assumes that property reads are stable and perform no work during one analyzed synchronous call. A getter or Proxy that changes its answer or performs work is outside the scope.
|
|
311
|
+
|
|
312
|
+
#### No object and array writes
|
|
313
|
+
|
|
314
|
+
Freerange allows local variables to be reassigned but does not track writes through an object or array. Return a new value when the application does not require mutation or stable object identity:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
export function moveRight(point: {x: number; y: number}, distance: number): {x: number; y: number} {
|
|
318
|
+
return {x: point.x + distance, y: point.y}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Object spread is also unsupported because JavaScript copies only an object's own enumerable properties, which may not match the fields declared by its TypeScript type. List the fields explicitly. Rebuilding an object is not equivalent to mutation when other code observes its identity or the mutation.
|
|
323
|
+
|
|
324
|
+
#### Reads of changing state are not referentially transparent
|
|
325
|
+
|
|
326
|
+
Referential transparency means that evaluating the same expression again is equivalent to reusing its previous result. Freerange does not make that assumption for a clock, viewport, scroll position, or mutable module binding. Store one read when the check and later use should observe the same value:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
export function viewportScale(): number {
|
|
330
|
+
const viewportWidth = window.innerWidth
|
|
331
|
+
if (viewportWidth === 0) return 0
|
|
332
|
+
return 100 / viewportWidth
|
|
333
|
+
}
|
|
334
|
+
```
|
|
141
335
|
|
|
142
|
-
|
|
336
|
+
Keep separate reads when two observations are intentional, such as two clock reads used to measure elapsed time.
|
|
143
337
|
|
|
144
|
-
|
|
338
|
+
#### Array reads require dense arrays and valid indexes
|
|
145
339
|
|
|
146
|
-
|
|
340
|
+
Use `values[index] ?? fallback` only when the application wants a fallback. Otherwise, prove that `index` is an integer from zero through `values.length - 1` before using `values[index]!`. A bounds check cannot detect a hole in a sparse array, so Freerange expects arrays to be dense. Audit codes: `[handle-missing-element]`, `[guard-array-index]`.
|
|
147
341
|
|
|
148
|
-
|
|
342
|
+
### Loops
|
|
149
343
|
|
|
150
|
-
|
|
344
|
+
#### Loops find stable ranges, not exact formulas
|
|
151
345
|
|
|
152
|
-
|
|
346
|
+
Freerange checks a loop until the possible values at the start of an iteration stop changing. It does not simulate the exact runtime iteration count or derive a formula for the final value:
|
|
153
347
|
|
|
154
|
-
|
|
348
|
+
```ts
|
|
349
|
+
export function fixedTotal(): number {
|
|
350
|
+
let total = 0
|
|
351
|
+
for (let index = 0; index < 3; index += 1) {
|
|
352
|
+
total += 2
|
|
353
|
+
}
|
|
354
|
+
return total
|
|
355
|
+
}
|
|
356
|
+
```
|
|
155
357
|
|
|
156
|
-
|
|
358
|
+
Freerange knows that the result is a nonnegative integer but does not derive that it is exactly `6`. Ordinary counting loops usually settle after two or three checks. If a range still changes after 16 checks, Freerange stops analyzing that path. Write a formula directly when it is the intended implementation, but do not replace repeated floating-point arithmetic with multiplication unless the different rounding behavior is acceptable.
|
|
157
359
|
|
|
158
|
-
|
|
360
|
+
Code outside this scope may make a result less precise or stop analysis. Freerange does not publish a stronger guarantee by pretending that unsupported code was understood. If an unsupported pattern is important and cannot be reasonably refactored, please file an issue.
|
|
159
361
|
|
|
160
362
|
## Recommended TypeScript Config
|
|
161
363
|
|
|
@@ -186,53 +388,17 @@ Freerange uses a few terms consistently:
|
|
|
186
388
|
- `partially supported`: Freerange can analyze some, but not all, of the function.
|
|
187
389
|
- `skipped`: some top-level statements in the modules weren't analyzed.
|
|
188
390
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
An `ensures` line assumes its `requires` and `assumes`. A requirement may be a real API rule, or it may expose a relationship Freerange cannot currently prove. An assumption may identify a real input boundary or an analysis limitation. Decide what the program should do before changing code to remove either one.
|
|
192
|
-
|
|
193
|
-
Always read the coverage line. No findings does not mean an unsupported file is safe. A derived guarantee becoming weaker, for example `at least 54` becoming `at least 0`, appears in the audit rather than the shorter findings output.
|
|
194
|
-
|
|
195
|
-
## Analysis Limits
|
|
196
|
-
|
|
197
|
-
If your production code actually needs support beyond these limits, please file an issue! We're open to relaxing the limits.
|
|
198
|
-
|
|
199
|
-
### Numbers
|
|
200
|
-
|
|
201
|
-
Freerange's numeric analysis is designed around arithmetic used in layouts and other everyday application code. For each TypeScript `number`, Freerange tracks one continuous range, whether the value is an integer, whether it may be `NaN` or infinite, and at most one exact number that a branch proved impossible. For example, after `value !== 0`, Freerange can remember that `value` cannot be `0`.
|
|
202
|
-
|
|
203
|
-
Freerange does not keep separate ranges or arbitrary sets of possible numbers. If one branch produces `1..2` and another produces `10..11`, Freerange keeps the combined range `1..11`. A later check that rules out a different exact number may replace the number remembered from an earlier check.
|
|
391
|
+
### Caller Requirements
|
|
204
392
|
|
|
205
|
-
|
|
393
|
+
Every plain `number` parameter must be finite and not `NaN`. The same rule applies to numeric fields in fixed-shape object parameters, even when the function does not read them. Numeric literal types such as `1 | 2` already satisfy the rule. Nullable numbers, arrays, tuples, and tagged unions use more specific `assumes` lines instead. A supported literal default can satisfy the requirement when a caller omits an argument.
|
|
206
394
|
|
|
207
|
-
|
|
395
|
+
Division and array reads can create additional requirements. Freerange tries to express them using the function's parameters so that supported same-file callers can prove them, pass them to their own callers, or report a definitely invalid argument. If a condition cannot be expressed that way, `fr --audit` prints a local `assumes` line instead.
|
|
208
396
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Freerange follows calls to supported functions in the same file using the ranges known at each call. Imported functions are not followed. Imported constants are followed only when they resolve to a numeric literal such as `export const GAP = 24`. Literal default parameters work in supported calls; object and calculated defaults do not. Passing more arguments than the implementation declares is also unsupported. Unknown calls, when callbacks run, caught exceptions, changes made through another reference to the same object, and most framework behavior remain outside the subset.
|
|
212
|
-
|
|
213
|
-
### Static assertions
|
|
214
|
-
|
|
215
|
-
Inside a `console.assert`, Freerange can follow common UI calculations through named values: `Math.min` and `Math.max`, addition or subtraction by a nonnegative value, the same multiplication by a nonnegative value on both sides of a comparison, the fact that `index % columnCount < columnCount` when `columnCount` is positive, and fields read from a freshly constructed record. Freerange does not chain arbitrary comparisons: `left <= middle` and `middle <= right` do not by themselves prove `left <= right`.
|
|
216
|
-
|
|
217
|
-
### Loops
|
|
218
|
-
|
|
219
|
-
Freerange analyzes a loop again until the ranges known at the start of an iteration stop changing. Freerange does not simulate every runtime iteration or try to produce a formula for the final value. Ordinary counting loops usually settle after two or three analysis passes. If the ranges still change after 16 passes, analysis stops for that path.
|
|
220
|
-
|
|
221
|
-
### Objects and arrays
|
|
222
|
-
|
|
223
|
-
Freerange follows records, tuples, arrays, and tagged unions declared in your project through at most eight nested levels. A deeper property becomes unknown. A function is unsupported when its top-level input type cannot be represented.
|
|
224
|
-
|
|
225
|
-
Property reads are assumed to be stable and side-effect-free during one analyzed function call. A getter or Proxy that changes its answer or performs work is outside the model. Property writes, including assignments that invoke setters, are unsupported. Object spread is also unsupported because JavaScript copies only an object's own enumerable properties, which may not match the fields declared by its TypeScript type.
|
|
226
|
-
|
|
227
|
-
### Caller requirements
|
|
228
|
-
|
|
229
|
-
Every plain `number` parameter must be finite and not `NaN`. The same rule applies to number fields in fixed-shape object parameters, even when the function does not read them. Numeric literal types such as `1 | 2` already satisfy the rule. Nullable numbers, arrays, tuples, and tagged unions keep their more specific `assumes` lines instead. A supported literal default is used when an argument is omitted, so the default can satisfy the requirement automatically.
|
|
230
|
-
|
|
231
|
-
Calls to supported functions in the same file either prove these requirements, pass them outward to their own callers, or report a definitely invalid argument. A successful call also proves that the same stored argument is finite afterward. Writing `console.assert(Number.isFinite(value))` at the start of the function is allowed but normally redundant; `Number.isInteger(value)` is stronger and replaces the finite requirement.
|
|
397
|
+
A caller requirement is not automatically a bug. For example, `requires: columns >= 1` means the function is safe under that condition; it does not mean Freerange found a caller passing zero. Freerange checks supported same-file calls, but it is not a repository-wide call-site verifier. Imported calls and unsupported callers may remain unchecked.
|
|
232
398
|
|
|
233
|
-
|
|
399
|
+
An `ensures` line assumes its `requires` and `assumes`. A requirement may be a real API rule, or it may expose a relationship Freerange cannot currently prove. An assumption may identify a real input boundary or an analysis limitation. Decide what the program should do before changing code to remove either one.
|
|
234
400
|
|
|
235
|
-
|
|
401
|
+
Always read the coverage line. No findings does not mean an unsupported file is safe. A derived guarantee becoming weaker, for example `at least 54` becoming `at least 0`, appears in the audit rather than the shorter findings output.
|
|
236
402
|
|
|
237
403
|
## Development
|
|
238
404
|
|
|
@@ -240,3 +406,7 @@ These limits may make a result less precise or stop analysis, but they cannot ma
|
|
|
240
406
|
bun install
|
|
241
407
|
bun run check
|
|
242
408
|
```
|
|
409
|
+
|
|
410
|
+
## Credits
|
|
411
|
+
|
|
412
|
+
[Infer](https://github.com/facebook/infer), [AlphaProof](https://deepmind.google/blog/ai-solves-imo-problems-at-silver-medal-level/)
|