Skip to content

Commit db67183

Browse files
committed
include ts lib deps as part of type checking
1 parent c237aa3 commit db67183

File tree

8 files changed

+530
-112
lines changed

8 files changed

+530
-112
lines changed

crates/pctx_executor/src/tests/type_checking.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,77 @@ async function run() {
302302
result.stderr
303303
);
304304
}
305+
306+
#[serial]
307+
#[tokio::test]
308+
async fn test_async_function_with_array_operations() {
309+
let code = r#"
310+
async function run() {
311+
const reservationIds = ["MZDDS4", "60RX9E", "S5IK51", "OUEA45", "Q69X3R"];
312+
const results = [];
313+
for (const id of reservationIds) {
314+
results.push(id);
315+
}
316+
return results;
317+
}
318+
"#;
319+
320+
let result = execute(code, ExecuteOptions::new())
321+
.await
322+
.expect("execution should succeed");
323+
324+
assert!(
325+
result.success,
326+
"Valid async function with array operations should pass type checking, got: diagnostics={:?}, runtime_error={:?}",
327+
result.diagnostics, result.runtime_error
328+
);
329+
assert!(
330+
result.diagnostics.is_empty(),
331+
"Valid async function should have no diagnostics, got: {:?}",
332+
result.diagnostics
333+
);
334+
}
335+
336+
#[serial]
337+
#[tokio::test]
338+
async fn test_promise_string_to_number_mismatch() {
339+
let code = r#"
340+
async function getString(): Promise<string> {
341+
return "hello"
342+
}
343+
344+
function processNumber(value: number): void {
345+
console.log(value);
346+
}
347+
348+
async function run() {
349+
const result = await getString();
350+
processNumber(result); // Type error: string is not assignable to number
351+
}
352+
"#;
353+
354+
let result = execute(code, ExecuteOptions::new())
355+
.await
356+
.expect("execution should succeed");
357+
358+
println!("Success: {}", result.success);
359+
println!("Diagnostics: {:?}", result.diagnostics);
360+
println!("Output: {:?}", result.output);
361+
assert!(
362+
!result.success,
363+
"Type mismatch should fail type checking"
364+
);
365+
assert!(
366+
!result.diagnostics.is_empty(),
367+
"Should have type error diagnostics"
368+
);
369+
assert!(
370+
result
371+
.diagnostics
372+
.iter()
373+
.any(|d| d.message.contains("not assignable")
374+
|| d.message.contains("string") && d.message.contains("number")),
375+
"Error should mention type incompatibility between string and number, got: {:?}",
376+
result.diagnostics
377+
);
378+
}

crates/pctx_type_check_runtime/build.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn generate_runtime_js_string() -> String {
3434
codes.join(", ")
3535
);
3636

37-
// Replace the placeholder
37+
// Replace the placeholder with codes
3838
TYPE_CHECK_RUNTIME_JS.replace("// CODEGEN_IGNORED_CODES_PLACEHOLDER", &codes_js)
3939
}
4040

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

6162
// Get the output directory
6263
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

crates/pctx_type_check_runtime/src/ignored_codes.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,18 @@
1111
/// - Tests: Used to verify filtering behavior
1212
///
1313
/// Each code includes a comment explaining why it's ignored.
14+
///
15+
/// With full ES2020 lib files, we only need to ignore JavaScript compatibility
16+
/// and runtime-specific errors. Type system errors (Promise, console, Array)
17+
/// are now properly checked.
1418
pub(crate) const IGNORED_DIAGNOSTIC_CODES: &[u32] = &[
1519
2307, // Cannot find module - module resolution handled by runtime
1620
2304, // Cannot find name 'require' - not used in ESM
1721
7016, // Could not find declaration file - not needed for runtime
18-
2318, // Cannot find global type 'Promise' - provided by runtime
19-
2580, // Cannot find name 'console' - provided by runtime
20-
2583, // Cannot find name 'Promise' (with lib suggestion) - provided by runtime
21-
2584, // Cannot find name 'console' (with dom suggestion) - provided by runtime
22-
2585, // 'Promise' only refers to a type - provided by runtime
23-
2591, // Cannot find name 'Promise' - provided by runtime
24-
2339, // Property does not exist on type - runtime provides full prototypes
25-
2693, // 'Array' only refers to a type - provided by runtime
2622
7006, // Parameter implicitly has an 'any' type - JS compatibility
2723
7053, // Element implicitly has an 'any' type - dynamic object access is valid
2824
7005, // Variable implicitly has an 'any[]' type - JS compatibility
2925
7034, // Variable implicitly has type 'any[]' - JS compatibility
30-
18046, // Variable is of type 'unknown' - reduce operations work at runtime
3126
2362, // Left-hand side of arithmetic operation - runtime handles coercion
3227
2363, // Right-hand side of arithmetic operation - runtime handles coercion
3328
];

crates/pctx_type_check_runtime/src/lib.rs

