@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.
@@ -0,0 +1,84 @@
1
+ import * as ts from 'typescript'
2
+ import type {DeclaredKind} from '../ir/program.ts'
3
+
4
+ export type ParameterDefaultLiteral =
5
+ | {kind: 'number'; value: number}
6
+ | {kind: 'boolean'; value: boolean}
7
+ | {kind: 'opaque'; content: string}
8
+ | {kind: 'nullish'; sentinel: 'null' | 'undefined'}
9
+
10
+ // Reads a numeric literal by runtime value, without trusting its TypeScript type. Parentheses
11
+ // and type assertions do not change that value, so `40 as unknown as number` still resolves
12
+ // to 40. Arithmetic, identifiers, Infinity, and NaN remain outside this literal rule.
13
+ export function numericLiteralValue(expression: ts.Expression): number | null {
14
+ const current = unwrapLiteral(expression)
15
+ if (ts.isNumericLiteral(current)) return Number(current.text)
16
+ if (ts.isPrefixUnaryExpression(current)
17
+ && (current.operator === ts.SyntaxKind.PlusToken || current.operator === ts.SyntaxKind.MinusToken)) {
18
+ const operand = unwrapLiteral(current.operand)
19
+ if (!ts.isNumericLiteral(operand)) return null
20
+ return Number(operand.text) * (current.operator === ts.SyntaxKind.MinusToken ? -1 : 1)
21
+ }
22
+ return null
23
+ }
24
+
25
+ export function parameterDefaultLiteral(
26
+ initializer: ts.Expression,
27
+ checker: ts.TypeChecker,
28
+ ): ParameterDefaultLiteral | null {
29
+ const current = unwrapLiteral(initializer)
30
+ const number = numericLiteralValue(current)
31
+ if (number != null) return Number.isFinite(number) ? {kind: 'number', value: number} : null
32
+ if (current.kind === ts.SyntaxKind.TrueKeyword || current.kind === ts.SyntaxKind.FalseKeyword) {
33
+ return {kind: 'boolean', value: current.kind === ts.SyntaxKind.TrueKeyword}
34
+ }
35
+ if (ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current)) {
36
+ return {kind: 'opaque', content: current.text}
37
+ }
38
+ if (current.kind === ts.SyntaxKind.NullKeyword) return {kind: 'nullish', sentinel: 'null'}
39
+ if (isUndefinedGlobal(current, checker)) {
40
+ return {kind: 'nullish', sentinel: 'undefined'}
41
+ }
42
+ return null
43
+ }
44
+
45
+ export function isUndefinedGlobal(expression: ts.Expression, checker: ts.TypeChecker): boolean {
46
+ if (!ts.isIdentifier(expression) || expression.text !== 'undefined') return false
47
+ const symbol = checker.getSymbolAtLocation(expression)
48
+ const global = checker.resolveName('undefined', undefined, ts.SymbolFlags.Value, false)
49
+ return symbol != null && symbol === global
50
+ }
51
+
52
+ export function parameterDefaultFits(default_: ParameterDefaultLiteral, declared: DeclaredKind): boolean {
53
+ if (declared.kind === 'nullish') {
54
+ if (default_.kind === 'nullish') {
55
+ return default_.sentinel === 'null'
56
+ ? declared.sentinels === 'null' || declared.sentinels === 'both'
57
+ : declared.sentinels === 'undefined' || declared.sentinels === 'both'
58
+ }
59
+ return parameterDefaultFits(default_, declared.inner)
60
+ }
61
+ switch (declared.kind) {
62
+ case 'number': {
63
+ if (default_.kind !== 'number') return false
64
+ return declared.interval == null
65
+ || (default_.value >= declared.interval.lower
66
+ && default_.value <= declared.interval.upper
67
+ && (!declared.interval.integer || Number.isInteger(default_.value)))
68
+ }
69
+ case 'boolean': return default_.kind === 'boolean'
70
+ case 'opaque': return default_.kind === 'opaque'
71
+ case 'record':
72
+ case 'tuple':
73
+ case 'array':
74
+ case 'taggedUnion': return false
75
+ }
76
+ }
77
+
78
+ function unwrapLiteral(expression: ts.Expression): ts.Expression {
79
+ let current = expression
80
+ while (ts.isParenthesizedExpression(current)
81
+ || ts.isAsExpression(current)
82
+ || ts.isTypeAssertionExpression(current)) current = current.expression
83
+ return current
84
+ }