From 397705b12bf3174d22b1ff569139fd89e3410101 Mon Sep 17 00:00:00 2001 From: tohlh Date: Sat, 5 Apr 2025 14:22:36 +0800 Subject: [PATCH 1/4] Add language options, any checker and tests --- src/createContext.ts | 13 +- src/mocks/context.ts | 5 +- src/parser/source/typed/index.ts | 176 ++++++++++++++++++++++++++++ src/repl/repl.ts | 4 +- src/repl/transpiler.ts | 4 +- src/repl/utils.ts | 14 ++- src/typeChecker/typeErrorChecker.ts | 42 ++++++- src/types.ts | 8 ++ src/utils/testing.ts | 7 +- src/vm/svmc.ts | 12 +- 10 files changed, 272 insertions(+), 13 deletions(-) diff --git a/src/createContext.ts b/src/createContext.ts index db4810f20..6365672e0 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -27,6 +27,7 @@ import { Context, CustomBuiltIns, Environment, + LanguageOptions, NativeStorage, Value, Variant @@ -149,6 +150,7 @@ const createNativeStorage = (): NativeStorage => ({ export const createEmptyContext = ( chapter: Chapter, variant: Variant = Variant.DEFAULT, + languageOptions: LanguageOptions = new Map(), externalSymbols: string[], externalContext?: T ): Context => { @@ -164,6 +166,7 @@ export const createEmptyContext = ( nativeStorage: createNativeStorage(), executionMethod: 'auto', variant, + languageOptions, moduleContexts: {}, unTypecheckedCode: [], typeEnvironment: createTypeEnvironment(chapter), @@ -841,6 +844,7 @@ const defaultBuiltIns: CustomBuiltIns = { const createContext = ( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, + languageOptions: LanguageOptions = new Map(), externalSymbols: string[] = [], externalContext?: T, externalBuiltIns: CustomBuiltIns = defaultBuiltIns @@ -851,6 +855,7 @@ const createContext = ( ...createContext( Chapter.SOURCE_4, variant, + languageOptions, externalSymbols, externalContext, externalBuiltIns @@ -858,7 +863,13 @@ const createContext = ( chapter } as Context } - const context = createEmptyContext(chapter, variant, externalSymbols, externalContext) + const context = createEmptyContext( + chapter, + variant, + languageOptions, + externalSymbols, + externalContext + ) importBuiltins(context, externalBuiltIns) importPrelude(context) diff --git a/src/mocks/context.ts b/src/mocks/context.ts index 32bfcda21..45b074747 100644 --- a/src/mocks/context.ts +++ b/src/mocks/context.ts @@ -9,9 +9,10 @@ import { Transformers } from '../cse-machine/interpreter' export function mockContext( chapter: Chapter = Chapter.SOURCE_1, - variant: Variant = Variant.DEFAULT + variant: Variant = Variant.DEFAULT, + languageOptions = new Map() ): Context { - return createContext(chapter, variant) + return createContext(chapter, variant, languageOptions) } export function mockImportDeclaration(): es.ImportDeclaration { diff --git a/src/parser/source/typed/index.ts b/src/parser/source/typed/index.ts index 5dbf5ebc9..3f8cd5f4c 100644 --- a/src/parser/source/typed/index.ts +++ b/src/parser/source/typed/index.ts @@ -67,6 +67,10 @@ export class SourceTypedParser extends SourceParser { } const typedProgram: TypedES.Program = ast.program as TypedES.Program + if (context.prelude !== programStr) { + // Check for any declaration only if the program is not the prelude + checkForAnyDeclaration(typedProgram, context) + } const typedCheckedProgram: Program = checkForTypeErrors(typedProgram, context) transformBabelASTToESTreeCompliantAST(typedCheckedProgram) @@ -77,3 +81,175 @@ export class SourceTypedParser extends SourceParser { return 'SourceTypedParser' } } + +function checkForAnyDeclaration(program: TypedES.Program, context: Context) { + function parseConfigOption(option: string | undefined) { + return option === 'true' || option === undefined + } + + const config = { + allowAnyInVariables: parseConfigOption(context.languageOptions['typedAllowAnyInVariables']), + allowAnyInParameters: parseConfigOption(context.languageOptions['typedAllowAnyInParameters']), + allowAnyInReturnType: parseConfigOption(context.languageOptions['typedAllowAnyInReturnType']), + allowAnyInTypeAnnotationParameters: parseConfigOption( + context.languageOptions['typedAllowAnyInTypeAnnotationParameters'] + ), + allowAnyInTypeAnnotationReturnType: parseConfigOption( + context.languageOptions['typedAllowAnyInTypeAnnotationReturnType'] + ) + } + + function pushAnyUsageError(message: string, node: TypedES.Node) { + if (node.loc) { + context.errors.push(new FatalSyntaxError(node.loc, message)) + } + } + + function isAnyType(node: TypedES.TSTypeAnnotation | undefined) { + return node?.typeAnnotation?.type === 'TSAnyKeyword' || node?.typeAnnotation === undefined + } + + function checkNode(node: TypedES.Node) { + switch (node.type) { + case 'VariableDeclaration': { + node.declarations.forEach(decl => { + const tsType = (decl as any).id?.typeAnnotation + if (!config.allowAnyInVariables && isAnyType(tsType)) { + pushAnyUsageError('Usage of "any" in variable declaration is not allowed.', node) + } + if (decl.init) { + // check for lambdas + checkNode(decl.init) + } + }) + break + } + case 'FunctionDeclaration': { + if (!config.allowAnyInParameters || !config.allowAnyInReturnType) { + const func = node as any + // Check parameters + func.params?.forEach((param: any) => { + if (!config.allowAnyInParameters && isAnyType(param.typeAnnotation)) { + pushAnyUsageError('Usage of "any" in function parameter is not allowed.', param) + } + }) + // Check return type + if (!config.allowAnyInReturnType && isAnyType(func.returnType)) { + pushAnyUsageError('Usage of "any" in function return type is not allowed.', node) + } + checkNode(node.body) + } + break + } + case 'ArrowFunctionExpression': { + if (!config.allowAnyInParameters || !config.allowAnyInReturnType) { + const arrow = node as any + // Check parameters + arrow.params?.forEach((param: any) => { + if (!config.allowAnyInParameters && isAnyType(param.typeAnnotation)) { + pushAnyUsageError('Usage of "any" in arrow function parameter is not allowed.', param) + } + }) + // Recursively check return type if present + if (!config.allowAnyInReturnType && isAnyType(arrow.returnType)) { + pushAnyUsageError('Usage of "any" in arrow function return type is not allowed.', arrow) + } + if ( + !config.allowAnyInReturnType && + arrow.params?.some((param: any) => isAnyType(param.typeAnnotation)) + ) { + pushAnyUsageError('Usage of "any" in arrow function return type is not allowed.', arrow) + } + checkNode(node.body) + } + break + } + case 'ReturnStatement': { + if (node.argument) { + checkNode(node.argument) + } + break + } + case 'BlockStatement': + node.body.forEach(checkNode) + break + default: + break + } + } + + function checkTSNode(node: TypedES.Node) { + if (!node) { + // Happens when there is no type annotation + // This should have been caught by checkNode function + return + } + switch (node.type) { + case 'VariableDeclaration': { + node.declarations.forEach(decl => { + const tsType = (decl as any).id?.typeAnnotation + checkTSNode(tsType) + }) + break + } + case 'TSTypeAnnotation': { + const annotation = node as TypedES.TSTypeAnnotation + // If it's a function type annotation, check params and return + if (annotation.typeAnnotation?.type === 'TSFunctionType') { + annotation.typeAnnotation.parameters?.forEach(param => { + // Recursively check nested TSTypeAnnotations in parameters + if (!config.allowAnyInTypeAnnotationParameters && isAnyType(param.typeAnnotation)) { + pushAnyUsageError( + 'Usage of "any" in type annotation\'s function parameter is not allowed.', + param + ) + } + if (param.typeAnnotation) { + checkTSNode(param.typeAnnotation) + } + }) + const returnAnno = (annotation.typeAnnotation as TypedES.TSFunctionType).typeAnnotation + if (!config.allowAnyInTypeAnnotationReturnType && isAnyType(returnAnno)) { + pushAnyUsageError( + 'Usage of "any" in type annotation\'s function return type is not allowed.', + annotation + ) + } + // Recursively check nested TSTypeAnnotations in return type + checkTSNode(returnAnno) + } + break + } + case 'FunctionDeclaration': { + // Here we also check param type annotations + return type via config + if ( + !config.allowAnyInTypeAnnotationParameters || + !config.allowAnyInTypeAnnotationReturnType + ) { + const func = node as any + // Check parameters + if (!config.allowAnyInTypeAnnotationParameters) { + func.params?.forEach((param: any) => { + checkTSNode(param.typeAnnotation) + }) + } + // Recursively check the function return type annotation + checkTSNode(func.returnType) + } + break + } + case 'BlockStatement': + node.body.forEach(checkTSNode) + break + default: + break + } + } + + if (!config.allowAnyInVariables || !config.allowAnyInParameters || !config.allowAnyInReturnType) { + program.body.forEach(checkNode) + } + if (!config.allowAnyInTypeAnnotationParameters || !config.allowAnyInTypeAnnotationReturnType) { + program.body.forEach(checkTSNode) + } +} diff --git a/src/repl/repl.ts b/src/repl/repl.ts index 0e13c5cda..34d67dcfe 100644 --- a/src/repl/repl.ts +++ b/src/repl/repl.ts @@ -12,6 +12,7 @@ import type { FileGetter } from '../modules/moduleTypes' import { chapterParser, getChapterOption, + getLanguageOption, getVariantOption, handleResult, validChapterVariant @@ -21,6 +22,7 @@ export const getReplCommand = () => new Command('run') .addOption(getChapterOption(Chapter.SOURCE_4, chapterParser)) .addOption(getVariantOption(Variant.DEFAULT, objectValues(Variant))) + .addOption(getLanguageOption()) .option('-v, --verbose', 'Enable verbose errors') .option('--modulesBackend ') .option('-r, --repl', 'Start a REPL after evaluating files') @@ -34,7 +36,7 @@ export const getReplCommand = () => const fs: typeof fslib = require('fs/promises') - const context = createContext(lang.chapter, lang.variant) + const context = createContext(lang.chapter, lang.variant, lang.languageOptions) if (modulesBackend !== undefined) { setModulesStaticURL(modulesBackend) diff --git a/src/repl/transpiler.ts b/src/repl/transpiler.ts index 0f4884d1c..bdc338b6c 100644 --- a/src/repl/transpiler.ts +++ b/src/repl/transpiler.ts @@ -12,6 +12,7 @@ import { Chapter, Variant } from '../types' import { chapterParser, getChapterOption, + getLanguageOption, getVariantOption, validateChapterAndVariantCombo } from './utils' @@ -19,6 +20,7 @@ import { export const transpilerCommand = new Command('transpiler') .addOption(getVariantOption(Variant.DEFAULT, [Variant.DEFAULT, Variant.NATIVE])) .addOption(getChapterOption(Chapter.SOURCE_4, chapterParser)) + .addOption(getLanguageOption()) .option( '-p, --pretranspile', "only pretranspile (e.g. GPU -> Source) and don't perform Source -> JS transpilation" @@ -32,7 +34,7 @@ export const transpilerCommand = new Command('transpiler') } const fs: typeof fslib = require('fs/promises') - const context = createContext(opts.chapter, opts.variant) + const context = createContext(opts.chapter, opts.variant, opts.languageOptions) const entrypointFilePath = resolve(fileName) const linkerResult = await parseProgramsAndConstructImportGraph( diff --git a/src/repl/utils.ts b/src/repl/utils.ts index aed3ab3e3..ff1404c7c 100644 --- a/src/repl/utils.ts +++ b/src/repl/utils.ts @@ -1,7 +1,7 @@ import { Option } from '@commander-js/extra-typings' import { pyLanguages, scmLanguages, sourceLanguages } from '../constants' -import { Chapter, type Language, Variant, type Result } from '../types' +import { Chapter, type Language, Variant, type Result, LanguageOptions } from '../types' import { stringify } from '../utils/stringify' import Closure from '../cse-machine/closure' import { parseError, type Context } from '..' @@ -38,6 +38,18 @@ export const getVariantOption = (defaultValue: T, choices: T[ return new Option('--variant ').default(defaultValue).choices(choices) } +export const getLanguageOption = () => { + return new Option('--languageOptions ') + .default(new Map()) + .argParser((value: string): LanguageOptions => { + const languageOptions = value.split(',').map(lang => { + const [key, value] = lang.split('=') + return { [key]: value } + }) + return Object.assign({}, ...languageOptions) + }) +} + export function validateChapterAndVariantCombo(language: Language) { for (const { chapter, variant } of sourceLanguages) { if (language.chapter === chapter && language.variant === variant) return true diff --git a/src/typeChecker/typeErrorChecker.ts b/src/typeChecker/typeErrorChecker.ts index 547199412..9e16269b6 100644 --- a/src/typeChecker/typeErrorChecker.ts +++ b/src/typeChecker/typeErrorChecker.ts @@ -393,9 +393,45 @@ function typeCheckAndReturnType(node: tsEs.Node): Type { } // Due to the use of generics, pair, list and stream functions are handled separately - const pairFunctions = ['pair'] - const listFunctions = ['list', 'map', 'filter', 'accumulate', 'reverse'] - const streamFunctions = ['stream_map', 'stream_reverse'] + const pairFunctions = ['pair', 'is_pair', 'head', 'tail', 'is_null', 'set_head', 'set_tail'] + const listFunctions = [ + 'list', + 'equal', + 'length', + 'map', + 'build_list', + 'for_each', + 'list_to_string', + 'append', + 'member', + 'remove', + 'remove_all', + 'filter', + 'enum_list', + 'list_ref', + 'accumulate', + 'reverse' + ] + const streamFunctions = [ + 'stream_tail', + 'is_stream', + 'list_to_stream', + 'stream_to_list', + 'stream_length', + 'stream_map', + 'build_stream', + 'stream_for_each', + 'stream_reverse', + 'stream_append', + 'stream_member', + 'stream_remove', + 'stream_remove_all', + 'stream_filter', + 'enum_stream', + 'integers_from', + 'eval_stream', + 'stream_ref' + ] if ( pairFunctions.includes(fnName) || listFunctions.includes(fnName) || diff --git a/src/types.ts b/src/types.ts index 58c2eb2b5..0022656a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,9 +98,12 @@ export enum Variant { EXPLICIT_CONTROL = 'explicit-control' } +export type LanguageOptions = Map + export interface Language { chapter: Chapter variant: Variant + languageOptions?: LanguageOptions } export type ValueWrapper = LetWrapper | ConstWrapper @@ -195,6 +198,11 @@ export interface Context { */ variant: Variant + /** + * Describes the custom language option to be used for evaluation + */ + languageOptions: LanguageOptions + /** * Contains the evaluated code that has not yet been typechecked. */ diff --git a/src/utils/testing.ts b/src/utils/testing.ts index f26de4bdc..8a8626b80 100644 --- a/src/utils/testing.ts +++ b/src/utils/testing.ts @@ -13,7 +13,8 @@ import { SourceError, Value, Variant, - type Finished + type Finished, + LanguageOptions } from '../types' import { stringify } from './stringify' @@ -62,18 +63,20 @@ export function createTestContext({ context, chapter = Chapter.SOURCE_1, variant = Variant.DEFAULT, + languageOptions = new Map(), testBuiltins = {} }: { context?: TestContext chapter?: Chapter variant?: Variant + languageOptions?: LanguageOptions testBuiltins?: TestBuiltins } = {}): TestContext { if (context !== undefined) { return context } else { const testContext: TestContext = { - ...createContext(chapter, variant, [], undefined, { + ...createContext(chapter, variant, languageOptions, [], undefined, { rawDisplay: (str1, str2, _externalContext) => { testContext.displayResult.push((str2 === undefined ? '' : str2 + ' ') + str1) return str1 diff --git a/src/vm/svmc.ts b/src/vm/svmc.ts index 0fe160ce1..be65a7717 100644 --- a/src/vm/svmc.ts +++ b/src/vm/svmc.ts @@ -4,7 +4,7 @@ import * as util from 'util' import { createEmptyContext } from '../createContext' import { parse } from '../parser/parser' import { INTERNAL_FUNCTIONS as concurrentInternalFunctions } from '../stdlib/vm.prelude' -import { Chapter, Variant } from '../types' +import { Chapter, LanguageOptions, Variant } from '../types' import { assemble } from './svml-assembler' import { compileToIns } from './svml-compiler' import { stringifyProgram } from './util' @@ -13,6 +13,7 @@ interface CliOptions { compileTo: 'debug' | 'json' | 'binary' | 'ast' sourceChapter: Chapter.SOURCE_1 | Chapter.SOURCE_2 | Chapter.SOURCE_3 sourceVariant: Variant.DEFAULT | Variant.CONCURRENT // does not support other variants + sourceLanguageOptions: LanguageOptions inputFilename: string outputFilename: string | null vmInternalFunctions: string[] | null @@ -29,6 +30,7 @@ function parseOptions(): CliOptions | null { compileTo: 'binary', sourceChapter: Chapter.SOURCE_3, sourceVariant: Variant.DEFAULT, + sourceLanguageOptions: new Map(), inputFilename: '', outputFilename: null, vmInternalFunctions: null @@ -156,7 +158,13 @@ Options: } const source = await readFileAsync(options.inputFilename, 'utf8') - const context = createEmptyContext(options.sourceChapter, options.sourceVariant, [], null) + const context = createEmptyContext( + options.sourceChapter, + options.sourceVariant, + options.sourceLanguageOptions, + [], + null + ) const program = parse(source, context) let numWarnings = 0 From ac835c62f7660bfc81dd64a485d714c6b283a98b Mon Sep 17 00:00:00 2001 From: tohlh Date: Sun, 6 Apr 2025 22:53:06 +0800 Subject: [PATCH 2/4] Use Record instead of Map --- src/createContext.ts | 4 ++-- src/mocks/context.ts | 2 +- src/repl/utils.ts | 2 +- src/types.ts | 2 +- src/utils/testing.ts | 2 +- src/vm/svmc.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/createContext.ts b/src/createContext.ts index 6365672e0..7a3a498aa 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -150,7 +150,7 @@ const createNativeStorage = (): NativeStorage => ({ export const createEmptyContext = ( chapter: Chapter, variant: Variant = Variant.DEFAULT, - languageOptions: LanguageOptions = new Map(), + languageOptions: LanguageOptions = {}, externalSymbols: string[], externalContext?: T ): Context => { @@ -844,7 +844,7 @@ const defaultBuiltIns: CustomBuiltIns = { const createContext = ( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, - languageOptions: LanguageOptions = new Map(), + languageOptions: LanguageOptions = {}, externalSymbols: string[] = [], externalContext?: T, externalBuiltIns: CustomBuiltIns = defaultBuiltIns diff --git a/src/mocks/context.ts b/src/mocks/context.ts index 45b074747..395e6d049 100644 --- a/src/mocks/context.ts +++ b/src/mocks/context.ts @@ -10,7 +10,7 @@ import { Transformers } from '../cse-machine/interpreter' export function mockContext( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, - languageOptions = new Map() + languageOptions = {} ): Context { return createContext(chapter, variant, languageOptions) } diff --git a/src/repl/utils.ts b/src/repl/utils.ts index ff1404c7c..e0783c2e0 100644 --- a/src/repl/utils.ts +++ b/src/repl/utils.ts @@ -40,7 +40,7 @@ export const getVariantOption = (defaultValue: T, choices: T[ export const getLanguageOption = () => { return new Option('--languageOptions ') - .default(new Map()) + .default({}) .argParser((value: string): LanguageOptions => { const languageOptions = value.split(',').map(lang => { const [key, value] = lang.split('=') diff --git a/src/types.ts b/src/types.ts index 0022656a0..be608c1a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,7 +98,7 @@ export enum Variant { EXPLICIT_CONTROL = 'explicit-control' } -export type LanguageOptions = Map +export type LanguageOptions = Record export interface Language { chapter: Chapter diff --git a/src/utils/testing.ts b/src/utils/testing.ts index 8a8626b80..ef3d7a860 100644 --- a/src/utils/testing.ts +++ b/src/utils/testing.ts @@ -63,7 +63,7 @@ export function createTestContext({ context, chapter = Chapter.SOURCE_1, variant = Variant.DEFAULT, - languageOptions = new Map(), + languageOptions = {}, testBuiltins = {} }: { context?: TestContext diff --git a/src/vm/svmc.ts b/src/vm/svmc.ts index be65a7717..d4ac6aa6c 100644 --- a/src/vm/svmc.ts +++ b/src/vm/svmc.ts @@ -30,7 +30,7 @@ function parseOptions(): CliOptions | null { compileTo: 'binary', sourceChapter: Chapter.SOURCE_3, sourceVariant: Variant.DEFAULT, - sourceLanguageOptions: new Map(), + sourceLanguageOptions: {}, inputFilename: '', outputFilename: null, vmInternalFunctions: null From c71f7ae51a2aebbabf62ecad22a69d5cf99c74a5 Mon Sep 17 00:00:00 2001 From: tohlh Date: Fri, 18 Apr 2025 11:45:22 +0800 Subject: [PATCH 3/4] Added tests for any checker --- .../__tests__/source4TypedAnyChecker.test.ts | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 src/typeChecker/__tests__/source4TypedAnyChecker.test.ts diff --git a/src/typeChecker/__tests__/source4TypedAnyChecker.test.ts b/src/typeChecker/__tests__/source4TypedAnyChecker.test.ts new file mode 100644 index 000000000..7bf1fed14 --- /dev/null +++ b/src/typeChecker/__tests__/source4TypedAnyChecker.test.ts @@ -0,0 +1,306 @@ +import { mockContext } from '../../mocks/context' +import { Chapter, Variant } from '../../types' +import { parseError } from '../../index' +import { SourceTypedParser } from '../../parser/source/typed' + +const parser = new SourceTypedParser(Chapter.SOURCE_4, Variant.TYPED) + +describe('Any checker tests', () => { + test('disallow any type in a variable declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInVariables'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const x = 4;', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in variable declaration is not allowed.' + ) + }) + + test('allow any type in a variable declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInVariables'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('let x: any = 4;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in a variable declaration, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInVariables'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('let x: number = 4;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in function parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function f(x: any) { return x; }', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in function parameter is not allowed.' + ) + }) + + test('allow any type in function parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function f(x: any) { return x; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in function parameter, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function f(x: number) { return x; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in function return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInReturnType'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function g(): any { return 4; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in function return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function g(): any { return 4; }', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in function return type is not allowed.' + ) + }) + + test('allow any type in function return type, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function g(): number { return 4; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in lambda parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const h = (x: any) => x + 1;', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in arrow function parameter is not allowed.' + ) + }) + + test('allow any type in lambda parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const h = (x: any) => x + 1;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in lambda parameter, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const h = (x: number) => x + 1;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in nested lambda', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const f = (x: number) => (y: any) => x + y;', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in arrow function parameter is not allowed.' + ) + }) + + test('allow any type in nested lambda', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const f = (x: number) => (y: any) => x + y;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in nested lambda, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const f = (x: number) => (y: number) => x + y;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in nested function', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) { + function g(y: any) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in nested function', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) { + function g(y: any) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual( + 'Line 3: Usage of "any" in function parameter is not allowed.' + ) + }) + + test('allow any type in nested function, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in type annotation parameters', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: any) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in type annotation parameters', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: any) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual( + 'Line 2: Usage of "any" in type annotation\'s function parameter is not allowed.' + ) + }) + + test('disallow any type in type annotation parameters, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in type annotation return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationReturnType'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) { + function g(y: number) : any { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in type annotation return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => any { + function g(y: number) : number { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual( + 'Line 2: Usage of "any" in type annotation\'s function return type is not allowed.' + ) + }) + + test('disallow any type in type annotation return type, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => number { + function g(y: number) : number { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) +}) From 229d518bc05353113fc1cb4d41a555d722682e94 Mon Sep 17 00:00:00 2001 From: tohlh Date: Fri, 18 Apr 2025 11:57:28 +0800 Subject: [PATCH 4/4] Added tests for type declaration --- .../__tests__/source4TypedModules.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/typeChecker/__tests__/source4TypedModules.test.ts b/src/typeChecker/__tests__/source4TypedModules.test.ts index f2449a0a2..b83940ef2 100644 --- a/src/typeChecker/__tests__/source4TypedModules.test.ts +++ b/src/typeChecker/__tests__/source4TypedModules.test.ts @@ -16,6 +16,8 @@ beforeEach(() => { class Test1 {} class Test2 {} class Test3 {} + type Test4 = (arg: Test1) => Test2; + const Test4 = (arg: Test1) => Test2; `, x: 'const x: string = "hello"', y: 'const y: number = 42', @@ -284,4 +286,25 @@ describe('Typed module tests', () => { `"Line 3: Type 'Test3' is not assignable to type 'boolean'."` ) }) + + /* TEST CASES FOR THE 'Test4' TYPE */ + it('should allow calling Test4 with a valid Test1 object', () => { + const code = ` + import { test2 } from 'exampleModule'; + const result: Test4 = (arg: Test1) => test2; + ` + parse(code, context) + expect(parseError(context.errors)).toMatchInlineSnapshot(`""`) + }) + + it('should error when calling Test4 with a string argument', () => { + const code = ` + import { test1 } from 'exampleModule'; + const result: Test4 = (arg: Test1) => test1; + ` + parse(code, context) + expect(parseError(context.errors)).toMatchInlineSnapshot( + `"Line 3: Type '(Test1) => Test1' is not assignable to type 'Test4'."` + ) + }) })