Skip to content

Commit

Permalink
Merge pull request #553 from dodona-edu/feat/add-typescript-with-loader
Browse files Browse the repository at this point in the history
Add TypeScript
  • Loading branch information
jorg-vr authored Nov 26, 2024
2 parents f9689ae + f507efc commit 5e62a7d
Show file tree
Hide file tree
Showing 68 changed files with 1,795 additions and 51 deletions.
14 changes: 9 additions & 5 deletions .devcontainer/dodona-tested.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ RUN <<EOF
xxd \
shellcheck

# JavaScript dependencies
bash -c 'set -o pipefail && curl -fsSL https://deb.nodesource.com/setup_22.x | bash -'
apt-get install -y --no-install-recommends nodejs
npm install -g [email protected] [email protected]

# TypeScript dependencies
npm install -g [email protected] [email protected]
npm install -g @types/node @typescript-eslint/parser @typescript-eslint/eslint-plugin

# Haskell dependencies
apt-get install -y --no-install-recommends \
hlint \
Expand All @@ -78,11 +87,6 @@ RUN <<EOF
cabal update
cabal v1-install --global aeson

# JavaScript dependencies
bash -c 'set -o pipefail && curl -fsSL https://deb.nodesource.com/setup_22.x | bash -'
apt-get install -y --no-install-recommends nodejs
npm install -g [email protected] [email protected]

# C# dependencies
curl https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb --output packages-microsoft-prod.deb
dpkg -i packages-microsoft-prod.deb
Expand Down
1 change: 1 addition & 0 deletions tested/dsl/schema-strict.json
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@
"haskell",
"java",
"javascript",
"typescript",
"kotlin",
"python",
"runhaskell",
Expand Down
1 change: 1 addition & 0 deletions tested/dsl/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@
"haskell",
"java",
"javascript",
"typescript",
"kotlin",
"python",
"runhaskell",
Expand Down
8 changes: 8 additions & 0 deletions tested/internationalization/nl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ nl:
linter:
crashed: "Pylint gecrasht"
output: "Pylint produceerde slechte uitvoer."
typescript:
runtime:
invalid:
exception: "%{actual_type} is geen instantie van Error (of een subklasse)."
linter:
timeout: "ESLint overschreed tijdslimiet."
memory: "ESLint overschreed geheugenlimiet."
output: "ESLint produceerde slechte uitvoer."
javascript:
runtime:
invalid:
Expand Down
2 changes: 2 additions & 0 deletions tested/languages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tested.languages.language import Language
from tested.languages.python.config import Python
from tested.languages.runhaskell.config import RunHaskell
from tested.languages.typescript.config import TypeScript

if TYPE_CHECKING:
from tested.configs import GlobalConfig
Expand All @@ -30,6 +31,7 @@
"haskell": Haskell,
"java": Java,
"javascript": JavaScript,
"typescript": TypeScript,
"kotlin": Kotlin,
"python": Python,
"runhaskell": RunHaskell,
Expand Down
28 changes: 14 additions & 14 deletions tested/languages/csharp/templates/dotnet.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<CSFile Include="*.cs" />
</ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<CSFile Include="*.cs" />
</ItemGroup>

