@chenglou/freerange 0.0.3 → 0.0.4
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 +7 -1
- package/README.md +3 -1
- package/dist/fr.js +356 -359
- package/package.json +1 -1
- package/src/audit.ts +0 -2
- package/src/ir/program.ts +0 -3
- package/src/lower/context.ts +3 -0
- package/src/lower/expression.ts +45 -55
- package/src/lower/module.ts +8 -5
- package/src/lower/numeric-intersection.ts +15 -0
- package/src/lower/platform.ts +25 -12
- package/src/lower/program.ts +6 -9
- package/src/lower/statements.ts +2 -10
- package/src/project.ts +1 -1
- package/src/report/index.ts +0 -2
- package/src/typescript/check.ts +2 -2
package/package.json
CHANGED
package/src/audit.ts
CHANGED
|
@@ -415,10 +415,8 @@ function guidesForUnsupportedReason(reason: UnsupportedReason): RefactorGuideID[
|
|
|
415
415
|
case 'computedPropertyName':
|
|
416
416
|
case 'objectSpread':
|
|
417
417
|
case 'asyncOrGeneratorFunction':
|
|
418
|
-
case 'typePredicate':
|
|
419
418
|
case 'protoProperty':
|
|
420
419
|
case 'enumMemberRead':
|
|
421
|
-
case 'prototypeMemberRead':
|
|
422
420
|
case 'binaryOperator':
|
|
423
421
|
case 'callWithFewerArguments':
|
|
424
422
|
case 'callWithMoreArguments':
|
package/src/ir/program.ts
CHANGED
|
@@ -97,11 +97,8 @@ export type UnsupportedReason =
|
|
|
97
97
|
| {kind: 'computedPropertyName'}
|
|
98
98
|
| {kind: 'objectSpread'}
|
|
99
99
|
| {kind: 'asyncOrGeneratorFunction'}
|
|
100
|
-
| {kind: 'typePredicate'}
|
|
101
100
|
| {kind: 'protoProperty'}
|
|
102
101
|
| {kind: 'enumMemberRead'}
|
|
103
|
-
// point.toString and friends: a prototype member the record value cannot answer.
|
|
104
|
-
| {kind: 'prototypeMemberRead'; property: string}
|
|
105
102
|
// e.g. '**', '>>', 'instanceof' in value position — the operators arithmetic and
|
|
106
103
|
// comparison lowering do not claim ('%' and '??' lower now and no longer land here)
|
|
107
104
|
| {kind: 'binaryOperator'; operator: string}
|
package/src/lower/context.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type TopLevelFunction = TopLevelFunctionUnit & {
|
|
|
31
31
|
export type FunctionContext = {
|
|
32
32
|
sourceFile: ts.SourceFile
|
|
33
33
|
checker: ts.TypeChecker
|
|
34
|
+
program: ts.Program
|
|
34
35
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>
|
|
35
36
|
moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>
|
|
36
37
|
staticAnnotations: Map<ts.CallExpression, StaticAnnotation>
|
|
@@ -60,6 +61,7 @@ export type LoopTarget = {
|
|
|
60
61
|
export function createFunctionContext(
|
|
61
62
|
sourceFile: ts.SourceFile,
|
|
62
63
|
checker: ts.TypeChecker,
|
|
64
|
+
program: ts.Program,
|
|
63
65
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
64
66
|
moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>,
|
|
65
67
|
sites: SourceSpan[],
|
|
@@ -70,6 +72,7 @@ export function createFunctionContext(
|
|
|
70
72
|
return {
|
|
71
73
|
sourceFile,
|
|
72
74
|
checker,
|
|
75
|
+
program,
|
|
73
76
|
functionsBySymbol,
|
|
74
77
|
moduleBindingsBySymbol,
|
|
75
78
|
staticAnnotations: new Map(staticAnnotations.map(annotation => [annotation.call, annotation])),
|
package/src/lower/expression.ts
CHANGED
|
@@ -2,7 +2,12 @@ import * as ts from 'typescript'
|
|
|
2
2
|
import type {ValueID} from '../ir/ids.ts'
|
|
3
3
|
import type {ComparisonOperator, InstructionIR} from '../ir/instructions.ts'
|
|
4
4
|
import type {StaticAssertionProblem} from '../ir/program.ts'
|
|
5
|
-
import {
|
|
5
|
+
import {numberConstituent} from './numeric-intersection.ts'
|
|
6
|
+
import {
|
|
7
|
+
declaredOnlyInDeclarationFiles,
|
|
8
|
+
hasDefaultLibraryDeclaration,
|
|
9
|
+
platformFact,
|
|
10
|
+
} from './platform.ts'
|
|
6
11
|
import {
|
|
7
12
|
addInstruction,
|
|
8
13
|
addInstructionAtSite,
|
|
@@ -152,7 +157,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
152
157
|
if (ts.isNumericLiteral(negated)) {
|
|
153
158
|
return addInstruction(context, current, {kind: 'constant', value: -Number(negated.text)})
|
|
154
159
|
}
|
|
155
|
-
if (
|
|
160
|
+
if (isStandardGlobal(negated, 'Infinity', context)) {
|
|
156
161
|
return addInstruction(context, current, {kind: 'constant', value: Number.NEGATIVE_INFINITY})
|
|
157
162
|
}
|
|
158
163
|
const zero = addInstruction(context, current, {kind: 'constant', value: 0})
|
|
@@ -346,7 +351,8 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
346
351
|
// `el instanceof HTMLDivElement` on a carried value: no narrowing (the analyzer does
|
|
347
352
|
// not model classes), but the check itself is an effect-free operator, so it answers
|
|
348
353
|
// unknown and both branches analyze — the function's other paths survive. The
|
|
349
|
-
// declaration-file
|
|
354
|
+
// A declaration-file constructor is opaque but safe to test; a local constructor is
|
|
355
|
+
// runtime code outside the accepted subset.
|
|
350
356
|
if (current.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword
|
|
351
357
|
&& ts.isIdentifier(current.right)
|
|
352
358
|
&& declaredOnlyInDeclarationFiles(context.checker.getSymbolAtLocation(current.right))) {
|
|
@@ -394,10 +400,11 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
394
400
|
if (staticAnnotation != null) return lowerStaticAnnotation(staticAnnotation, context)
|
|
395
401
|
if (ts.isIdentifier(current.expression)) {
|
|
396
402
|
// Global parseFloat / parseInt / Number(x): honest NaN-carrying results, like their
|
|
397
|
-
// Number.* spellings below
|
|
403
|
+
// Number.* spellings below. Standard-library identity defends against shadows and
|
|
404
|
+
// project declarations that merely reuse a built-in name.
|
|
398
405
|
const globalName = current.expression.text
|
|
399
406
|
if ((globalName === 'parseFloat' || globalName === 'parseInt' || globalName === 'Number')
|
|
400
|
-
&&
|
|
407
|
+
&& isStandardGlobal(current.expression, globalName, context)) {
|
|
401
408
|
for (const argument of current.arguments) lowerExpression(argument, context)
|
|
402
409
|
return addInstruction(context, current, {kind: 'parsedNumber', integer: globalName === 'parseInt'})
|
|
403
410
|
}
|
|
@@ -463,12 +470,14 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
463
470
|
})
|
|
464
471
|
}
|
|
465
472
|
if (ts.isPropertyAccessExpression(current.expression)) {
|
|
466
|
-
const platformCall = current.arguments.length === 0
|
|
473
|
+
const platformCall = current.arguments.length === 0
|
|
474
|
+
? platformFact(current.expression, true, context.checker, context.program)
|
|
475
|
+
: null
|
|
467
476
|
if (platformCall != null) {
|
|
468
477
|
return addInstruction(context, current, {kind: 'platformValue', ...platformCall})
|
|
469
478
|
}
|
|
470
479
|
const method = current.expression.name.text
|
|
471
|
-
const standardMath =
|
|
480
|
+
const standardMath = isStandardGlobal(current.expression.expression, 'Math', context)
|
|
472
481
|
if (standardMath && method === 'floor' && current.arguments.length === 1) {
|
|
473
482
|
requireNumberType(current.arguments[0]!, context.checker)
|
|
474
483
|
const value = lowerExpression(current.arguments[0]!, context)
|
|
@@ -493,7 +502,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
493
502
|
// Number.isInteger / Number.isFinite: predicate checks whose branches narrow — the
|
|
494
503
|
// missing halves of the bounds-check idiom (`i >= 0 && i < arr.length` proves the
|
|
495
504
|
// range; Number.isInteger(i) proves the read hits an element rather than arr[1.5]).
|
|
496
|
-
const standardNumber =
|
|
505
|
+
const standardNumber = isStandardGlobal(current.expression.expression, 'Number', context)
|
|
497
506
|
// Number.parseFloat / Number.parseInt: honest NaN sources — the result is any
|
|
498
507
|
// number including NaN, and the isFinite/isNaN/isInteger narrowing downstream is
|
|
499
508
|
// exactly what launders it. Arguments still lower (opaque strings carry).
|
|
@@ -522,7 +531,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
522
531
|
}
|
|
523
532
|
}
|
|
524
533
|
if (ts.isPropertyAccessExpression(current)) {
|
|
525
|
-
const platform = platformFact(current, false, context.checker)
|
|
534
|
+
const platform = platformFact(current, false, context.checker, context.program)
|
|
526
535
|
if (platform != null) {
|
|
527
536
|
return addInstruction(context, current, {kind: 'platformValue', ...platform})
|
|
528
537
|
}
|
|
@@ -597,15 +606,15 @@ function lowerStaticAnnotation(annotation: StaticAnnotation, context: FunctionCo
|
|
|
597
606
|
if (requirement == null) {
|
|
598
607
|
throw unsupported(condition, {
|
|
599
608
|
kind: 'staticAssertionForm',
|
|
600
|
-
problem: staticAssertionProblem(condition, context
|
|
609
|
+
problem: staticAssertionProblem(condition, context, 'callerRequirement'),
|
|
601
610
|
})
|
|
602
611
|
}
|
|
603
612
|
value = lowerWrittenRequirement(requirement, condition, context)
|
|
604
613
|
} else {
|
|
605
|
-
if (!supportedWrittenAssertion(condition, context
|
|
614
|
+
if (!supportedWrittenAssertion(condition, context)) {
|
|
606
615
|
throw unsupported(condition, {
|
|
607
616
|
kind: 'staticAssertionForm',
|
|
608
|
-
problem: staticAssertionProblem(condition, context
|
|
617
|
+
problem: staticAssertionProblem(condition, context, 'directCheck'),
|
|
609
618
|
})
|
|
610
619
|
}
|
|
611
620
|
value = lowerExpression(condition, context)
|
|
@@ -642,7 +651,7 @@ function writtenRequirement(condition: ts.Expression, context: FunctionContext):
|
|
|
642
651
|
if (ts.isCallExpression(current) && current.questionDotToken == null
|
|
643
652
|
&& current.arguments.length === 1 && ts.isPropertyAccessExpression(current.expression)
|
|
644
653
|
&& current.expression.questionDotToken == null
|
|
645
|
-
&&
|
|
654
|
+
&& isStandardGlobal(current.expression.expression, 'Number', context)
|
|
646
655
|
&& (current.expression.name.text === 'isInteger' || current.expression.name.text === 'isFinite')) {
|
|
647
656
|
const argument = current.arguments[0]!
|
|
648
657
|
const value = staticRequirementParameterPathValue(argument, context)
|
|
@@ -744,13 +753,14 @@ function lowerWrittenRequirement(
|
|
|
744
753
|
// calculation is written and checked as normal code, then the assertion names its result.
|
|
745
754
|
// This gives agents one syntax boundary instead of exposing whichever instructions happen
|
|
746
755
|
// to be removable after general expression lowering.
|
|
747
|
-
function supportedWrittenAssertion(condition: ts.Expression,
|
|
756
|
+
function supportedWrittenAssertion(condition: ts.Expression, context: FunctionContext): boolean {
|
|
748
757
|
const current = unwrapParentheses(condition)
|
|
749
758
|
if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
|
|
750
|
-
return staticAssertionNumericAtom(current.left,
|
|
759
|
+
return staticAssertionNumericAtom(current.left, context.checker)
|
|
760
|
+
&& staticAssertionNumericAtom(current.right, context.checker)
|
|
751
761
|
}
|
|
752
|
-
const operand = staticNumberCheckOperand(current,
|
|
753
|
-
return operand != null && staticAssertionNumericAtom(operand, checker)
|
|
762
|
+
const operand = staticNumberCheckOperand(current, context)
|
|
763
|
+
return operand != null && staticAssertionNumericAtom(operand, context.checker)
|
|
754
764
|
}
|
|
755
765
|
|
|
756
766
|
function staticAssertionNumericAtom(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
@@ -802,25 +812,25 @@ function staticAssertionAtom(expression: ts.Expression): boolean {
|
|
|
802
812
|
|
|
803
813
|
function staticAssertionProblem(
|
|
804
814
|
condition: ts.Expression,
|
|
805
|
-
|
|
815
|
+
context: FunctionContext,
|
|
806
816
|
fallback: 'directCheck' | 'callerRequirement',
|
|
807
817
|
): StaticAssertionProblem {
|
|
808
818
|
const current = unwrapParentheses(condition)
|
|
809
819
|
if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
|
|
810
820
|
return staticAssertionAtomProblem(current.left) ?? staticAssertionAtomProblem(current.right) ?? fallback
|
|
811
821
|
}
|
|
812
|
-
const operand = staticNumberCheckOperand(current,
|
|
822
|
+
const operand = staticNumberCheckOperand(current, context)
|
|
813
823
|
if (operand != null) return staticAssertionAtomProblem(operand) ?? fallback
|
|
814
824
|
if (ts.isCallExpression(current)) return 'functionCall'
|
|
815
825
|
return 'directCheck'
|
|
816
826
|
}
|
|
817
827
|
|
|
818
|
-
function staticNumberCheckOperand(expression: ts.Expression,
|
|
828
|
+
function staticNumberCheckOperand(expression: ts.Expression, context: FunctionContext): ts.Expression | null {
|
|
819
829
|
if (!ts.isCallExpression(expression) || expression.questionDotToken != null
|
|
820
830
|
|| expression.arguments.length !== 1 || !ts.isPropertyAccessExpression(expression.expression)
|
|
821
831
|
|| expression.expression.questionDotToken != null) return null
|
|
822
832
|
const callee = expression.expression
|
|
823
|
-
return
|
|
833
|
+
return isStandardGlobal(callee.expression, 'Number', context)
|
|
824
834
|
&& (callee.name.text === 'isInteger' || callee.name.text === 'isFinite' || callee.name.text === 'isNaN')
|
|
825
835
|
? expression.arguments[0]!
|
|
826
836
|
: null
|
|
@@ -1150,7 +1160,7 @@ export function valueKind(type: ts.Type, checker: ts.TypeChecker, depth = 0): Va
|
|
|
1150
1160
|
}
|
|
1151
1161
|
|
|
1152
1162
|
function valueKindUncached(type: ts.Type, checker: ts.TypeChecker, depth: number): ValueKindResult {
|
|
1153
|
-
if ((type
|
|
1163
|
+
if (numberConstituent(type) != null) return 'number'
|
|
1154
1164
|
if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) return 'boolean'
|
|
1155
1165
|
// Strings are carried without claims: a label or id must not reject the numeric
|
|
1156
1166
|
// contract of the function around it.
|
|
@@ -1440,22 +1450,6 @@ export function requireBooleanCondition(node: ts.Node, checker: ts.TypeChecker):
|
|
|
1440
1450
|
}
|
|
1441
1451
|
|
|
1442
1452
|
function requireAccessedPropertyKind(access: ts.PropertyAccessExpression, checker: ts.TypeChecker): void {
|
|
1443
|
-
// An optional property reads as its maybe-undefined value: declared kinds wrap it in
|
|
1444
|
-
// the undefined sentinel and object literals fill omitted ones explicitly, so a record
|
|
1445
|
-
// value always carries every property its static type declares — there is always
|
|
1446
|
-
// something honest to read.
|
|
1447
|
-
const receiverType = checker.getTypeAtLocation(access.expression)
|
|
1448
|
-
// For an optional read the receiver includes the missing sentinels; the property lives
|
|
1449
|
-
// on the non-missing part, which getNonNullableType strips to.
|
|
1450
|
-
const presentType = access.questionDotToken != null ? checker.getNonNullableType(receiverType) : receiverType
|
|
1451
|
-
const property = checker.getPropertyOfType(presentType, access.name.text)
|
|
1452
|
-
// point.toString type-checks on every object literal, but the record value carries only
|
|
1453
|
-
// its own properties — an inherited prototype member has no honest answer. The
|
|
1454
|
-
// ownership test is the .d.ts rule: a property symbol declared only in declaration
|
|
1455
|
-
// files was not written by the project, and on a project record that means prototype.
|
|
1456
|
-
if (valueKind(presentType, checker) === 'object' && property != null && declaredOnlyInDeclarationFiles(property)) {
|
|
1457
|
-
throw unsupported(access, {kind: 'prototypeMemberRead', property: access.name.text})
|
|
1458
|
-
}
|
|
1459
1453
|
const type = checker.getTypeAtLocation(access)
|
|
1460
1454
|
if (valueKind(type, checker) != null) return
|
|
1461
1455
|
throw unsupported(access, {kind: 'valueType', typeText: checker.typeToString(type)})
|
|
@@ -1466,14 +1460,12 @@ function resolvedSymbol(symbol: ts.Symbol | undefined, checker: ts.TypeChecker):
|
|
|
1466
1460
|
return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol)
|
|
1467
1461
|
}
|
|
1468
1462
|
|
|
1469
|
-
function
|
|
1470
|
-
if (!ts.isIdentifier(expression) || expression.text !==
|
|
1471
|
-
return
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'Number') return false
|
|
1476
|
-
return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
|
|
1463
|
+
function isStandardGlobal(expression: ts.Expression, name: string, context: FunctionContext): boolean {
|
|
1464
|
+
if (!ts.isIdentifier(expression) || expression.text !== name) return false
|
|
1465
|
+
return hasDefaultLibraryDeclaration(
|
|
1466
|
+
context.checker.getSymbolAtLocation(expression),
|
|
1467
|
+
context.program,
|
|
1468
|
+
)
|
|
1477
1469
|
}
|
|
1478
1470
|
|
|
1479
1471
|
function lowerElementAccess(access: ts.ElementAccessExpression, asserted: boolean, context: FunctionContext): ValueID {
|
|
@@ -1636,7 +1628,10 @@ function missingSentinelCheck(expression: ts.BinaryExpression, context: Function
|
|
|
1636
1628
|
const missing = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1637
1629
|
const members = operandType.isUnion() ? operandType.types : [operandType]
|
|
1638
1630
|
const restMatches = members.every(member =>
|
|
1639
|
-
(member.flags & missing) !== 0
|
|
1631
|
+
(member.flags & missing) !== 0
|
|
1632
|
+
|| (typeofSide.flags === ts.TypeFlags.NumberLike
|
|
1633
|
+
? valueKind(member, context.checker) === 'number'
|
|
1634
|
+
: (member.flags & typeofSide.flags) !== 0))
|
|
1640
1635
|
&& members.some(member => (member.flags & missing) === 0)
|
|
1641
1636
|
const value = lowerExpression(typeofSide.operand.expression, context)
|
|
1642
1637
|
if (restMatches) {
|
|
@@ -1670,22 +1665,17 @@ function missingSentinelCheck(expression: ts.BinaryExpression, context: Function
|
|
|
1670
1665
|
})
|
|
1671
1666
|
}
|
|
1672
1667
|
|
|
1673
|
-
function isGlobalInfinity(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
1674
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'Infinity') return false
|
|
1675
|
-
return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
1668
|
// Resolves an identifier read: a function-local binding first, then a module binding
|
|
1679
1669
|
// (which reads the binding's slot), then the global `Infinity` as an exact constant,
|
|
1680
1670
|
// else the identifier is unknown. A local or module binding named Infinity has a
|
|
1681
|
-
// different symbol and wins above;
|
|
1682
|
-
//
|
|
1671
|
+
// different symbol and wins above; only TypeScript's standard Infinity receives the
|
|
1672
|
+
// exact constant.
|
|
1683
1673
|
function identifierValue(symbol: ts.Symbol, node: ts.Identifier, context: FunctionContext): ValueID {
|
|
1684
1674
|
const local = context.bindings.get(symbol)
|
|
1685
1675
|
if (local != null) return local
|
|
1686
1676
|
const binding = context.moduleBindingsBySymbol.get(symbol)
|
|
1687
1677
|
if (binding != null) return addInstruction(context, node, {kind: 'moduleRead', binding})
|
|
1688
|
-
if (
|
|
1678
|
+
if (isStandardGlobal(node, 'Infinity', context)) {
|
|
1689
1679
|
return addInstruction(context, node, {kind: 'constant', value: Number.POSITIVE_INFINITY})
|
|
1690
1680
|
}
|
|
1691
1681
|
if (isUndefinedGlobal(node, context.checker)) {
|
package/src/lower/module.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {declaredOnlyInDeclarationFiles} from './platform.ts'
|
|
|
19
19
|
import {addInstruction, addSite, createFunctionContext, LoweringStop, restoreLowering, sealBlocks, snapshotLowering, terminate, type FunctionContext, type TopLevelFunction} from './context.ts'
|
|
20
20
|
import {lowerExpression, nonMissingUnionMembers, tagLiteralValues, taggedUnionProperty, valueKind} from './expression.ts'
|
|
21
21
|
import {directFunctionExpression} from './function-unit.ts'
|
|
22
|
+
import {numberConstituent} from './numeric-intersection.ts'
|
|
22
23
|
import {lowerStatement} from './statements.ts'
|
|
23
24
|
|
|
24
25
|
export type ModuleScan = {
|
|
@@ -197,11 +198,12 @@ function demoteModuleWritesInNode(
|
|
|
197
198
|
export function lowerModuleInitializer(
|
|
198
199
|
sourceFile: ts.SourceFile,
|
|
199
200
|
checker: ts.TypeChecker,
|
|
201
|
+
program: ts.Program,
|
|
200
202
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
201
203
|
scan: ModuleScan,
|
|
202
204
|
sites: SourceSpan[],
|
|
203
205
|
): {initializer: FunctionIR; skips: InitializerSkip[]} {
|
|
204
|
-
const context = createFunctionContext(sourceFile, checker, functionsBySymbol, scan.bindingsBySymbol, sites)
|
|
206
|
+
const context = createFunctionContext(sourceFile, checker, program, functionsBySymbol, scan.bindingsBySymbol, sites)
|
|
205
207
|
const skips: InitializerSkip[] = []
|
|
206
208
|
const statements = sourceFile.statements
|
|
207
209
|
// Each statement gets its own catch: an unsupported one is skipped — its half-lowered
|
|
@@ -682,14 +684,15 @@ function declaredKindUncached(type: ts.Type, checker: ts.TypeChecker, seen: ts.T
|
|
|
682
684
|
|
|
683
685
|
function numericLiteralInterval(type: ts.Type): DeclaredNumberInterval | 'nonFinite' | null {
|
|
684
686
|
const members = type.isUnion() ? type.types : [type]
|
|
685
|
-
if (!members.every(member =>
|
|
686
|
-
(member.flags & ts.TypeFlags.NumberLiteral) !== 0
|
|
687
|
-
&& (member.flags & ts.TypeFlags.EnumLiteral) === 0)) return null
|
|
688
687
|
let lower = Infinity
|
|
689
688
|
let upper = -Infinity
|
|
690
689
|
let integer = true
|
|
691
690
|
for (const member of members) {
|
|
692
|
-
const
|
|
691
|
+
const number = numberConstituent(member)
|
|
692
|
+
if (number == null
|
|
693
|
+
|| (number.flags & ts.TypeFlags.NumberLiteral) === 0
|
|
694
|
+
|| (number.flags & ts.TypeFlags.EnumLiteral) !== 0) return null
|
|
695
|
+
const value = (number as ts.NumberLiteralType).value
|
|
693
696
|
if (!Number.isFinite(value)) return 'nonFinite'
|
|
694
697
|
lower = Math.min(lower, value)
|
|
695
698
|
upper = Math.max(upper, value)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
// TypeScript represents numeric brands as a number intersected with object metadata.
|
|
4
|
+
// Freerange uses the number and ignores the metadata; reading that metadata still stops
|
|
5
|
+
// because the runtime value is modeled as a number, not a record.
|
|
6
|
+
export function numberConstituent(type: ts.Type): ts.Type | null {
|
|
7
|
+
if ((type.flags & ts.TypeFlags.NumberLike) !== 0) return type
|
|
8
|
+
if (!type.isIntersection()) return null
|
|
9
|
+
const numberTypes = type.types.filter(member => (member.flags & ts.TypeFlags.NumberLike) !== 0)
|
|
10
|
+
if (numberTypes.length !== 1) return null
|
|
11
|
+
for (const member of type.types) {
|
|
12
|
+
if (member !== numberTypes[0] && (member.flags & ts.TypeFlags.Object) === 0) return null
|
|
13
|
+
}
|
|
14
|
+
return numberTypes[0]!
|
|
15
|
+
}
|
package/src/lower/platform.ts
CHANGED
|
@@ -29,6 +29,7 @@ type PlatformEntry = {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const anyFinite = {lower: -Number.MAX_VALUE, upper: Number.MAX_VALUE}
|
|
32
|
+
const maximumDateTime = 8_640_000_000_000_000
|
|
32
33
|
|
|
33
34
|
const catalog: PlatformEntry[] = [
|
|
34
35
|
// Element layout sizes are nonnegative integers (the spec rounds them).
|
|
@@ -43,19 +44,23 @@ const catalog: PlatformEntry[] = [
|
|
|
43
44
|
{path: ['window', 'scrollY'], call: false, fact: {...anyFinite, integer: false}},
|
|
44
45
|
{path: ['document', 'body', 'scrollTop'], call: false, fact: {...anyFinite, integer: false}},
|
|
45
46
|
{path: ['document', 'body', 'scrollLeft'], call: false, fact: {...anyFinite, integer: false}},
|
|
46
|
-
//
|
|
47
|
-
//
|
|
47
|
+
// performance.now is monotonic, finite, nonnegative, and fractional. Date.now is an
|
|
48
|
+
// integer UTC time value and may be negative for a clock set before the Unix epoch.
|
|
48
49
|
{path: ['performance', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
|
|
49
|
-
{path: ['Date', 'now'], call: true, fact: {lower:
|
|
50
|
+
{path: ['Date', 'now'], call: true, fact: {lower: -maximumDateTime, upper: maximumDateTime, integer: true}},
|
|
50
51
|
// Math.random returns one of JavaScript's discrete floating-point numbers below 1.
|
|
51
52
|
// Writing the predecessor of 1 keeps the exact closed range without adding open bounds.
|
|
52
53
|
{path: ['Math', 'random'], call: true, fact: {lower: 0, upper: 0.9999999999999999, integer: false}},
|
|
53
54
|
]
|
|
54
55
|
|
|
55
|
-
// Matches a property chain rooted at a global
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// Matches a property chain rooted at a global from TypeScript's standard library. A local
|
|
57
|
+
// variable or a project-supplied ambient declaration with the same name never matches.
|
|
58
|
+
export function platformFact(
|
|
59
|
+
expression: ts.Expression,
|
|
60
|
+
call: boolean,
|
|
61
|
+
checker: ts.TypeChecker,
|
|
62
|
+
program: ts.Program,
|
|
63
|
+
): PlatformFact | null {
|
|
59
64
|
const parts: string[] = []
|
|
60
65
|
let current: ts.Expression = expression
|
|
61
66
|
while (ts.isPropertyAccessExpression(current)) {
|
|
@@ -69,14 +74,22 @@ export function platformFact(expression: ts.Expression, call: boolean, checker:
|
|
|
69
74
|
&& candidate.path.length === parts.length
|
|
70
75
|
&& candidate.path.every((segment, index) => segment === parts[index]))
|
|
71
76
|
if (entry == null) return null
|
|
72
|
-
if (!
|
|
77
|
+
if (!hasDefaultLibraryDeclaration(checker.getSymbolAtLocation(current), program)) return null
|
|
73
78
|
return entry.fact
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
81
|
+
// A standard-library declaration anchors the symbol to the real JavaScript or browser
|
|
82
|
+
// global. Additional project declarations may augment that symbol; a lookalike symbol
|
|
83
|
+
// declared only by the project does not receive Freerange's built-in behavior.
|
|
84
|
+
export function hasDefaultLibraryDeclaration(
|
|
85
|
+
symbol: ts.Symbol | undefined,
|
|
86
|
+
program: ts.Program,
|
|
87
|
+
): boolean {
|
|
88
|
+
const declarations = symbol?.declarations
|
|
89
|
+
if (declarations == null || declarations.length === 0) return false
|
|
90
|
+
return declarations.some(declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile()))
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
export function declaredOnlyInDeclarationFiles(symbol: ts.Symbol | undefined): boolean {
|
|
81
94
|
const declarations = symbol?.declarations
|
|
82
95
|
if (declarations == null || declarations.length === 0) return false
|
package/src/lower/program.ts
CHANGED
|
@@ -12,7 +12,8 @@ import {scanStaticAnnotations, type StaticAnnotation} from './static-intrinsics.
|
|
|
12
12
|
import {lowerStatements} from './statements.ts'
|
|
13
13
|
|
|
14
14
|
export function lowerSource(checked: CheckedSource, baseDirectory: string = process.cwd()): ProgramIR {
|
|
15
|
-
const {sourceFile,
|
|
15
|
+
const {sourceFile, program} = checked
|
|
16
|
+
const checker = program.getTypeChecker()
|
|
16
17
|
const declarations = topLevelFunctionUnits(sourceFile)
|
|
17
18
|
const staticScan = scanStaticAnnotations(sourceFile, declarations, checker)
|
|
18
19
|
const recordStaticAnnotationIssues = (sites: SourceSpan[]): ProgramIR['staticAnnotationIssues'] =>
|
|
@@ -91,7 +92,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
91
92
|
// A failed function lowering discards the half-built FunctionContext wholesale; only
|
|
92
93
|
// the name, annotation presence, offending node's site, and tagged reason survive.
|
|
93
94
|
try {
|
|
94
|
-
functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, functionsBySymbol, scan, sites))
|
|
95
|
+
functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, program, functionsBySymbol, scan, sites))
|
|
95
96
|
} catch (error) {
|
|
96
97
|
if (!(error instanceof LoweringStop)) throw error
|
|
97
98
|
sites.push(nodeSpan(sourceFile, error.node))
|
|
@@ -104,7 +105,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
104
105
|
})
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
|
-
const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, functionsBySymbol, scan, sites)
|
|
108
|
+
const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, program, functionsBySymbol, scan, sites)
|
|
108
109
|
return {
|
|
109
110
|
file: sourceFile.fileName,
|
|
110
111
|
baseDirectory,
|
|
@@ -123,6 +124,7 @@ function lowerFunction(
|
|
|
123
124
|
staticAnnotations: StaticAnnotation[],
|
|
124
125
|
sourceFile: ts.SourceFile,
|
|
125
126
|
checker: ts.TypeChecker,
|
|
127
|
+
program: ts.Program,
|
|
126
128
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
127
129
|
scan: ModuleScan,
|
|
128
130
|
sites: SourceSpan[],
|
|
@@ -148,12 +150,6 @@ function lowerFunction(
|
|
|
148
150
|
kind: unit.initializer == null ? 'functionWithoutSignature' : 'constFunctionSignature',
|
|
149
151
|
})
|
|
150
152
|
}
|
|
151
|
-
// A type predicate (`shape is Circle`, `asserts x`) is the checker taking the author's
|
|
152
|
-
// word: callers' narrowing then exposes properties the analysis cannot confirm the
|
|
153
|
-
// value carries. Rejecting the declaring function stops every caller at the call.
|
|
154
|
-
if (checker.getTypePredicateOfSignature(signature) != null) {
|
|
155
|
-
throw unsupported(declaration, {kind: 'typePredicate'})
|
|
156
|
-
}
|
|
157
153
|
const returnType = checker.getReturnTypeOfSignature(signature)
|
|
158
154
|
// `never` counts as returning nothing: the idiomatic annotation for an always-throwing
|
|
159
155
|
// helper (`function fail(code: number): never`), whose paths all end in throw — the
|
|
@@ -167,6 +163,7 @@ function lowerFunction(
|
|
|
167
163
|
const context = createFunctionContext(
|
|
168
164
|
sourceFile,
|
|
169
165
|
checker,
|
|
166
|
+
program,
|
|
170
167
|
functionsBySymbol,
|
|
171
168
|
scan.bindingsBySymbol,
|
|
172
169
|
sites,
|
package/src/lower/statements.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
type MutableBlock,
|
|
15
15
|
} from './context.ts'
|
|
16
16
|
import {taggedUnionTagRead, identifierAssignment, lowerBranchingCondition, lowerExpression, lowerStatementExpression, requireBooleanCondition, valueKind} from './expression.ts'
|
|
17
|
-
import {declaredOnlyInDeclarationFiles} from './platform.ts'
|
|
18
17
|
|
|
19
18
|
export function lowerStatements(statements: readonly ts.Statement[], context: FunctionContext): void {
|
|
20
19
|
for (const statement of statements) {
|
|
@@ -489,15 +488,8 @@ function lowerVariableDeclarationList(declarations: ts.VariableDeclarationList,
|
|
|
489
488
|
: null
|
|
490
489
|
if (property == null) throw unsupported(element, {kind: 'variableDeclarationShape'})
|
|
491
490
|
const elementType = context.checker.getTypeAtLocation(element.name)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// Optional properties destructure like any other read now that the filling
|
|
495
|
-
// invariant guarantees the record value carries them as maybe-undefined (the old
|
|
496
|
-
// gate here predated the optionals milestone). Prototype members stay out via
|
|
497
|
-
// the same ownership rule property reads use.
|
|
498
|
-
if (propertySymbol != null && declaredOnlyInDeclarationFiles(propertySymbol)) {
|
|
499
|
-
throw unsupported(element, {kind: 'prototypeMemberRead', property})
|
|
500
|
-
}
|
|
491
|
+
// Optional properties destructure like other reads because the record carries
|
|
492
|
+
// omitted properties as maybe-undefined values.
|
|
501
493
|
if (valueKind(elementType, context.checker) == null) {
|
|
502
494
|
throw unsupported(element, {kind: 'valueType', typeText: context.checker.typeToString(elementType)})
|
|
503
495
|
}
|
package/src/project.ts
CHANGED
|
@@ -461,7 +461,7 @@ function analyzeProjectSource(
|
|
|
461
461
|
): DetailedAnalysis {
|
|
462
462
|
return analyzeCheckedSource({
|
|
463
463
|
sourceFile: source.sourceFile,
|
|
464
|
-
|
|
464
|
+
program: source.project.program,
|
|
465
465
|
}, reportBaseDirectory)
|
|
466
466
|
}
|
|
467
467
|
|
package/src/report/index.ts
CHANGED
|
@@ -918,10 +918,8 @@ export function formatUnsupportedReason(reason: UnsupportedReason): string {
|
|
|
918
918
|
case 'computedPropertyName': return 'computed object property name'
|
|
919
919
|
case 'objectSpread': return 'object spread (list every field explicitly, e.g. {gain: config.gain})'
|
|
920
920
|
case 'asyncOrGeneratorFunction': return 'an async or generator function (the runtime result is a Promise or iterator, not the body\'s return value)'
|
|
921
|
-
case 'typePredicate': return 'a type predicate (the checker takes the predicate on faith; return a plain boolean and check properties where they are read)'
|
|
922
921
|
case 'protoProperty': return 'a property named __proto__ (prototype-setting syntax at runtime, not a data property)'
|
|
923
922
|
case 'enumMemberRead': return 'an enum member read (replace the enum with plain module consts, e.g. const directionUp = 1)'
|
|
924
|
-
case 'prototypeMemberRead': return `read of the inherited prototype member ${reason.property} (records carry only their own data properties)`
|
|
925
923
|
case 'binaryOperator': return reason.operator === 'in'
|
|
926
924
|
? 'the `in` operator (use a distinct string or boolean tag when property presence distinguishes union variants)'
|
|
927
925
|
: `binary operator ${reason.operator} (supported: + - * / %, comparisons, and boolean && || !)`
|
package/src/typescript/check.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {TypeScriptDiagnosticsError} from './diagnostics.ts'
|
|
|
4
4
|
|
|
5
5
|
export type CheckedSource = {
|
|
6
6
|
sourceFile: ts.SourceFile
|
|
7
|
-
|
|
7
|
+
program: ts.Program
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// When no tsconfig is in scope, single-file analysis gets the recommended authoring
|
|
@@ -52,5 +52,5 @@ function checkedSource(program: ts.Program, file: string, options: ts.CompilerOp
|
|
|
52
52
|
}
|
|
53
53
|
const sourceFile = program.getSourceFile(file)
|
|
54
54
|
if (sourceFile == null) throw new Error(`TypeScript did not load ${file}`)
|
|
55
|
-
return {sourceFile,
|
|
55
|
+
return {sourceFile, program}
|
|
56
56
|
}
|