Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- CORS layer added to pctx session server. Add custom allowed origins via the `--allow-origin` flag
- Added more of typescript `.d.ts` files for more comprehensive type checking.

### Changed

- [#53](https://github.com/portofcontext/pctx/issues/53) Improved code generation support for tools with no input schema or all optional input schemas
Expand Down
4 changes: 2 additions & 2 deletions crates/pctx_executor/src/tests/callback_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function test() {
const val = await invokeCallback({ id: "MyMath.add", arguments: { a: 12, b: 4 } });
return { error: false, value: val };
} catch (e) {
return { error: true, message: e.message };
return { error: true, message: e instanceof Error ? e.message : String(e) };
}
}

Expand Down Expand Up @@ -92,7 +92,7 @@ async function test() {
const val = await invokeCallback({ id: "MyAsync.wait", arguments: { ms: 50 } });
return { error: false, value: val };
} catch (e) {
return { error: true, message: e.message };
return { error: true, message: e instanceof Error ? e.message : String(e) };
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/pctx_executor/src/tests/mcp_client_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function test() {
});
return { error: false };
} catch (e) {
return { error: true, message: e.message };
return { error: true, message: e instanceof Error ? e.message : String(e) };
}
}

Expand Down
71 changes: 71 additions & 0 deletions crates/pctx_executor/src/tests/type_checking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,74 @@ async function run() {
result.stderr
);
}

#[serial]
#[tokio::test]
async fn test_async_function_with_array_operations() {
let code = r#"
async function run() {
const reservationIds = ["MZDDS4", "60RX9E", "S5IK51", "OUEA45", "Q69X3R"];
const results = [];
for (const id of reservationIds) {
results.push(id);
}
return results;
}
"#;

let result = execute(code, ExecuteOptions::new())
.await
.expect("execution should succeed");

assert!(
result.success,
"Valid async function with array operations should pass type checking, got: diagnostics={:?}, runtime_error={:?}",
result.diagnostics, result.runtime_error
);
assert!(
result.diagnostics.is_empty(),
"Valid async function should have no diagnostics, got: {:?}",
result.diagnostics
);
}

#[serial]
#[tokio::test]
async fn test_promise_string_to_number_mismatch() {
let code = r#"
async function getString(): Promise<string> {
return "hello"
}

function processNumber(value: number): void {
console.log(value);
}

async function run() {
const result = await getString();
processNumber(result); // Type error: string is not assignable to number
}
"#;

let result = execute(code, ExecuteOptions::new())
.await
.expect("execution should succeed");

println!("Success: {}", result.success);
println!("Diagnostics: {:?}", result.diagnostics);
println!("Output: {:?}", result.output);
assert!(!result.success, "Type mismatch should fail type checking");
assert!(
!result.diagnostics.is_empty(),
"Should have type error diagnostics"
);
assert!(
result
.diagnostics
.iter()
.any(|d| d.message.contains("not assignable")
|| d.message.contains("string") && d.message.contains("number")),
"Error should mention type incompatibility between string and number, got: {:?}",
result.diagnostics
);
}
3 changes: 2 additions & 1 deletion crates/pctx_type_check_runtime/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn generate_runtime_js_string() -> String {
codes.join(", ")
);

// Replace the placeholder
// Replace the placeholder with codes
TYPE_CHECK_RUNTIME_JS.replace("// CODEGEN_IGNORED_CODES_PLACEHOLDER", &codes_js)
}

