diff --git a/crates/ts_goport/src/checker/checker_p01.rs b/crates/ts_goport/src/checker/checker_p01.rs index f52dd5c36..ede728960 100644 --- a/crates/ts_goport/src/checker/checker_p01.rs +++ b/crates/ts_goport/src/checker/checker_p01.rs @@ -812,6 +812,7 @@ pub struct Checker { pub total_instantiation_count: u32, pub instantiation_count: u32, pub instantiation_stack: Vec, + pub(crate) resolving_members_stack: Vec, pub conditional_constraint_depth: u32, pub inline_level: i32, pub serialization_level: i32, @@ -1314,6 +1315,7 @@ impl Checker { total_instantiation_count: 0, instantiation_count: 0, instantiation_stack: Vec::new(), + resolving_members_stack: Vec::new(), conditional_constraint_depth: 0, inline_level: 0, serialization_level: 0, diff --git a/crates/ts_goport/src/checker/checker_p21.rs b/crates/ts_goport/src/checker/checker_p21.rs index 45d688908..35694336e 100644 --- a/crates/ts_goport/src/checker/checker_p21.rs +++ b/crates/ts_goport/src/checker/checker_p21.rs @@ -490,6 +490,18 @@ impl Checker { let t = self.get_reduced_apparent_type(t); let flags = self.ty(t).flags; if flags.intersects(TypeFlags::OBJECT) { + // PORT: not in the pinned Go (pingdotgg/ts-rust#20). See + // `get_resolving_members`. + if !self + .ty(t) + .object_flags + .intersects(ObjectFlags::MEMBERS_RESOLVED) + { + let symbol = self.get_declared_property_of_resolving_type(t, name); + if symbol.is_some() { + return symbol; + } + } self.resolve_structured_type_members(t); let members = self.ty(t).as_structured_type().members; let mut symbol = self.symbols.get_key(members, name); @@ -916,6 +928,11 @@ impl Checker { members = self.symbols.clone_table(members); } let this_argument = type_arguments.last().copied().unwrap_or(TypeId::NIL); + self.resolving_members_stack.push(ResolvingMembers { + t, + declared_members, + members, + }); for base_type in base_types { let mut instantiated_base_type = base_type; if this_argument.is_some() { @@ -952,6 +969,7 @@ impl Checker { .collect(); index_infos = SharedList::concat(index_infos, SharedList::from(filtered)); } + self.resolving_members_stack.pop(); let call_signature_count = call_signatures.len(); self.set_structured_type_members_ex( t, @@ -974,6 +992,48 @@ impl Checker { ); } + // While `resolve_object_type_members` adds the inherited members of a + // class or interface type, or a reference to one, the type arguments of + // its base types may refer back to the type. Inherited members never + // replace a declared value member (`add_inherited_members`), so a + // property the type declares itself is known before its base types are + // resolved. This lets such a circular reference see the declared + // properties without resolving the members of the type again (which + // repeats the same instantiations until the depth limit) and without + // storing a partial resolution on the type. + // PORT: not in the pinned Go (pingdotgg/ts-rust#20, microsoft/TypeScript#64605). + fn get_resolving_members(&self, t: TypeId) -> Option<&ResolvingMembers> { + self.resolving_members_stack.iter().rev().find(|r| r.t == t) + } + + /// The property with the given name that `t` declares itself, if `t` is + /// a type whose members are being resolved. + pub fn get_declared_property_of_resolving_type<'a>( + &self, + t: TypeId, + name: impl Into>, + ) -> SymbolId { + if let Some(r) = self.get_resolving_members(t) { + let name = name.into(); + let symbol = self.symbols.get_key(r.declared_members, name); + if symbol.is_some() && self.sym(symbol).flags.intersects(SymbolFlags::VALUE) { + return self.symbols.get_key(r.members, name); + } + } + SymbolId::NIL + } + + /// Whether `t` is a type whose members are being resolved and that + /// declares a property itself. + pub fn resolving_type_declares_properties(&self, t: TypeId) -> bool { + self.get_resolving_members(t).is_some_and(|r| { + self.symbols.iter(r.declared_members).any(|(name, symbol)| { + self.sym(symbol).flags.intersects(SymbolFlags::VALUE) + && !is_reserved_member_name(name) + }) + }) + } + /// Go `instantiateList` on a shared list. When no element changes, the /// result is `values` itself, as Go returns the input slice. fn instantiate_shared_list( @@ -1755,3 +1815,189 @@ impl Checker { ); } } + +/// The declared members of a type whose base types +/// `resolve_object_type_members` is resolving, and its members table. +pub(crate) struct ResolvingMembers { + t: TypeId, + declared_members: SymbolTable, + members: SymbolTable, +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The code and 1-based line of each semantic diagnostic of `source`, a + /// strict es2020 file with no `types`. `first` runs on the checker of the + /// file before it reports them. + fn diagnostics( + source: &'static str, + first: impl FnOnce(&mut Checker, Node) + Send + 'static, + ) -> Vec<(i32, usize)> { + static CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let call = CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!( + "ts_goport_resolving_members_{}_{call}", + std::process::id() + )); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("a.ts"), source).unwrap(); + std::fs::write( + dir.join("tsconfig.json"), + r#"{ "compilerOptions": { "strict": true, "target": "es2020", "types": [] }, "files": ["a.ts"] }"#, + ) + .unwrap(); + let config = dir.join("tsconfig.json"); + let program = crate::program::try_load_version(&config.to_string_lossy(), |_| {}) + .unwrap_or_else(|e| panic!("cannot load {}: {e}", config.display())); + let _ = std::fs::remove_dir_all(&dir); + let scope = crate::core::enter_program(Some(program)); + let file = program + .source_files() + .find(|file| file.info.file_name.ends_with("/a.ts")) + .expect("a.ts is not in the program") + .root; + let result = crate::program::with_type_checker_for_file(file, move |checker| { + first(checker, file); + crate::program::get_semantic_diagnostics_with_checker( + &crate::gostd::context::background(), + checker, + file, + ) + .iter() + .map(|d| { + let line = 1 + source[..d.pos() as usize].matches('\n').count(); + (d.code(), line) + }) + .collect() + }); + drop(scope); + crate::program::release_program(program); + result + } + + /// pingdotgg/ts-rust#20, microsoft/TypeScript#64605. The type arguments + /// of a base type read a property that the derived type declares itself + /// (`Json["_zod"]`, `JsonInternals["input"]`, the `optin` of + /// `RecordInternals`) while the members of the derived type are + /// being resolved. tsc 7.0.2 and Go before microsoft/TypeScript#64372 + /// report nothing. The pinned Go resolves the members again, repeats the + /// same instantiations and reports TS5115. + #[test] + fn base_type_arguments_read_declared_members_of_the_derived_type() { + // The example of microsoft/TypeScript#64605. + const ISSUE: &str = r#" +interface Internals { input: I } +interface Schema { _zod: Internals } +type RecordInput = V extends unknown ? Record : never; +interface RecordSchema extends Schema { _zod: Internals> } +interface UnionInternals extends Internals {} +interface UnionSchema extends Schema { _zod: UnionInternals } +type JsonValue = { [k: string]: JsonValue }; +type _Json = UnionSchema<[RecordSchema]>; +type _JsonInternals = _Json["_zod"]; +interface JsonInternals extends _JsonInternals { input: JsonValue } +interface Json extends _Json { _zod: JsonInternals } +"#; + // The shape of the Zod 4.6 `z.json()` types: the record internals + // declare `optin`, and the record input asks for it. + const ZOD: &str = r#" +interface Internals { output: O; input: I; optin?: "optional" | undefined } +interface Schema { _zod: Z } +type OptionalIn = { _zod: { optin: "optional" } }; +type RecordInput = [V] extends [OptionalIn] ? Partial> : Record; +interface RecordInternals extends Internals> { optin?: "optional" | undefined } +interface RecordSchema extends Schema> { _zod: RecordInternals } +type IsOptionalIn = T extends OptionalIn ? true : false; +interface UnionInternals extends Internals { + optin: IsOptionalIn extends false ? "optional" | undefined : "optional"; +} +interface UnionSchema extends Schema> { _zod: UnionInternals } +type _Json = UnionSchema<[RecordSchema]>; +interface JsonInternals extends UnionInternals<[RecordSchema]> { input: unknown } +interface Json extends _Json { _zod: JsonInternals } +export const r: RecordInput = { a: 1 }; +export const o: Json["_zod"]["optin"] = undefined; +export const bad: Json["_zod"]["optin"] = "required"; +"#; + assert_eq!(diagnostics(ISSUE, |_, _| {}), vec![]); + assert_eq!(diagnostics(ZOD, |_, _| {}), vec![(2322, 18)]); + } + + /// What microsoft/TypeScript#64372 fixed stays fixed. In its fourslash + /// test `noGhostErrors` (microsoft/TypeScript#62180) the type of the + /// `parent` getter comes first, and then there is no error and the + /// `output` of `Category` has its properties. A base type argument that + /// needs an inherited member (`keyof PersonModel` in + /// `keyofGenericExtendingClassDoubleLayer`) is still infinitely circular. + #[test] + fn inherited_members_are_not_read_before_the_base_types_resolve() { + const NO_GHOST_ERRORS: &str = r#" +interface ZodType { + optional: "true" | "false"; + output: T; +} +interface ZodString extends ZodType { + optional: "false"; +} +type ZodShape = Record; +type Prettify = { [K in keyof T]: T[K] } & {}; +type InferObjectType = Prettify< + { + [k in keyof Shape as Shape[k] extends { optional: "true" } + ? k + : never]?: Shape[k]["output"]; + } & { + [k in keyof Shape as Shape[k] extends { optional: "true" } + ? never + : k]: Shape[k]["output"]; + } +>; +interface ZodObject extends ZodType> { + optional: "false"; +} +interface ZodOptional> + extends ZodType { + optional: "true"; +} +declare function object(shape: T): ZodObject; +declare function string(): ZodString; +declare function optional>(schema: T): ZodOptional; +const Category = object({ + name: string(), + get parent() { + return optional(Category); + }, +}); +export const output = Category.output; +export const name: string = output.name; +export const bad: number = output.name; +"#; + const DOUBLE_LAYER: &str = r#" +class Model { + public createdAt!: Date; +} +type ModelAttributes = Omit; +class AutoModel extends Model> {} +class PersonModel extends AutoModel { + public age!: number; +} +"#; + let parent_first = |checker: &mut Checker, file: Node| { + let category = file + .statements() + .iter() + .find(|s| s.kind() == SyntaxKind::VariableStatement) + .expect("const Category"); + let declaration = category.declaration_list().declarations().nodes().get(0); + let shape = declaration.initializer().arguments().get(0); + let parent = shape.properties().get(1); + let symbol = checker.get_symbol_of_declaration(parent); + checker.get_type_of_symbol(symbol); + }; + assert_eq!(diagnostics(NO_GHOST_ERRORS, parent_first), vec![(2322, 40)]); + assert_eq!(diagnostics(NO_GHOST_ERRORS, |_, _| {}), vec![(2322, 40)]); + assert_eq!(diagnostics(DOUBLE_LAYER, |_, _| {}), vec![(5115, 7)]); + } +} diff --git a/crates/ts_goport/src/checker/checker_p30.rs b/crates/ts_goport/src/checker/checker_p30.rs index 97635c80c..22a1b8528 100644 --- a/crates/ts_goport/src/checker/checker_p30.rs +++ b/crates/ts_goport/src/checker/checker_p30.rs @@ -765,6 +765,18 @@ impl Checker { // Go: checker/checker.go:27830 isStringIndexSignatureOnlyTypeWorker pub fn is_string_index_signature_only_type_worker(&mut self, t: TypeId) -> bool { let flags = self.ty(t).flags; + // PORT: not in the pinned Go (pingdotgg/ts-rust#20). A type whose + // members are being resolved and that declares a property itself has + // properties. + if flags.intersects(TypeFlags::OBJECT) + && !self + .ty(t) + .object_flags + .intersects(ObjectFlags::MEMBERS_RESOLVED) + && self.resolving_type_declares_properties(t) + { + return false; + } (flags.intersects(TypeFlags::OBJECT) && !self.is_generic_mapped_type(t) && self.get_properties_of_type_count(t) == 0