Lines changed: 27 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ pub struct CheckResult {
125125
pub static TYPE_CHECK_SNAPSHOT: &[u8] =
126126
include_bytes!(concat!(env!("OUT_DIR"), "/PCTX_TYPE_CHECK_SNAPSHOT.bin"));
127127

128+
/// TypeScript lib.d.ts files for ES2020 support
129+
///
130+
/// This JSON contains all TypeScript standard library definition files,
131+
/// providing type definitions for built-in JavaScript types (Array, Promise,
132+
/// Map, console, etc.). These are injected into the type checker runtime
133+
/// to enable full ES2020 type checking.
134+
static TS_LIBS_JSON: &str = include_str!("../ts-libs.json");
135+
128136
// Define the type check extension
129137
deno_core::extension!(
130138
pctx_type_check_snapshot,
@@ -213,6 +221,15 @@ pub async fn type_check(code: &str) -> Result<CheckResult> {
213221
..Default::default()
214222
});
215223

224+
// Inject TypeScript lib files as a global variable
225+
let inject_libs_script = format!(
226+
"globalThis.TS_LIBS = {};",
227+
TS_LIBS_JSON
228+
);
229+
js_runtime
230+
.execute_script("<inject_ts_libs>", inject_libs_script)
231+
.map_err(|e| TypeCheckError::InternalError(format!("Failed to inject TS_LIBS: {}", e)))?;
232+
216233
// Call the type checking function from the runtime
217234
let code_json =
218235
serde_json::to_string(code).map_err(|e| TypeCheckError::InternalError(e.to_string()))?;
@@ -248,15 +265,12 @@ pub async fn type_check(code: &str) -> Result<CheckResult> {
248265
///
249266
/// # Filtered Error Codes
250267
///
251-
/// The following TypeScript error codes are considered irrelevant and will return `false`:
252-
/// - `2307`: Cannot find module (module resolution)
253-
/// - `2304`: Cannot find name 'require'
254-
/// - `7016`: Could not find declaration file
255-
/// - `2580`, `2585`, `2591`: Promise/console not found (runtime provides these)
256-
/// - `2693`: Type-only imports (Array, etc.) used as values
268+
/// With ES2020 lib files, the following TypeScript error codes are considered irrelevant:
269+
/// - `2307`: Cannot find module (module resolution handled by runtime)
270+
/// - `2304`: Cannot find name 'require' (not used in ESM)
271+
/// - `7016`: Could not find declaration file (not needed for runtime)
257272
/// - `7006`, `7053`, `7005`, `7034`: Implicit any types (JavaScript compatibility)
258-
/// - `18046`: Variable of type 'unknown' (reduce operations)
259-
/// - `2362`, `2363`: Arithmetic operation strictness
273+
/// - `2362`, `2363`: Arithmetic operation strictness (runtime handles coercion)
260274
///
261275
/// # Arguments
262276
///
@@ -281,15 +295,15 @@ pub async fn type_check(code: &str) -> Result<CheckResult> {
281295
/// };
282296
/// assert!(is_relevant_error(&type_error));
283297
///
284-
/// // Console not found - irrelevant (runtime provides it)
285-
/// let console_error = Diagnostic {
286-
/// message: "Cannot find name 'console'.".to_string(),
298+
/// // Module not found - irrelevant (module resolution handled by runtime)
299+
/// let module_error = Diagnostic {
300+
/// message: "Cannot find module './foo'.".to_string(),
287301
/// line: Some(1),
288302
/// column: Some(1),
289303
/// severity: "error".to_string(),
290-
/// code: Some(2580),
304+
/// code: Some(2307),
291305
/// };
292-
/// assert!(!is_relevant_error(&console_error));
306+
/// assert!(!is_relevant_error(&module_error));
293307
/// ```
294308
pub fn is_relevant_error(diagnostic: &Diagnostic) -> bool {
295309
// Use the shared ignored codes list from ignored_codes module
@@ -312,89 +326,3 @@ pub fn is_relevant_error(diagnostic: &Diagnostic) -> bool {
312326
pub fn version() -> &'static str {
313327
env!("CARGO_PKG_VERSION")
314328
}
315-
316-
#[cfg(test)]
317-
mod tests {
318-
use super::*;
319-
320-
#[tokio::test]
321-
async fn test_type_check_valid_code() {
322-
let code = r"const x: number = 42;";
323-
let result = type_check(code).await.expect("type check should not fail");
324-
assert!(result.success);
325-
assert!(result.diagnostics.is_empty());
326-
}
327-
328-
#[tokio::test]
329-
async fn test_type_check_syntax_error() {
330-
let code = r"const x: number = ;";
331-
let result = type_check(code).await.expect("type check should not fail");
332-
assert!(!result.success);
333-
assert!(!result.diagnostics.is_empty());
334-
}
335-
336-
#[test]
337-
fn test_is_relevant_error_function() {
338-
// Relevant error (type mismatch TS2322)
339-
let relevant = Diagnostic {
340-
message: "Type 'string' is not assignable to type 'number'.".to_string(),
341-
line: Some(1),
342-
column: Some(1),
343-
severity: "error".to_string(),
344-
code: Some(2322),
345-
};
346-
assert!(is_relevant_error(&relevant), "TS2322 should be relevant");
347-
348-
// Irrelevant error (console TS2580)
349-
let irrelevant_console = Diagnostic {
350-
message: "Cannot find name 'console'.".to_string(),
351-
line: Some(1),
352-
column: Some(1),
353-
severity: "error".to_string(),
354-
code: Some(2580),
355-
};
356-
assert!(
357-
!is_relevant_error(&irrelevant_console),
358-
"TS2580 should be irrelevant"
359-
);
360-
361-
// Irrelevant error (Promise TS2591)
362-
let irrelevant_promise = Diagnostic {
363-
message: "Cannot find name 'Promise'.".to_string(),
364-
line: Some(1),
365-
column: Some(1),
366-
severity: "error".to_string(),
367-
code: Some(2591),
368-
};
369-
assert!(
370-
!is_relevant_error(&irrelevant_promise),
371-
"TS2591 should be irrelevant"
372-
);
373-
374-
// Irrelevant error (implicit any TS7006)
375-
let irrelevant_implicit_any = Diagnostic {
376-
message: "Parameter implicitly has an 'any' type.".to_string(),
377-
line: Some(1),
378-
column: Some(1),
379-
severity: "error".to_string(),
380-
code: Some(7006),
381-
};
382-
assert!(
383-
!is_relevant_error(&irrelevant_implicit_any),
384-
"TS7006 should be irrelevant"
385-
);
386-
387-
// Error without code should be relevant
388-
let no_code = Diagnostic {
389-
message: "Some error".to_string(),
390-
line: Some(1),
391-
column: Some(1),
392-
severity: "error".to_string(),
393-
code: None,
394-
};
395-
assert!(
396-
is_relevant_error(&no_code),
397-
"Errors without code should be relevant"
398-
);
399-
}
400-
}

crates/pctx_type_check_runtime/src/type_check_runtime.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import * as tsModule from "ext:pctx_type_check_snapshot/typescript.min.js";
99
// CODEGEN_IGNORED_CODES_PLACEHOLDER
1010
// This placeholder is replaced at build time with the actual ignored diagnostic codes
1111
// from src/ignored_codes.rs, ensuring Rust and JavaScript stay in sync.
12+
//
13+
// NOTE: TS_LIBS is injected as a global variable by the Rust runtime before
14+
// calling typeCheckCode(). It contains all TypeScript lib.d.ts files.
1215

1316
// Access ts from the imported module or globalThis
1417
const ts = tsModule.ts || tsModule.default || globalThis.ts;
@@ -35,6 +38,32 @@ interface InvokeCallbackProps {
3538
3639
declare function callMCPTool<T = any>(call: MCPToolProps): Promise<T>;
3740
declare function invokeCallback<T = any>(call: InvokeCallbackProps): Promise<T>;
41+
42+
// Console API (from lib.dom.d.ts, but needed for runtime)
43+
interface Console {
44+
log(...data: any[]): void;
45+
error(...data: any[]): void;
46+
warn(...data: any[]): void;
47+
info(...data: any[]): void;
48+
debug(...data: any[]): void;
49+
trace(...data: any[]): void;
50+
assert(condition?: boolean, ...data: any[]): void;
51+
clear(): void;
52+
count(label?: string): void;
53+
countReset(label?: string): void;
54+
dir(item?: any, options?: any): void;
55+
dirxml(...data: any[]): void;
56+
group(...data: any[]): void;
57+
groupCollapsed(...data: any[]): void;
58+
groupEnd(): void;
59+
table(tabularData?: any, properties?: string[]): void;
60+
time(label?: string): void;
61+
timeEnd(label?: string): void;
62+
timeLog(label?: string, ...data: any[]): void;
63+
timeStamp(label?: string): void;
64+
}
65+
66+
declare var console: Console;
3867
`;
3968

4069
/**
@@ -51,6 +80,13 @@ function typeCheckCode(code) {
5180
const fileName = "check.ts";
5281
const files = new Map();
5382
files.set(fileName, code);
83+
84+
// Add all TypeScript lib files to the virtual file system
85+
for (const [libName, libContent] of Object.entries(TS_LIBS)) {
86+
files.set(libName, libContent);
87+
}
88+
89+
// Add custom lib.deno.d.ts AFTER TypeScript libs (to allow augmentation)
5490
files.set("lib.deno.d.ts", LIB_DENO_NS);
5591

5692
// Create a custom compiler host
@@ -68,7 +104,7 @@ function typeCheckCode(code) {
68104
// Return undefined for files we don't have
69105
return undefined;
70106
},
71-
getDefaultLibFileName: () => "lib.deno.d.ts",
107+
getDefaultLibFileName: () => "lib.es2020.d.ts",
72108
writeFile: () => {},
73109
getCurrentDirectory: () => "/",
74110
getDirectories: () => [],

0 commit comments

Comments
 (0)