Skip to content

Commit 4aaaeaf

Browse files
authored
Limit type alias declarations and import statements in Source Typed to top level (#1369)
* Limit type alias declarations to top level * Fix catching of import export syntax error * Add test for type alias top level * Add test for import statements
1 parent 67fa5a8 commit 4aaaeaf

File tree

3 files changed

+77
-28
lines changed

3 files changed

+77
-28
lines changed

src/parser/source/typed/index.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,28 @@ export class SourceTypedParser extends SourceParser {
2525
options?: Partial<AcornOptions>,
2626
throwOnError?: boolean
2727
): Program | null {
28-
TypeParser.parse(
29-
programStr,
30-
createAcornParserOptions(DEFAULT_ECMA_VERSION, context.errors, options)
31-
)
28+
// Parse with acorn type parser first to catch errors such as
29+
// import/export not at top level, trailing commas, missing semicolons
30+
try {
31+
TypeParser.parse(
32+
programStr,
33+
createAcornParserOptions(DEFAULT_ECMA_VERSION, context.errors, options)
34+
)
35+
} catch (error) {
36+
if (error instanceof SyntaxError) {
37+
error = new FatalSyntaxError(
38+
positionToSourceLocation((error as any).loc, options?.sourceFile),
39+
error.toString()
40+
)
41+
}
3242

43+
if (throwOnError) throw error
44+
context.errors.push(error)
45+
return null
46+
}
47+
48+
// Parse again with babel parser to capture all type syntax
49+
// and catch remaining syntax errors not caught by acorn type parser
3350
const ast = babelParse(programStr, {
3451
...SourceTypedParser.defaultBabelOptions,
3552
sourceFilename: options?.sourceFile,

src/typeChecker/__tests__/source1Typed.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,19 @@ describe('type aliases', () => {
426426
expect(program).toMatchSnapshot() // Should not contain TSTypeAliasDeclaration node
427427
})
428428

429+
it('should only be used at top level', () => {
430+
const code = `type x = string;
431+
{
432+
type y = number;
433+
}
434+
`
435+
436+
parse(code, context)
437+
expect(parseError(context.errors)).toMatchInlineSnapshot(
438+
`"Line 3: Type alias declarations may only appear at the top level"`
439+
)
440+
})
441+
429442
it('should not be used as variables', () => {
430443
const code = `type x = string | number;
431444
x;
@@ -990,6 +1003,19 @@ describe('import statements', () => {
9901003
expect(parseError(context.errors)).toMatchInlineSnapshot(`""`)
9911004
})
9921005

1006+
it('should only be used at top level', () => {
1007+
const code = `import { show } from 'rune';
1008+
{
1009+
import { heart } from 'rune';
1010+
}
1011+
`
1012+
1013+
parse(code, context)
1014+
expect(parseError(context.errors)).toMatchInlineSnapshot(
1015+
`"Line 3: SyntaxError: 'import' and 'export' may only appear at the top level (3:8)"`
1016+
)
1017+
})
1018+
9931019
it('defaults to any for all imports', () => {
9941020
const code = `import { show, heart } from 'rune';
9951021
show(heart);

src/typeChecker/typeErrorChecker.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -571,24 +571,24 @@ function handleImportDeclarations(node: tsEs.Program) {
571571
* Type checking is not carried out as this function is only responsible for hoisting declarations.
572572
*/
573573
function addTypeDeclarationsToEnvironment(node: tsEs.Program | tsEs.BlockStatement) {
574-
node.body.forEach(node => {
575-
switch (node.type) {
574+
node.body.forEach(bodyNode => {
575+
switch (bodyNode.type) {
576576
case 'FunctionDeclaration':
577-
if (node.id === null) {
577+
if (bodyNode.id === null) {
578578
throw new Error(
579579
'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.'
580580
)
581581
}
582582
// Only identifiers/rest elements are used as function params in Source
583-
const params = node.params.filter(
583+
const params = bodyNode.params.filter(
584584
(param): param is tsEs.Identifier | tsEs.RestElement =>
585585
param.type === 'Identifier' || param.type === 'RestElement'
586586
)
587-
if (params.length !== node.params.length) {
588-
throw new TypecheckError(node, 'Unknown function parameter type')
587+
if (params.length !== bodyNode.params.length) {
588+
throw new TypecheckError(bodyNode, 'Unknown function parameter type')
589589
}
590-
const fnName = node.id.name
591-
const returnType = getTypeAnnotationType(node.returnType)
590+
const fnName = bodyNode.id.name
591+
const returnType = getTypeAnnotationType(bodyNode.returnType)
592592

593593
// If the function has variable number of arguments, set function type as any
594594
// TODO: Add support for variable number of function arguments
@@ -607,46 +607,52 @@ function addTypeDeclarationsToEnvironment(node: tsEs.Program | tsEs.BlockStateme
607607
setType(fnName, fnType, env)
608608
break
609609
case 'VariableDeclaration':
610-
if (node.kind === 'var') {
611-
throw new TypecheckError(node, 'Variable declaration using "var" is not allowed')
610+
if (bodyNode.kind === 'var') {
611+
throw new TypecheckError(bodyNode, 'Variable declaration using "var" is not allowed')
612612
}
613-
if (node.declarations.length !== 1) {
613+
if (bodyNode.declarations.length !== 1) {
614614
throw new TypecheckError(
615-
node,
615+
bodyNode,
616616
'Variable declaration should have one and only one declaration'
617617
)
618618
}
619-
if (node.declarations[0].id.type !== 'Identifier') {
620-
throw new TypecheckError(node, 'Variable declaration ID should be an identifier')
619+
if (bodyNode.declarations[0].id.type !== 'Identifier') {
620+
throw new TypecheckError(bodyNode, 'Variable declaration ID should be an identifier')
621621
}
622-
const id = node.declarations[0].id as tsEs.Identifier
622+
const id = bodyNode.declarations[0].id as tsEs.Identifier
623623
const expectedType = getTypeAnnotationType(id.typeAnnotation)
624624

625625
// Save variable type and decl kind in type env
626626
setType(id.name, expectedType, env)
627-
setDeclKind(id.name, node.kind, env)
627+
setDeclKind(id.name, bodyNode.kind, env)
628628
break
629629
case 'TSTypeAliasDeclaration':
630-
const alias = node.id.name
630+
if (node.type === 'BlockStatement') {
631+
throw new TypecheckError(
632+
bodyNode,
633+
'Type alias declarations may only appear at the top level'
634+
)
635+
}
636+
const alias = bodyNode.id.name
631637
if (Object.values(typeAnnotationKeywordToBasicTypeMap).includes(alias as TSBasicType)) {
632-
context.errors.push(new TypeAliasNameNotAllowedError(node, alias))
638+
context.errors.push(new TypeAliasNameNotAllowedError(bodyNode, alias))
633639
break
634640
}
635641
if (lookupTypeAlias(alias, env) !== undefined) {
636642
// Only happens when attempting to declare type aliases that share names with predeclared types (e.g. Pair, List)
637643
// Declaration of two type aliases with the same name will be caught as syntax error by parser
638-
context.errors.push(new DuplicateTypeAliasError(node, alias))
644+
context.errors.push(new DuplicateTypeAliasError(bodyNode, alias))
639645
break
640646
}
641647

642648
let type: BindableType = tAny
643-
if (node.typeParameters && node.typeParameters.params.length > 0) {
649+
if (bodyNode.typeParameters && bodyNode.typeParameters.params.length > 0) {
644650
const typeParams: Variable[] = []
645651
// Check validity of type parameters
646652
pushEnv(env)
647-
node.typeParameters.params.forEach(param => {
653+
bodyNode.typeParameters.params.forEach(param => {
648654
if (param.type !== 'TSTypeParameter') {
649-
throw new TypecheckError(node, 'Invalid type parameter type')
655+
throw new TypecheckError(bodyNode, 'Invalid type parameter type')
650656
}
651657
const name = param.name
652658
if (Object.values(typeAnnotationKeywordToBasicTypeMap).includes(name as TSBasicType)) {
@@ -655,10 +661,10 @@ function addTypeDeclarationsToEnvironment(node: tsEs.Program | tsEs.BlockStateme
655661
}
656662
typeParams.push(tVar(name))
657663
})
658-
type = tForAll(getTypeAnnotationType(node), typeParams)
664+
type = tForAll(getTypeAnnotationType(bodyNode), typeParams)
659665
env.pop()
660666
} else {
661-
type = getTypeAnnotationType(node)
667+
type = getTypeAnnotationType(bodyNode)
662668
}
663669
setTypeAlias(alias, type, env)
664670
break

0 commit comments

Comments
 (0)