@panagos/jojoscript 0.7.0

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,191 @@
1
+ import { tokenize, significantTokens } from './lexer.js'
2
+
3
+ /*
4
+ * Optional, opt-in type annotations for `fn` parameters and return values:
5
+ *
6
+ * fn add(a: number, b: number): number { return a + b }
7
+ * fn double(value: number): number = value * 2
8
+ *
9
+ * Annotations are never checked. They are stripped from the emitted
10
+ * JavaScript and re-expressed as a JSDoc block immediately above the
11
+ * function, so editors/tools that understand JSDoc (including plain
12
+ * Node/TypeScript-in-JS tooling) get parameter and return type
13
+ * information "for free" without JojoScript needing its own type checker:
14
+ *
15
+ * /**
16
+ * * @param {number} a
17
+ * * @param {number} b
18
+ * * @returns {number}
19
+ * * /
20
+ * function add(a, b) { return a + b }
21
+ *
22
+ * Functions without any annotations are left completely untouched.
23
+ */
24
+
25
+ function text(source, tokens) {
26
+ if (!tokens.length) return ''
27
+ return source.slice(tokens[0].start, tokens[tokens.length - 1].end)
28
+ }
29
+
30
+ function splitTopLevelCommas(tokens) {
31
+ const result = []
32
+ let start = 0
33
+ let depth = 0
34
+
35
+ for (let i = 0; i < tokens.length; i++) {
36
+ const value = tokens[i].value
37
+ if (value === '(' || value === '[' || value === '{') depth++
38
+ else if (value === ')' || value === ']' || value === '}') depth--
39
+ else if (value === ',' && depth === 0) {
40
+ result.push(tokens.slice(start, i))
41
+ start = i + 1
42
+ }
43
+ }
44
+
45
+ if (start < tokens.length) result.push(tokens.slice(start))
46
+ return result
47
+ }
48
+
49
+ function findTopLevel(tokens, value) {
50
+ let depth = 0
51
+ for (let i = 0; i < tokens.length; i++) {
52
+ const v = tokens[i].value
53
+ if (v === '(' || v === '[' || v === '{') depth++
54
+ else if (v === ')' || v === ']' || v === '}') depth--
55
+ else if (v === value && depth === 0) return i
56
+ }
57
+ return -1
58
+ }
59
+
60
+ function parseParameter(source, tokens) {
61
+ const colonIndex = findTopLevel(tokens, ':')
62
+
63
+ if (colonIndex === -1) {
64
+ const code = text(source, tokens).trim()
65
+ return { code, type: null }
66
+ }
67
+
68
+ const nameTokens = tokens.slice(0, colonIndex)
69
+ const afterColon = tokens.slice(colonIndex + 1)
70
+ const equalsIndex = findTopLevel(afterColon, '=')
71
+
72
+ const typeTokens = equalsIndex === -1 ? afterColon : afterColon.slice(0, equalsIndex)
73
+ const defaultTokens = equalsIndex === -1 ? [] : afterColon.slice(equalsIndex)
74
+
75
+ const name = text(source, nameTokens).trim()
76
+ const type = text(source, typeTokens).trim()
77
+ const code = defaultTokens.length
78
+ ? `${name} ${text(source, defaultTokens).trim()}`
79
+ : name
80
+
81
+ return { code, name, type: type || null }
82
+ }
83
+
84
+ function jsdocBlock(params, returnType, indent) {
85
+ const lines = ['/**']
86
+
87
+ for (const param of params) {
88
+ if (param.type) {
89
+ lines.push(` * @param {${param.type}} ${param.name}`)
90
+ }
91
+ }
92
+
93
+ if (returnType) {
94
+ lines.push(` * @returns {${returnType}}`)
95
+ }
96
+
97
+ lines.push(' */')
98
+ return lines.map(line => `${indent}${line}`).join('\n')
99
+ }
100
+
101
+ function findFnCandidates(tokens) {
102
+ const candidates = []
103
+
104
+ for (let i = 0; i < tokens.length; i++) {
105
+ if (tokens[i].type !== 'identifier' || tokens[i].value !== 'fn') continue
106
+
107
+ const prev = tokens[i - 1]
108
+ if (prev && prev.value === '.') continue
109
+
110
+ const nameToken = tokens[i + 1]
111
+ const parenToken = tokens[i + 2]
112
+ if (!nameToken || nameToken.type !== 'identifier') continue
113
+ if (!parenToken || parenToken.value !== '(') continue
114
+
115
+ candidates.push({ fnIndex: i, parenIndex: i + 2 })
116
+ }
117
+
118
+ return candidates
119
+ }
120
+
121
+ export function transformTypeAnnotations(source) {
122
+ const tokens = significantTokens(tokenize(source))
123
+ const candidates = findFnCandidates(tokens)
124
+ const replacements = []
125
+
126
+ for (const candidate of candidates) {
127
+ let depth = 0
128
+ let closeIndex = -1
129
+
130
+ for (let i = candidate.parenIndex; i < tokens.length; i++) {
131
+ const value = tokens[i].value
132
+ if (value === '(') depth++
133
+ else if (value === ')') {
134
+ depth--
135
+ if (depth === 0) { closeIndex = i; break }
136
+ }
137
+ }
138
+
139
+ if (closeIndex === -1) continue
140
+
141
+ const paramTokens = tokens.slice(candidate.parenIndex + 1, closeIndex)
142
+ const hasParamTypes = findTopLevel(paramTokens, ':') !== -1
143
+
144
+ let returnType = null
145
+ let end = tokens[closeIndex].end
146
+
147
+ const afterClose = tokens[closeIndex + 1]
148
+ if (afterClose && afterClose.value === ':') {
149
+ let j = closeIndex + 2
150
+ while (j < tokens.length && tokens[j].value !== '{' && tokens[j].value !== '=') j++
151
+ if (j < tokens.length) {
152
+ returnType = text(source, tokens.slice(closeIndex + 2, j)).trim()
153
+ end = tokens[j].start
154
+ }
155
+ }
156
+
157
+ if (!hasParamTypes && !returnType) continue
158
+
159
+ const params = splitTopLevelCommas(paramTokens)
160
+ .filter(group => group.length)
161
+ .map(group => parseParameter(source, group))
162
+
163
+ const paramList = params.map(param => param.code).join(', ')
164
+
165
+ const fnStart = tokens[candidate.fnIndex].start
166
+ const lineStart = source.lastIndexOf('\n', fnStart - 1) + 1
167
+ const indentMatch = source.slice(lineStart, fnStart).match(/^[ \t]*/)
168
+ const indent = indentMatch ? indentMatch[0] : ''
169
+
170
+ const doc = jsdocBlock(params, returnType, indent)
171
+ const headerPrefix = source.slice(lineStart, tokens[candidate.parenIndex].start + 1)
172
+ const tail = returnType ? ') ' : ')'
173
+
174
+ replacements.push({
175
+ start: lineStart,
176
+ end,
177
+ text: `${doc}\n${headerPrefix}${paramList}${tail}`
178
+ })
179
+ }
180
+
181
+ if (!replacements.length) return source
182
+
183
+ replacements.sort((a, b) => b.start - a.start)
184
+
185
+ let result = source
186
+ for (const replacement of replacements) {
187
+ result = result.slice(0, replacement.start) + replacement.text + result.slice(replacement.end)
188
+ }
189
+
190
+ return result
191
+ }