</Project>
1 change: 0 additions & 1 deletion tested/languages/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ def compilation(self, files: list[str]) -> CallbackResult:
:param files: A suggestion containing the dependencies TESTed thinks might
be useful to compile. By convention, the last file in the list
is the file containing the "main" function.
:return: The compilation command and either the resulting files or a filter
for the resulting files.
"""
Expand Down
258 changes: 258 additions & 0 deletions tested/languages/typescript/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import json
import logging
import os
import re
from pathlib import Path
from typing import TYPE_CHECKING

from tested.datatypes import (
AdvancedObjectTypes,
AllTypes,
BasicStringTypes,
ExpressionTypes,
)
from tested.dodona import AnnotateCode, Message, Status
from tested.features import Construct, TypeSupport
from tested.languages.conventionalize import (
EXECUTION_PREFIX,
Conventionable,
NamingConventions,
submission_file,
submission_name,
)
from tested.languages.language import (
CallbackResult,
Command,
Language,
TypeDeclarationMetadata,
)
from tested.languages.utils import cleanup_description
from tested.serialisation import Statement, Value

if TYPE_CHECKING:
from tested.languages.generation import PreparedExecutionUnit

logger = logging.getLogger(__name__)


class TypeScript(Language):

def initial_dependencies(self) -> list[str]:
return ["values.ts"]

def needs_selector(self) -> bool:
return False

def file_extension(self) -> str:
return "ts"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "camel_case",
"function": "camel_case",
"identifier": "camel_case",
"global_identifier": "macro_case",
"property": "camel_case",
"class": "pascal_case",
}

def supported_constructs(self) -> set[Construct]:
return {
Construct.OBJECTS,
Construct.EXCEPTIONS,
Construct.FUNCTION_CALLS,
Construct.ASSIGNMENTS,
Construct.HETEROGENEOUS_COLLECTIONS,
Construct.HETEROGENEOUS_ARGUMENTS,
Construct.EVALUATION,
Construct.DEFAULT_PARAMETERS,
Construct.GLOBAL_VARIABLES,
Construct.NAMED_ARGUMENTS,
}

def datatype_support(self) -> dict[AllTypes, TypeSupport]:
return { # type: ignore
"integer": "supported",
"real": "supported",
"char": "reduced",
"text": "supported",
"string": "supported",
"boolean": "supported",
"sequence": "supported",
"set": "supported",
"map": "supported",
"dictionary": "supported",
"object": "supported",
"nothing": "supported",
"undefined": "supported",
"null": "supported",
"int8": "reduced",
"uint8": "reduced",
"int16": "reduced",
"uint16": "reduced",
"int32": "reduced",
"uint32": "reduced",
"int64": "reduced",
"uint64": "reduced",
"bigint": "supported",
"single_precision": "reduced",
"double_precision": "reduced",
"double_extended": "reduced",
"array": "reduced",
"list": "reduced",
"tuple": "reduced",
}

def collection_restrictions(self) -> dict[AllTypes, set[ExpressionTypes]]:
return {AdvancedObjectTypes.OBJECT: {BasicStringTypes.TEXT}}

def compilation(self, files: list[str]) -> CallbackResult:
submission = submission_file(self)
main_file = self.find_main_file(
list(map(lambda name: Path(name), files)), submission
)

if main_file != Status.COMPILATION_ERROR:
path_to_modules = os.environ["NODE_PATH"]
return (
[
"tsc",
"--target",
"esnext",
"--module",
"nodenext",
"--allowJs",
"--allowImportingTsExtensions",
"--noEmit",
"--esModuleInterop",
"--typeRoots",
f"{path_to_modules}/@types",
str(main_file.name),
],
files,
)
else:
return [], files

def execution(self, cwd: Path, file: str, arguments: list[str]) -> Command:
return ["tsx", file, *arguments]

def modify_solution(self, solution: Path):
# import local to prevent errors
from tested.judge.utils import run_command

assert self.config

parse_file = str(Path(__file__).parent / "parseAst.ts")
output = run_command(
solution.parent,
timeout=None,
command=["tsx", parse_file, str(solution.absolute())],
check=True,
)
assert output, "Missing output from TypesScript's modify_solution"
namings = output.stdout.strip()
with open(solution, "a") as file:
print(f"\nexport {{{namings}}};", file=file)

# Add strict mode to the script.
with open(solution, "r") as file:
non_strict = file.read()

# TODO: This may be deleted in the future.
with open(solution, "w") as file:
file.write('"use strict";\n' + non_strict)
self.config.dodona.source_offset -= 2

def linter(self, remaining: float) -> tuple[list[Message], list[AnnotateCode]]:
# Import locally to prevent errors.
from tested.languages.typescript import linter

assert self.config
return linter.run_eslint(self.config.dodona, remaining)

def cleanup_stacktrace(self, stacktrace: str) -> str:
assert self.config
# What this does:
# 1a. While inside the submission code, replace all references to the location with <code>
# 1b. Remove any "submission.SOMETHING" -> "SOMETHING"
# 2. Once we encounter a line with the execution location, skip all lines.
execution_submission_location_regex = f"{self.config.dodona.workdir}/{EXECUTION_PREFIX}[_0-9]+/{submission_file(self)}"
submission_location = (
self.config.dodona.workdir / "common" / submission_file(self)
)
compilation_submission_location = str(submission_location.resolve())
execution_location_regex = f"{self.config.dodona.workdir}/{EXECUTION_PREFIX}[_0-9]+/{EXECUTION_PREFIX}[_0-9]+.ts"
submission_namespace = f"{submission_name(self)}."

resulting_lines = ""
for line in stacktrace.splitlines(keepends=True):
# If we encounter an execution location, we are done.
if re.search(execution_location_regex, line):
break

# Replace any reference to the submission.
line = re.sub(execution_submission_location_regex, "<code>", line)
line = line.replace(compilation_submission_location, "<code>")
# Remove any references of the form "submission.SOMETHING"
line = line.replace(submission_namespace, "")

resulting_lines += line

return resulting_lines

def cleanup_description(self, statement: str) -> str:
statement = cleanup_description(self, statement)
await_regex = re.compile(r"await\s+")
return await_regex.sub("", statement)

def generate_statement(self, statement: Statement) -> str:
from tested.languages.typescript import generators

return generators.convert_statement(statement, full=True)

def generate_execution_unit(self, execution_unit: "PreparedExecutionUnit") -> str:
from tested.languages.typescript import generators

return generators.convert_execution_unit(execution_unit)

def generate_encoder(self, values: list[Value]) -> str:
from tested.languages.typescript import generators

return generators.convert_encoder(values)

def get_declaration_metadata(self) -> TypeDeclarationMetadata:
return {
"names": { # type: ignore
"integer": "number",
"real": "number",
"char": "string",
"text": "string",
"string": "string",
"boolean": "boolean",
"sequence": "array",
"set": "set",
"map": "object",
"nothing": "null",
"undefined": "undefined",
"int8": "number",
"uint8": "number",
"int16": "number",
"uint16": "number",
"int32": "number",
"uint32": "number",
"int64": "number",
"uint64": "number",
"bigint": "number",
"single_precision": "number",
"double_precision": "number",
"double_extended": "number",
"fixed_precision": "number",
"array": "array",
"list": "array",
"tuple": "array",
"any": "object",
},
"nested": ("<", ">"),
"exception": "Error",
}
16 changes: 16 additions & 0 deletions tested/languages/typescript/eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
parser: "@typescript-eslint/parser"
parserOptions:
ecmaVersion: "latest"
sourceType: "module"
ecmaFeatures: {}
plugins:
- "@typescript-eslint"
extends: "plugin:@typescript-eslint/recommended"
env:
node: yes
es2020: yes
rules:
no-var: "warn"
semi: "warn"
'@typescript-eslint/no-unused-expressions': "off"

Loading

0 comments on commit 5e62a7d

Please sign in to comment.