Expand All @@ -57,6 +57,7 @@ fn main() {
println!("cargo:rerun-if-changed=src/type_check_runtime.js");
println!("cargo:rerun-if-changed=src/ignored_codes.rs");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=ts-libs.json");

// Get the output directory
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
Expand Down
31 changes: 13 additions & 18 deletions crates/pctx_type_check_runtime/src/ignored_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,18 @@
/// - Tests: Used to verify filtering behavior
///
/// Each code includes a comment explaining why it's ignored.
///
/// With full ES2020 lib files, we only need to ignore JavaScript compatibility
/// and runtime-specific errors. Type system errors (Promise, console, Array)
/// are now properly checked.
pub(crate) const IGNORED_DIAGNOSTIC_CODES: &[u32] = &[
2307, // Cannot find module - module resolution handled by runtime
2304, // Cannot find name 'require' - not used in ESM
7016, // Could not find declaration file - not needed for runtime
2318, // Cannot find global type 'Promise' - provided by runtime
2580, // Cannot find name 'console' - provided by runtime
2583, // Cannot find name 'Promise' (with lib suggestion) - provided by runtime
2584, // Cannot find name 'console' (with dom suggestion) - provided by runtime
2585, // 'Promise' only refers to a type - provided by runtime
2591, // Cannot find name 'Promise' - provided by runtime
2339, // Property does not exist on type - runtime provides full prototypes
2693, // 'Array' only refers to a type - provided by runtime
7006, // Parameter implicitly has an 'any' type - JS compatibility
7053, // Element implicitly has an 'any' type - dynamic object access is valid
7005, // Variable implicitly has an 'any[]' type - JS compatibility
7034, // Variable implicitly has type 'any[]' - JS compatibility
18046, // Variable is of type 'unknown' - reduce operations work at runtime
2362, // Left-hand side of arithmetic operation - runtime handles coercion
2363, // Right-hand side of arithmetic operation - runtime handles coercion
2307, // Cannot find module - module resolution handled by runtime
2304, // Cannot find name 'require' - not used in ESM
7016, // Could not find declaration file - not needed for runtime
7006, // Parameter implicitly has an 'any' type - JS compatibility
7053, // Element implicitly has an 'any' type - dynamic object access is valid
7005, // Variable implicitly has an 'any[]' type - JS compatibility
7034, // Variable implicitly has type 'any[]' - JS compatibility
2362, // Left-hand side of arithmetic operation - runtime handles coercion
2363, // Right-hand side of arithmetic operation - runtime handles coercion
];
123 changes: 24 additions & 99 deletions crates/pctx_type_check_runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ pub struct CheckResult {
pub static TYPE_CHECK_SNAPSHOT: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/PCTX_TYPE_CHECK_SNAPSHOT.bin"));

/// TypeScript lib.d.ts files for ES2020 support
///
/// This JSON contains all TypeScript standard library definition files,
/// providing type definitions for built-in JavaScript types (Array, Promise,
/// Map, console, etc.). These are injected into the type checker runtime
/// to enable full ES2020 type checking.
static TS_LIBS_JSON: &str = include_str!("../ts-libs.json");

// Define the type check extension
deno_core::extension!(
pctx_type_check_snapshot,
Expand Down Expand Up @@ -213,6 +221,12 @@ pub fn type_check(code: &str) -> Result<CheckResult> {
..Default::default()
});

// Inject TypeScript lib files as a global variable
let inject_libs_script = format!("globalThis.TS_LIBS = {};", TS_LIBS_JSON);
js_runtime
.execute_script("<inject_ts_libs>", inject_libs_script)
.map_err(|e| TypeCheckError::InternalError(format!("Failed to inject TS_LIBS: {}", e)))?;

// Call the type checking function from the runtime
let code_json =
serde_json::to_string(code).map_err(|e| TypeCheckError::InternalError(e.to_string()))?;
Expand Down Expand Up @@ -248,15 +262,12 @@ pub fn type_check(code: &str) -> Result<CheckResult> {
///
/// # Filtered Error Codes
///
/// The following TypeScript error codes are considered irrelevant and will return `false`:
/// - `2307`: Cannot find module (module resolution)
/// - `2304`: Cannot find name 'require'
/// - `7016`: Could not find declaration file
/// - `2580`, `2585`, `2591`: Promise/console not found (runtime provides these)
/// - `2693`: Type-only imports (Array, etc.) used as values
/// With ES2020 lib files, the following TypeScript error codes are considered irrelevant:
/// - `2307`: Cannot find module (module resolution handled by runtime)
/// - `2304`: Cannot find name 'require' (not used in ESM)
/// - `7016`: Could not find declaration file (not needed for runtime)
/// - `7006`, `7053`, `7005`, `7034`: Implicit any types (JavaScript compatibility)
/// - `18046`: Variable of type 'unknown' (reduce operations)
/// - `2362`, `2363`: Arithmetic operation strictness
/// - `2362`, `2363`: Arithmetic operation strictness (runtime handles coercion)
///
/// # Arguments
///
Expand All @@ -281,15 +292,15 @@ pub fn type_check(code: &str) -> Result<CheckResult> {
/// };
/// assert!(is_relevant_error(&type_error));
///
/// // Console not found - irrelevant (runtime provides it)
/// let console_error = Diagnostic {
/// message: "Cannot find name 'console'.".to_string(),
/// // Module not found - irrelevant (module resolution handled by runtime)
/// let module_error = Diagnostic {
/// message: "Cannot find module './foo'.".to_string(),
/// line: Some(1),
/// column: Some(1),
/// severity: "error".to_string(),
/// code: Some(2580),
/// code: Some(2307),
/// };
/// assert!(!is_relevant_error(&console_error));
/// assert!(!is_relevant_error(&module_error));
/// ```
pub fn is_relevant_error(diagnostic: &Diagnostic) -> bool {
// Use the shared ignored codes list from ignored_codes module
Expand All @@ -312,89 +323,3 @@ pub fn is_relevant_error(diagnostic: &Diagnostic) -> bool {
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_type_check_valid_code() {
let code = r"const x: number = 42;";
let result = type_check(code).expect("type check should not fail");
assert!(result.success);
assert!(result.diagnostics.is_empty());
}

#[tokio::test]
async fn test_type_check_syntax_error() {
let code = r"const x: number = ;";
let result = type_check(code).expect("type check should not fail");
assert!(!result.success);
assert!(!result.diagnostics.is_empty());
}

#[test]
fn test_is_relevant_error_function() {
// Relevant error (type mismatch TS2322)
let relevant = Diagnostic {
message: "Type 'string' is not assignable to type 'number'.".to_string(),
line: Some(1),
column: Some(1),
severity: "error".to_string(),
code: Some(2322),
};
assert!(is_relevant_error(&relevant), "TS2322 should be relevant");

// Irrelevant error (console TS2580)
let irrelevant_console = Diagnostic {
message: "Cannot find name 'console'.".to_string(),
line: Some(1),
column: Some(1),
severity: "error".to_string(),
code: Some(2580),
};
assert!(
!is_relevant_error(&irrelevant_console),
"TS2580 should be irrelevant"
);

// Irrelevant error (Promise TS2591)
let irrelevant_promise = Diagnostic {
message: "Cannot find name 'Promise'.".to_string(),
line: Some(1),
column: Some(1),
severity: "error".to_string(),
code: Some(2591),
};
assert!(
!is_relevant_error(&irrelevant_promise),
"TS2591 should be irrelevant"
);

// Irrelevant error (implicit any TS7006)
let irrelevant_implicit_any = Diagnostic {
message: "Parameter implicitly has an 'any' type.".to_string(),
line: Some(1),
column: Some(1),
severity: "error".to_string(),
code: Some(7006),
};
assert!(
!is_relevant_error(&irrelevant_implicit_any),
"TS7006 should be irrelevant"
);

// Error without code should be relevant
let no_code = Diagnostic {
message: "Some error".to_string(),
line: Some(1),
column: Some(1),
severity: "error".to_string(),
code: None,
};
assert!(
is_relevant_error(&no_code),
"Errors without code should be relevant"
);
}
}
38 changes: 37 additions & 1 deletion crates/pctx_type_check_runtime/src/type_check_runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import * as tsModule from "ext:pctx_type_check_snapshot/typescript.min.js";
// CODEGEN_IGNORED_CODES_PLACEHOLDER
// This placeholder is replaced at build time with the actual ignored diagnostic codes
// from src/ignored_codes.rs, ensuring Rust and JavaScript stay in sync.
//
// NOTE: TS_LIBS is injected as a global variable by the Rust runtime before
// calling typeCheckCode(). It contains all TypeScript lib.d.ts files.

// Access ts from the imported module or globalThis
const ts = tsModule.ts || tsModule.default || globalThis.ts;
Expand All @@ -35,6 +38,32 @@ interface InvokeCallbackProps {

declare function callMCPTool<T = any>(call: MCPToolProps): Promise<T>;
declare function invokeCallback<T = any>(call: InvokeCallbackProps): Promise<T>;

// Console API (from lib.dom.d.ts, but needed for runtime)
interface Console {
log(...data: any[]): void;
error(...data: any[]): void;
warn(...data: any[]): void;
info(...data: any[]): void;
debug(...data: any[]): void;
trace(...data: any[]): void;
assert(condition?: boolean, ...data: any[]): void;
clear(): void;
count(label?: string): void;
countReset(label?: string): void;
dir(item?: any, options?: any): void;
dirxml(...data: any[]): void;
group(...data: any[]): void;
groupCollapsed(...data: any[]): void;
groupEnd(): void;
table(tabularData?: any, properties?: string[]): void;
time(label?: string): void;
timeEnd(label?: string): void;
timeLog(label?: string, ...data: any[]): void;
timeStamp(label?: string): void;
}

declare var console: Console;
`;

/**
Expand All @@ -51,6 +80,13 @@ function typeCheckCode(code) {
const fileName = "check.ts";
const files = new Map();
files.set(fileName, code);

// Add all TypeScript lib files to the virtual file system
for (const [libName, libContent] of Object.entries(TS_LIBS)) {
files.set(libName, libContent);
}

// Add custom lib.deno.d.ts AFTER TypeScript libs (to allow augmentation)
files.set("lib.deno.d.ts", LIB_DENO_NS);

// Create a custom compiler host
Expand All @@ -68,7 +104,7 @@ function typeCheckCode(code) {
// Return undefined for files we don't have
return undefined;
},
getDefaultLibFileName: () => "lib.deno.d.ts",
getDefaultLibFileName: () => "lib.es2020.d.ts",
writeFile: () => {},
getCurrentDirectory: () => "/",
getDirectories: () => [],
Expand Down
Loading