Skip to content

Commit 462a06a

Browse files
authored
Add Full TypeScript Chapter (#1376)
* Add ts-morph * Add Full TS chapter * Add tests and comments * Add support for builtins and preludes * Add modules support
1 parent eea4cf4 commit 462a06a

File tree

12 files changed

+395
-13
lines changed

12 files changed

+395
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"dependencies": {
3434
"@babel/parser": "^7.19.4",
3535
"@joeychenofficial/alt-ergo-modified": "^2.4.0",
36+
"@ts-morph/bootstrap": "^0.18.0",
3637
"@types/estree": "0.0.52",
3738
"acorn": "^8.0.3",
3839
"acorn-class-fields": "^1.0.0",

src/createContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ const createContext = <T>(
430430
externalContext?: T,
431431
externalBuiltIns: CustomBuiltIns = defaultBuiltIns
432432
): Context => {
433-
if (chapter === Chapter.FULL_JS) {
433+
if (chapter === Chapter.FULL_JS || chapter === Chapter.FULL_TS) {
434434
// fullJS will include all builtins and preludes of source 4
435435
return {
436436
...createContext(
@@ -440,7 +440,7 @@ const createContext = <T>(
440440
externalContext,
441441
externalBuiltIns
442442
),
443-
chapter: Chapter.FULL_JS
443+
chapter
444444
} as Context
445445
}
446446

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export async function runFilesInContext(
329329
return resolvedErrorPromise
330330
}
331331

332-
if (context.chapter === Chapter.FULL_JS) {
332+
if (context.chapter === Chapter.FULL_JS || context.chapter === Chapter.FULL_TS) {
333333
const program = parse(code, context)
334334
if (program === null) {
335335
return resolvedErrorPromise
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`fullTS parser returns ESTree compliant program 1`] = `
4+
Node {
5+
"body": Array [
6+
Node {
7+
"declarations": Array [
8+
Node {
9+
"end": 72,
10+
"id": Node {
11+
"end": 68,
12+
"loc": SourceLocation {
13+
"end": Position {
14+
"column": 29,
15+
"line": 2,
16+
},
17+
"identifierName": "x",
18+
"source": undefined,
19+
"start": Position {
20+
"column": 12,
21+
"line": 2,
22+
},
23+
},
24+
"name": "x",
25+
"start": 51,
26+
"type": "Identifier",
27+
"typeAnnotation": Node {
28+
"end": 68,
29+
"loc": SourceLocation {
30+
"end": Position {
31+
"column": 29,
32+
"line": 2,
33+
},
34+
"identifierName": undefined,
35+
"source": undefined,
36+
"start": Position {
37+
"column": 13,
38+
"line": 2,
39+
},
40+
},
41+
"start": 52,
42+
"type": "TSTypeAnnotation",
43+
"typeAnnotation": Node {
44+
"end": 68,
45+
"loc": SourceLocation {
46+
"end": Position {
47+
"column": 29,
48+
"line": 2,
49+
},
50+
"identifierName": undefined,
51+
"source": undefined,
52+
"start": Position {
53+
"column": 15,
54+
"line": 2,
55+
},
56+
},
57+
"start": 54,
58+
"type": "TSTypeReference",
59+
"typeName": Node {
60+
"end": 68,
61+
"loc": SourceLocation {
62+
"end": Position {
63+
"column": 29,
64+
"line": 2,
65+
},
66+
"identifierName": "StringOrNumber",
67+
"source": undefined,
68+
"start": Position {
69+
"column": 15,
70+
"line": 2,
71+
},
72+
},
73+
"name": "StringOrNumber",
74+
"start": 54,
75+
"type": "Identifier",
76+
},
77+
},
78+
},
79+
},
80+
"init": Node {
81+
"end": 72,
82+
"loc": SourceLocation {
83+
"end": Position {
84+
"column": 33,
85+
"line": 2,
86+
},
87+
"identifierName": undefined,
88+
"source": undefined,
89+
"start": Position {
90+
"column": 32,
91+
"line": 2,
92+
},
93+
},
94+
"raw": "1",
95+
"start": 71,
96+
"type": "Literal",
97+
"value": 1,
98+
},
99+
"loc": SourceLocation {
100+
"end": Position {
101+
"column": 33,
102+
"line": 2,
103+
},
104+
"identifierName": undefined,
105+
"source": undefined,
106+
"start": Position {
107+
"column": 12,
108+
"line": 2,
109+
},
110+
},
111+
"start": 51,
112+
"type": "VariableDeclarator",
113+
},
114+
],
115+
"end": 73,
116+
"kind": "const",
117+
"loc": SourceLocation {
118+
"end": Position {
119+
"column": 34,
120+
"line": 2,
121+
},
122+
"identifierName": undefined,
123+
"source": undefined,
124+
"start": Position {
125+
"column": 6,
126+
"line": 2,
127+
},
128+
},
129+
"start": 45,
130+
"type": "VariableDeclaration",
131+
},
132+
],
133+
"end": 78,
134+
"interpreter": null,
135+
"loc": SourceLocation {
136+
"end": Position {
137+
"column": 4,
138+
"line": 3,
139+
},
140+
"identifierName": undefined,
141+
"source": undefined,
142+
"start": Position {
143+
"column": 0,
144+
"line": 1,
145+
},
146+
},
147+
"sourceType": "module",
148+
"start": 0,
149+
"type": "Program",
150+
}
151+
`;

src/parser/__tests__/fullTS.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { parseError } from '../..'
2+
import { mockContext } from '../../mocks/context'
3+
import { Chapter } from '../../types'
4+
import { FullTSParser } from '../fullTS'
5+
6+
const parser = new FullTSParser()
7+
let context = mockContext(Chapter.FULL_TS)
8+
9+
beforeEach(() => {
10+
context = mockContext(Chapter.FULL_TS)
11+
})
12+
13+
describe('fullTS parser', () => {
14+
it('formats errors correctly', () => {
15+
const code = `type StringOrNumber = string | number;
16+
const x: StringOrNumber = true;
17+
`
18+
19+
parser.parse(code, context)
20+
expect(parseError(context.errors)).toMatchInlineSnapshot(
21+
`"Line 2: Type \'boolean\' is not assignable to type \'StringOrNumber\'."`
22+
)
23+
})
24+
25+
it('allows usage of builtins/preludes', () => {
26+
const code = `const xs = list(1);
27+
const ys = list(1);
28+
equal(xs, ys);
29+
`
30+
31+
parser.parse(code, context)
32+
expect(parseError(context.errors)).toMatchInlineSnapshot(`""`)
33+
})
34+
35+
it('allows usage of imports/modules', () => {
36+
const code = `import { show, heart } from "rune";
37+
show(heart);
38+
`
39+
40+
parser.parse(code, context)
41+
expect(parseError(context.errors)).toMatchInlineSnapshot(`""`)
42+
})
43+
44+
it('returns ESTree compliant program', () => {
45+
const code = `type StringOrNumber = string | number;
46+
const x: StringOrNumber = 1;
47+
`
48+
49+
// Resulting program should not have node for type alias declaration
50+
const parsedProgram = parser.parse(code, context)
51+
expect(parsedProgram).toMatchSnapshot()
52+
})
53+
})

src/parser/fullTS/index.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { parse as babelParse } from '@babel/parser'
2+
import { createProjectSync, ts } from '@ts-morph/bootstrap'
3+
import { Program } from 'estree'
4+
5+
import { Context } from '../..'
6+
import * as TypedES from '../../typeChecker/tsESTree'
7+
import { removeTSNodes } from '../../typeChecker/typeErrorChecker'
8+
import { FatalSyntaxError } from '../errors'
9+
import { transformBabelASTToESTreeCompliantAST } from '../source/typed/utils'
10+
import { AcornOptions, Parser } from '../types'
11+
import { defaultBabelOptions, positionToSourceLocation } from '../utils'
12+
13+
const IMPORT_TOP_LEVEL_ERROR =
14+
'An import declaration can only be used at the top level of a namespace or module.'
15+
const START_OF_MODULE_ERROR = 'Cannot find module '
16+
17+
export class FullTSParser implements Parser<AcornOptions> {
18+
parse(
19+
programStr: string,
20+
context: Context,
21+
options?: Partial<AcornOptions>,
22+
throwOnError?: boolean
23+
): Program | null {
24+
let code = ''
25+
// Add builtins to code
26+
// Each declaration is replaced with a single constant declaration with type `any`
27+
// to reduce evaluation time
28+
for (const builtin of context.nativeStorage.builtins) {
29+
code += `const ${builtin[0]}: any = 1\n`
30+
}
31+
// Add prelude functions to code
32+
// Each declaration is replaced with a single constant declaration with type `any`
33+
// to reduce evaluation time
34+
if (context.prelude) {
35+
const preludeFns = context.prelude.split('\nfunction ').slice(1)
36+
preludeFns.forEach(fnString => {
37+
const fnName = fnString.split('(')[0]
38+
// Functions in prelude that start with $ are not added
39+
if (fnName.startsWith('$')) {
40+
return
41+
}
42+
code += `const ${fnName}: any = 1\n`
43+
})
44+
}
45+
// Get line offset
46+
const lineOffset = code.split('\n').length - 1
47+
48+
// Add program string to code string,
49+
// wrapping it in a block to allow redeclaration of variables
50+
code = code + '{' + programStr + '}'
51+
// Initialize file to analyze
52+
const project = createProjectSync({ useInMemoryFileSystem: true })
53+
const filename = 'program.ts'
54+
project.createSourceFile(filename, code)
55+
56+
// Get TS diagnostics from file, formatted as TS error string
57+
const diagnostics = ts.getPreEmitDiagnostics(project.createProgram())
58+
const formattedString = project.formatDiagnosticsWithColorAndContext(diagnostics)
59+
60+
// Reformat TS error string to Source error by getting line number using regex
61+
// This is because logic to retrieve line number is only present in
62+
// formatDiagnosticsWithColorAndContext and cannot be called directly
63+
const lineNumRegex = /(?<=\[7m)\d+/
64+
diagnostics.forEach(diagnostic => {
65+
const message = diagnostic.messageText.toString()
66+
// Ignore errors regarding imports
67+
// as TS does not have information about Source modules
68+
if (message === IMPORT_TOP_LEVEL_ERROR || message.startsWith(START_OF_MODULE_ERROR)) {
69+
return
70+
}
71+
const lineNumRegExpArr = lineNumRegex.exec(formattedString.split(message)[1])
72+
const lineNum = (lineNumRegExpArr === null ? 0 : parseInt(lineNumRegExpArr[0])) - lineOffset
73+
// Ignore any errors that occur in builtins/prelude (line number <= 0)
74+
if (lineNum <= 0) {
75+
return
76+
}
77+
const position = { line: lineNum, column: 0, offset: 0 }
78+
context.errors.push(new FatalSyntaxError(positionToSourceLocation(position), message))
79+
})
80+
81+
if (context.errors.length > 0) {
82+
return null
83+
}
84+
85+
// Parse code into Babel AST, which supports type syntax
86+
const ast = babelParse(programStr, {
87+
...defaultBabelOptions,
88+
sourceFilename: options?.sourceFile,
89+
errorRecovery: throwOnError ?? true
90+
})
91+
92+
if (ast.errors.length) {
93+
ast.errors
94+
.filter(error => error instanceof SyntaxError)
95+
.forEach(error => {
96+
context.errors.push(
97+
new FatalSyntaxError(
98+
positionToSourceLocation((error as any).loc, options?.sourceFile),
99+
error.toString()
100+
)
101+
)
102+
})
103+
104+
return null
105+
}
106+
107+
// Transform Babel AST into ESTree AST
108+
const typedProgram: TypedES.Program = ast.program as TypedES.Program
109+
const transpiledProgram: Program = removeTSNodes(typedProgram)
110+
transformBabelASTToESTreeCompliantAST(transpiledProgram)
111+
112+
return transpiledProgram
113+
}
114+
115+
validate(_ast: Program, _context: Context, _throwOnError: boolean): boolean {
116+
return true
117+
}
118+
119+
toString(): string {
120+
return 'FullTSParser'
121+
}
122+
}

src/parser/parser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Program } from 'estree'
33
import { Context } from '..'
44
import { Chapter, Variant } from '../types'
55
import { FullJSParser } from './fullJS'
6+
import { FullTSParser } from './fullTS'
67
import { SourceParser } from './source'
78
import { SourceTypedParser } from './source/typed'
89
import { AcornOptions, Parser } from './types'
@@ -18,6 +19,9 @@ export function parse<TOptions extends AcornOptions>(
1819
case Chapter.FULL_JS:
1920
parser = new FullJSParser()
2021
break
22+
case Chapter.FULL_TS:
23+
parser = new FullTSParser()
24+
break
2125
default:
2226
switch (context.variant) {
2327
case Variant.TYPED:

0 commit comments

Comments
 (0)