diff --git a/packages/typia/native/cmd/ttsc-typia/build.go b/packages/typia/native/cmd/ttsc-typia/build.go index be63f724d7..9faf29aff3 100644 --- a/packages/typia/native/cmd/ttsc-typia/build.go +++ b/packages/typia/native/cmd/ttsc-typia/build.go @@ -14,6 +14,7 @@ import ( "github.com/samchon/ttsc/packages/ttsc/driver" typiaadapter "github.com/samchon/typia/packages/typia/native/adapter" nativecontext "github.com/samchon/typia/packages/typia/native/core/context" + schemametadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata" nativetransform "github.com/samchon/typia/packages/typia/native/transform" ) @@ -107,6 +108,8 @@ func runBuild(args []string) int { } } defer prog.Close() + registerTypiaDefaultLibraryClassifier(prog) + defer schemametadata.MetadataDefaultLibrary_release(prog.Checker) transformDiags := []typiaTransformDiagnostic{} transformOptions := pluginOptions.TransformOptions() diff --git a/packages/typia/native/cmd/ttsc-typia/default_library_spoof_native_identity_transform_test.go b/packages/typia/native/cmd/ttsc-typia/default_library_spoof_native_identity_transform_test.go new file mode 100644 index 0000000000..7152406689 --- /dev/null +++ b/packages/typia/native/cmd/ttsc-typia/default_library_spoof_native_identity_transform_test.go @@ -0,0 +1,216 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestDefaultLibrarySpoofNativeIdentityTransform keeps a declaration package out +// of the runtime-native authority even when its file is named like a TypeScript +// default library (#2200). +// +// Runtime-native identity admits a global whose constructor a default library +// provides, so how "default library" is decided is the whole boundary. `lib.d.ts` +// is an ordinary published file name, so a base-name test makes that authority +// claimable by any package a project pulls in through `typeRoots` — the forged +// declarations below are a full-member copy of the DOM `File` and `Blob`, +// including their `declare var` constructor bindings, and nothing but the +// containing directory distinguishes them from the real thing. +// +// 1. Publish `lib.dom.d.ts` and `lib.es5.d.ts` from a fixture `@types` package +// that declares global File and Blob with their constructors. +// 2. Transform validators for those globals with no DOM library and no Node +// types loaded, so the forged package is their only declaration source. +// 3. Require the emit to keep the forged members instead of an `instanceof`. +// 4. Execute the validators in Node against the forged structural values and +// against the real runtime instances, requiring an exact case count. +func TestDefaultLibrarySpoofNativeIdentityTransform(t *testing.T) { + project := defaultLibrarySpoofNativeIdentityProject(t) + js, errText, code := ttscTypiaTestCapture(func() int { + return runTransform([]string{ + "--cwd", project, + "--tsconfig", "tsconfig.json", + "--file", "src/input.ts", + "--output", "js", + }) + }) + if code != 0 { + t.Fatalf("default library spoof transform failed: code=%d stderr=\n%s", code, errText) + } + + failures := []string{} + for _, native := range []string{"instanceof File", "instanceof Blob"} { + if strings.Contains(js, native) { + failures = append(failures, fmt.Sprintf("a lib.*.d.ts named package declaration was promoted to %q", native)) + } + } + for _, member := range []string{"spoofBlobBrand", "spoofFileBrand"} { + if !strings.Contains(js, member) { + failures = append(failures, fmt.Sprintf("emit dropped forged declaration member %q", member)) + } + } + + output, runtimeErr := defaultLibrarySpoofNativeIdentityRun(t, project, js) + if runtimeErr != nil { + failures = append(failures, fmt.Sprintf("runtime matrix failed: %v\n%s", runtimeErr, output)) + } + if expected := "RAN 6 SPOOF CASES"; !strings.Contains(output, expected) { + failures = append(failures, fmt.Sprintf("spoof runner did not report %q; got:\n%s", expected, output)) + } + if len(failures) != 0 { + t.Fatalf("default library spoof mismatches:\n%s\n\nemit:\n%s", strings.Join(failures, "\n"), js) + } +} + +func defaultLibrarySpoofNativeIdentityProject(t *testing.T) string { + t.Helper() + root := ttscTypiaTestRepoRoot(t) + base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests") + if err := os.MkdirAll(base, 0o755); err != nil { + t.Fatalf("mkdir temp base: %v", err) + } + dir, err := os.MkdirTemp(base, "default-library-spoof-native-identity-") + if err != nil { + t.Fatalf("create temp fixture: %v", err) + } + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + for name, content := range map[string]string{ + "tsconfig.json": defaultLibrarySpoofNativeIdentityTSConfig, + "node_modules/@types/spoof-lib/package.json": defaultLibrarySpoofNativeIdentityPackageJSON, + "node_modules/@types/spoof-lib/lib.dom.d.ts": defaultLibrarySpoofNativeIdentityDeclarations, + "node_modules/@types/spoof-lib/lib.es5.d.ts": defaultLibrarySpoofNativeIdentityExtraDeclarations, + "src/input.ts": defaultLibrarySpoofNativeIdentitySource, + } { + path := filepath.Join(dir, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir fixture path %s: %v", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write fixture file %s: %v", path, err) + } + } + return dir +} + +func defaultLibrarySpoofNativeIdentityRun(t *testing.T, project string, js string) (string, error) { + t.Helper() + node, err := exec.LookPath("node") + if err != nil { + t.Skip("node executable not found") + return "", nil + } + runtimeDir := filepath.Join(project, "runtime") + if err := os.MkdirAll(runtimeDir, 0o755); err != nil { + t.Fatalf("mkdir runtime dir: %v", err) + } + ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir) + for name, content := range map[string]string{ + "input.cjs": ttscTypiaTestRewriteCommonJS(t, js), + "run.cjs": defaultLibrarySpoofNativeIdentityRuntimeRunner, + } { + if err := os.WriteFile(filepath.Join(runtimeDir, name), []byte(content), 0o644); err != nil { + t.Fatalf("write runtime file %s: %v", name, err) + } + } + cmd := exec.Command(node, filepath.Join(runtimeDir, "run.cjs")) + cmd.Dir = runtimeDir + output, err := cmd.CombinedOutput() + return string(output), err +} + +const defaultLibrarySpoofNativeIdentityTSConfig = `{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "types": ["spoof-lib"], + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} +` + +const defaultLibrarySpoofNativeIdentityPackageJSON = `{ + "name": "@types/spoof-lib", + "version": "1.0.0", + "types": "lib.dom.d.ts" +} +` + +const defaultLibrarySpoofNativeIdentityDeclarations = `/// + +interface Blob { + readonly size: number; + readonly type: string; + spoofBlobBrand: string; +} +declare var Blob: { + prototype: Blob; + new (parts?: unknown[], options?: unknown): Blob; +}; +` + +const defaultLibrarySpoofNativeIdentityExtraDeclarations = `interface File extends Blob { + readonly lastModified: number; + readonly name: string; + readonly webkitRelativePath: string; + spoofFileBrand: string; +} +declare var File: { + prototype: File; + new (parts: unknown[], name: string, options?: unknown): File; +}; +` + +const defaultLibrarySpoofNativeIdentitySource = `import typia from "typia"; + +export const createdIsBlob = typia.createIs(); +export const createdIsFile = typia.createIs(); +` + +const defaultLibrarySpoofNativeIdentityRuntimeRunner = `const validators = require("./input.cjs"); + +let ran = 0; +const failures = []; +const eq = (name, actual, expected) => { + ran += 1; + if (actual !== expected) { + failures.push(name + ": expected " + expected + " but got " + actual); + } +}; + +const spoofBlob = { + size: 1, + type: "text/plain", + spoofBlobBrand: "spoof", +}; +const spoofFile = { + ...spoofBlob, + lastModified: 0, + name: "x.txt", + webkitRelativePath: "", + spoofFileBrand: "spoof", +}; + +eq("spoof Blob structural", validators.createdIsBlob(spoofBlob), true); +eq("spoof Blob rejects runtime instance", validators.createdIsBlob(new Blob(["x"])), false); +eq("spoof Blob missing brand", validators.createdIsBlob({ size: 1, type: "text/plain" }), false); +eq("spoof File structural", validators.createdIsFile(spoofFile), true); +eq("spoof File rejects runtime instance", validators.createdIsFile(new File(["x"], "x.txt")), false); +eq("spoof File missing brand", validators.createdIsFile({ ...spoofFile, spoofFileBrand: undefined }), false); + +console.log("RAN " + ran + " SPOOF CASES"); +if (failures.length !== 0) { + throw new Error("MISMATCHES:\n" + failures.join("\n")); +} +` diff --git a/packages/typia/native/cmd/ttsc-typia/lib_replacement_native_identity_transform_test.go b/packages/typia/native/cmd/ttsc-typia/lib_replacement_native_identity_transform_test.go new file mode 100644 index 0000000000..a3ae69663c --- /dev/null +++ b/packages/typia/native/cmd/ttsc-typia/lib_replacement_native_identity_transform_test.go @@ -0,0 +1,277 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestLibReplacementNativeIdentityTransform keeps a `libReplacement` project's +// natives native even though their declaration file lives in node_modules +// (#2200). +// +// Runtime-native identity requires a runtime authority to declare the global, so +// which files count as a TypeScript default library became load-bearing. Under +// `libReplacement` the compiler resolves `lib.es2015.collection.d.ts` to +// `@typescript/lib-es2015/collection`, an ordinary package file whose name does +// not even begin with `lib.` — so a file-name rule answers "not a default +// library" and would demote Map, Set, WeakMap, and WeakSet to structural checks +// that accept any object. Only the program's own classification is right here, +// and this fixture is what proves the transform consults it. +// +// 1. Publish `@typescript/lib-es2015/collection.d.ts` carrying the real +// Map/Set/WeakMap/WeakSet declarations plus a probe interface that exists +// nowhere else, and enable `libReplacement`. +// 2. Transform validators for the four replaced collection natives, for Date +// and Uint8Array as bundled-library controls, and for the probe. +// 3. Require the probe to transform at all, which fails unless the replacement +// file — not the bundled lib — is what the program loaded. +// 4. Require every native to keep its `instanceof` check and execute all of +// them in Node against real instances and plain objects. +func TestLibReplacementNativeIdentityTransform(t *testing.T) { + project := libReplacementNativeIdentityProject(t) + js, errText, code := ttscTypiaTestCapture(func() int { + return runTransform([]string{ + "--cwd", project, + "--tsconfig", "tsconfig.json", + "--file", "src/input.ts", + "--output", "js", + }) + }) + if code != 0 { + t.Fatalf("lib replacement transform failed: code=%d stderr=\n%s", code, errText) + } + + failures := []string{} + if !strings.Contains(js, "libReplacementMarker") { + failures = append(failures, "emit lost the replacement-only probe member; the project did not load the replaced lib file") + } + for _, kept := range []string{ + "instanceof Map", + "instanceof Set", + "instanceof WeakMap", + "instanceof WeakSet", + "instanceof Date", + "instanceof Uint8Array", + } { + if !strings.Contains(js, kept) { + failures = append(failures, fmt.Sprintf("replaced or bundled default library native lost %q", kept)) + } + } + + output, runtimeErr := libReplacementNativeIdentityRun(t, project, js) + if runtimeErr != nil { + failures = append(failures, fmt.Sprintf("runtime matrix failed: %v\n%s", runtimeErr, output)) + } + if expected := "RAN 13 REPLACEMENT CASES"; !strings.Contains(output, expected) { + failures = append(failures, fmt.Sprintf("replacement runner did not report %q; got:\n%s", expected, output)) + } + if len(failures) != 0 { + t.Fatalf("lib replacement native identity mismatches:\n%s\n\nemit:\n%s", strings.Join(failures, "\n"), js) + } +} + +func libReplacementNativeIdentityProject(t *testing.T) string { + t.Helper() + root := ttscTypiaTestRepoRoot(t) + base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests") + if err := os.MkdirAll(base, 0o755); err != nil { + t.Fatalf("mkdir temp base: %v", err) + } + dir, err := os.MkdirTemp(base, "lib-replacement-native-identity-") + if err != nil { + t.Fatalf("create temp fixture: %v", err) + } + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + for name, content := range map[string]string{ + "tsconfig.json": libReplacementNativeIdentityTSConfig, + "node_modules/@typescript/lib-es2015/package.json": libReplacementNativeIdentityPackageJSON, + "node_modules/@typescript/lib-es2015/collection.d.ts": libReplacementNativeIdentityCollectionLib, + "src/input.ts": libReplacementNativeIdentitySource, + } { + path := filepath.Join(dir, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir fixture path %s: %v", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write fixture file %s: %v", path, err) + } + } + return dir +} + +func libReplacementNativeIdentityRun(t *testing.T, project string, js string) (string, error) { + t.Helper() + node, err := exec.LookPath("node") + if err != nil { + t.Skip("node executable not found") + return "", nil + } + runtimeDir := filepath.Join(project, "runtime") + if err := os.MkdirAll(runtimeDir, 0o755); err != nil { + t.Fatalf("mkdir runtime dir: %v", err) + } + ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir) + for name, content := range map[string]string{ + "input.cjs": ttscTypiaTestRewriteCommonJS(t, js), + "run.cjs": libReplacementNativeIdentityRuntimeRunner, + } { + if err := os.WriteFile(filepath.Join(runtimeDir, name), []byte(content), 0o644); err != nil { + t.Fatalf("write runtime file %s: %v", name, err) + } + } + cmd := exec.Command(node, filepath.Join(runtimeDir, "run.cjs")) + cmd.Dir = runtimeDir + output, err := cmd.CombinedOutput() + return string(output), err +} + +const libReplacementNativeIdentityTSConfig = `{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "types": [], + "libReplacement": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} +` + +const libReplacementNativeIdentityPackageJSON = `{ + "name": "@typescript/lib-es2015", + "version": "1.0.0" +} +` + +// libReplacementNativeIdentityCollectionLib is the replacement TypeScript loads +// instead of its bundled `lib.es2015.collection.d.ts`. The collection +// declarations are transcribed from that file so the rest of the standard +// library still typechecks against them; `LibReplacementProbe` is the one +// addition, and it is what proves the program loaded this file. +const libReplacementNativeIdentityCollectionLib = `interface LibReplacementProbe { + libReplacementMarker: string; +} + +interface Map { + clear(): void; + delete(key: K): boolean; + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void; + get(key: K): V | undefined; + has(key: K): boolean; + set(key: K, value: V): this; + readonly size: number; +} + +interface MapConstructor { + new (): Map; + new (entries?: readonly (readonly [K, V])[] | null): Map; + readonly prototype: Map; +} +declare var Map: MapConstructor; + +interface ReadonlyMap { + forEach(callbackfn: (value: V, key: K, map: ReadonlyMap) => void, thisArg?: any): void; + get(key: K): V | undefined; + has(key: K): boolean; + readonly size: number; +} + +interface WeakMap { + delete(key: K): boolean; + get(key: K): V | undefined; + has(key: K): boolean; + set(key: K, value: V): this; +} + +interface WeakMapConstructor { + new (entries?: readonly (readonly [K, V])[] | null): WeakMap; + readonly prototype: WeakMap; +} +declare var WeakMap: WeakMapConstructor; + +interface Set { + add(value: T): this; + clear(): void; + delete(value: T): boolean; + forEach(callbackfn: (value: T, value2: T, set: Set) => void, thisArg?: any): void; + has(value: T): boolean; + readonly size: number; +} + +interface SetConstructor { + new (values?: readonly T[] | null): Set; + readonly prototype: Set; +} +declare var Set: SetConstructor; + +interface ReadonlySet { + forEach(callbackfn: (value: T, value2: T, set: ReadonlySet) => void, thisArg?: any): void; + has(value: T): boolean; + readonly size: number; +} + +interface WeakSet { + add(value: T): this; + delete(value: T): boolean; + has(value: T): boolean; +} + +interface WeakSetConstructor { + new (values?: readonly T[] | null): WeakSet; + readonly prototype: WeakSet; +} +declare var WeakSet: WeakSetConstructor; +` + +const libReplacementNativeIdentitySource = `import typia from "typia"; + +export const createdIsProbe = typia.createIs(); +export const createdIsMap = typia.createIs>(); +export const createdIsSet = typia.createIs>(); +export const createdIsWeakMap = typia.createIs>(); +export const createdIsWeakSet = typia.createIs>(); +export const createdIsDate = typia.createIs(); +export const createdIsBytes = typia.createIs(); +` + +const libReplacementNativeIdentityRuntimeRunner = `const validators = require("./input.cjs"); + +let ran = 0; +const failures = []; +const eq = (name, actual, expected) => { + ran += 1; + if (actual !== expected) { + failures.push(name + ": expected " + expected + " but got " + actual); + } +}; + +eq("probe structural", validators.createdIsProbe({ libReplacementMarker: "x" }), true); +eq("Map real", validators.createdIsMap(new Map([["x", 1]])), true); +eq("Map plain", validators.createdIsMap({}), false); +eq("Set real", validators.createdIsSet(new Set(["x"])), true); +eq("Set plain", validators.createdIsSet({}), false); +eq("WeakMap real", validators.createdIsWeakMap(new WeakMap()), true); +eq("WeakMap plain", validators.createdIsWeakMap({}), false); +eq("WeakSet real", validators.createdIsWeakSet(new WeakSet()), true); +eq("WeakSet plain", validators.createdIsWeakSet({}), false); +eq("Date real", validators.createdIsDate(new Date()), true); +eq("Date plain", validators.createdIsDate({}), false); +eq("Uint8Array real", validators.createdIsBytes(new Uint8Array(1)), true); +eq("Uint8Array plain", validators.createdIsBytes({}), false); + +console.log("RAN " + ran + " REPLACEMENT CASES"); +if (failures.length !== 0) { + throw new Error("MISMATCHES:\n" + failures.join("\n")); +} +` diff --git a/packages/typia/native/cmd/ttsc-typia/transform.go b/packages/typia/native/cmd/ttsc-typia/transform.go index b4149f3b30..94ae2c634d 100644 --- a/packages/typia/native/cmd/ttsc-typia/transform.go +++ b/packages/typia/native/cmd/ttsc-typia/transform.go @@ -80,6 +80,8 @@ func runTransform(args []string) int { return 2 } defer prog.Close() + registerTypiaDefaultLibraryClassifier(prog) + defer schemametadata.MetadataDefaultLibrary_release(prog.Checker) transformDiags := []typiaTransformDiagnostic{} transformOptions := pluginOptions.TransformOptions() @@ -122,6 +124,27 @@ func runTransform(args []string) int { }) } +// registerTypiaDefaultLibraryClassifier hands metadata analysis the program's +// own default-library classification, which runtime-native identity is decided +// from: a global is the JavaScript built-in only when a runtime authority +// declares it (#2200). `prog.TSProgram.IsLibFile` is the same oracle the +// dependency collector above uses, and for the same reason — a default library +// is not recognizable from its file name under `bundled:///`, `noembed`, or +// `libReplacement` (#2108). +// +// Every program-loading host registers it against the checker whose types the +// analysis reads, and releases it when that program closes, so no closed +// program's checker stays reachable from the registry. +func registerTypiaDefaultLibraryClassifier(prog *driver.Program) { + if prog == nil || prog.TSProgram == nil { + return + } + program := prog.TSProgram + schemametadata.MetadataDefaultLibrary_register(prog.Checker, func(source *shimast.SourceFile) bool { + return source != nil && program.IsLibFile(source) + }) +} + // runTransformSingle is the single-file counterpart of runTransformProject's // diagnostic decision: produce the artifact into memory, then publish it only // when typia's analysis reported nothing. Every single-file output mode routes diff --git a/packages/typia/native/cmd/ttsc-typia/user_global_native_identity_transform_test.go b/packages/typia/native/cmd/ttsc-typia/user_global_native_identity_transform_test.go new file mode 100644 index 0000000000..1274709951 --- /dev/null +++ b/packages/typia/native/cmd/ttsc-typia/user_global_native_identity_transform_test.go @@ -0,0 +1,484 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestUserGlobalNativeIdentityTransform keeps a user-authored global structural +// while a runtime-provided global of the same name stays runtime-native (#2200). +// +// Winning the checker's global type table proves only that a declaration is +// global. A project whose sole `File` or `Blob` declaration is its own `declare +// global` block wins that same lookup, so a resolution-only identity gate erases +// the user's members, emits `input instanceof File`, and throws `ReferenceError` +// wherever no runtime supplies that constructor. Native classification therefore +// has to ask which authority provides the constructor, and it must ask that +// question without demoting the genuine default-library, Node bare-global, or +// augmented natives that legitimately answer it. +// +// 1. Transform a project whose only File/Blob declarations are user globals in +// both the interface and the class spelling, one of them carrying its own +// `declare var` constructor binding, across is/assert/assertGuard/validate/ +// equals/random, direct and factory forms, and alias, nested, intersection, +// union, and re-exported compositions. +// 2. Transform that project's json, protobuf, and llm schema surfaces, which a +// native classification removes from those consumers entirely. +// 3. Transform a global `type` alias spelling of the same collision in a second +// project, and near-miss control names in the first. +// 4. Transform a third project where @types/node provides the runtime bridge +// and the user contributes only an empty augmentation, requiring every +// default-library and Node-provided native to keep its `instanceof` check. +// 5. Execute every emitted validator in Node against structural user values and +// against the real runtime instances, requiring exact case counts so a +// dropped branch cannot pass silently. +func TestUserGlobalNativeIdentityTransform(t *testing.T) { + project := userGlobalNativeIdentityProject(t) + aliasProject := userGlobalNativeIdentityAliasProject(t) + providedProject := userGlobalNativeIdentityProvidedProject(t) + transform := func(dir string, file string) string { + t.Helper() + js, errText, code := ttscTypiaTestCapture(func() int { + return runTransform([]string{ + "--cwd", dir, + "--tsconfig", "tsconfig.json", + "--file", file, + "--output", "js", + }) + }) + if code != 0 { + t.Fatalf("user global native identity transform %s/%s failed: code=%d stderr=\n%s", dir, file, code, errText) + } + return js + } + + js := transform(project, "src/input.ts") + schemasJS := transform(project, "src/schemas.ts") + controlsJS := transform(project, "src/controls.ts") + aliasJS := transform(aliasProject, "src/input.ts") + providedJS := transform(providedProject, "src/input.ts") + + failures := []string{} + for _, native := range []string{"instanceof File", "instanceof Blob"} { + for name, emit := range map[string]string{ + "validator emit": js, + "schema emit": schemasJS, + "type alias emit": aliasJS, + "near-miss control": controlsJS, + } { + if strings.Contains(emit, native) { + failures = append(failures, fmt.Sprintf("%s promoted a user-authored global to %q", name, native)) + } + } + } + for _, member := range []string{"userBlobBrand", "userFileBrand"} { + for name, emit := range map[string]string{ + "validator emit": js, + "schema emit": schemasJS, + "type alias emit": aliasJS, + } { + if !strings.Contains(emit, member) { + failures = append(failures, fmt.Sprintf("%s dropped user-declared member %q", name, member)) + } + } + } + for _, member := range []string{"intersectionBrand", "unionControl", "nestedHolder"} { + if !strings.Contains(js, member) { + failures = append(failures, fmt.Sprintf("validator emit dropped composition member %q", member)) + } + } + for _, member := range []string{"nearMissBrand", "prefixBrand"} { + if !strings.Contains(controlsJS, member) { + failures = append(failures, fmt.Sprintf("near-miss control emit dropped member %q", member)) + } + } + for _, kept := range []string{ + "instanceof Date", + "instanceof RegExp", + "instanceof Uint8Array", + "instanceof Map", + "instanceof Set", + "instanceof WeakMap", + "instanceof WeakSet", + "instanceof File", + "instanceof Blob", + } { + if !strings.Contains(providedJS, kept) { + failures = append(failures, fmt.Sprintf("runtime-provided native lost %q", kept)) + } + } + + output, runtimeErr := userGlobalNativeIdentityRun(t, project, js, controlsJS) + if runtimeErr != nil { + failures = append(failures, fmt.Sprintf("user global runtime matrix failed: %v\n%s", runtimeErr, output)) + } + // Counted from the runner's own table, which is the ground truth: 11 rows for + // the is/assert/assertGuard/validate/equals/random matrix, 15 for the + // createIs forms across direct, alias, re-export, branded intersection, union, + // and nested positions, and 2 near-miss name controls. The count is asserted + // rather than the exit code so a row that silently stops running cannot pass + // as a row that ran and agreed. + if expected := "RAN 28 USER GLOBAL CASES"; !strings.Contains(output, expected) { + failures = append(failures, fmt.Sprintf("user global runner did not report %q; got:\n%s", expected, output)) + } + providedOutput, providedErr := userGlobalNativeIdentityProvidedRun(t, providedProject, providedJS) + if providedErr != nil { + failures = append(failures, fmt.Sprintf("runtime-provided matrix failed: %v\n%s", providedErr, providedOutput)) + } + if expected := "RAN 18 PROVIDED CASES"; !strings.Contains(providedOutput, expected) { + failures = append(failures, fmt.Sprintf("provided runner did not report %q; got:\n%s", expected, providedOutput)) + } + + if len(failures) != 0 { + t.Fatalf( + "user global native identity mismatches:\n%s\n\nvalidator emit:\n%s\n\nschema emit:\n%s\n\nalias emit:\n%s\n\ncontrol emit:\n%s\n\nprovided emit:\n%s", + strings.Join(failures, "\n"), + js, + schemasJS, + aliasJS, + controlsJS, + providedJS, + ) + } +} + +func userGlobalNativeIdentityWriteProject(t *testing.T, prefix string, files map[string]string) string { + t.Helper() + root := ttscTypiaTestRepoRoot(t) + base := filepath.Join(root, "packages", "typia", "native", ".tmp-ttsc-typia-tests") + if err := os.MkdirAll(base, 0o755); err != nil { + t.Fatalf("mkdir temp base: %v", err) + } + dir, err := os.MkdirTemp(base, prefix) + if err != nil { + t.Fatalf("create temp fixture: %v", err) + } + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + for name, content := range files { + path := filepath.Join(dir, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir fixture path %s: %v", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write fixture file %s: %v", path, err) + } + } + return dir +} + +func userGlobalNativeIdentityProject(t *testing.T) string { + t.Helper() + return userGlobalNativeIdentityWriteProject(t, "user-global-native-identity-", map[string]string{ + "tsconfig.json": userGlobalNativeIdentityTSConfig, + "src/globals.d.ts": userGlobalNativeIdentityGlobals, + "src/reexport.ts": userGlobalNativeIdentityReexport, + "src/input.ts": userGlobalNativeIdentitySource, + "src/schemas.ts": userGlobalNativeIdentitySchemaSource, + "src/controls.ts": userGlobalNativeIdentityControlSource, + }) +} + +func userGlobalNativeIdentityAliasProject(t *testing.T) string { + t.Helper() + return userGlobalNativeIdentityWriteProject(t, "user-global-native-identity-alias-", map[string]string{ + "tsconfig.json": userGlobalNativeIdentityTSConfig, + "src/globals.d.ts": userGlobalNativeIdentityAliasGlobals, + "src/input.ts": userGlobalNativeIdentityAliasSource, + }) +} + +func userGlobalNativeIdentityProvidedProject(t *testing.T) string { + t.Helper() + return userGlobalNativeIdentityWriteProject(t, "user-global-native-identity-provided-", map[string]string{ + "tsconfig.json": userGlobalNativeIdentityProvidedTSConfig, + "src/globals.d.ts": userGlobalNativeIdentityProvidedGlobals, + "src/input.ts": userGlobalNativeIdentityProvidedSource, + }) +} + +func userGlobalNativeIdentityRunner(t *testing.T, project string, modules map[string]string) (string, error) { + t.Helper() + node, err := exec.LookPath("node") + if err != nil { + t.Skip("node executable not found") + return "", nil + } + runtimeDir := filepath.Join(project, "runtime") + if err := os.MkdirAll(runtimeDir, 0o755); err != nil { + t.Fatalf("mkdir runtime dir: %v", err) + } + ttscTypiaTestWriteCommonRuntimeStubs(t, runtimeDir) + for name, content := range modules { + if err := os.WriteFile(filepath.Join(runtimeDir, name), []byte(content), 0o644); err != nil { + t.Fatalf("write runtime file %s: %v", name, err) + } + } + cmd := exec.Command(node, filepath.Join(runtimeDir, "run.cjs")) + cmd.Dir = runtimeDir + output, err := cmd.CombinedOutput() + return string(output), err +} + +func userGlobalNativeIdentityRun(t *testing.T, project string, js string, controlsJS string) (string, error) { + t.Helper() + return userGlobalNativeIdentityRunner(t, project, map[string]string{ + "input.cjs": ttscTypiaTestRewriteCommonJS(t, js), + "controls.cjs": ttscTypiaTestRewriteCommonJS(t, controlsJS), + "run.cjs": userGlobalNativeIdentityRuntimeRunner, + }) +} + +func userGlobalNativeIdentityProvidedRun(t *testing.T, project string, js string) (string, error) { + t.Helper() + return userGlobalNativeIdentityRunner(t, project, map[string]string{ + "input.cjs": ttscTypiaTestRewriteCommonJS(t, js), + "run.cjs": userGlobalNativeIdentityProvidedRuntimeRunner, + }) +} + +const userGlobalNativeIdentityTSConfig = `{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "types": [], + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} +` + +const userGlobalNativeIdentityProvidedTSConfig = `{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "types": ["node"], + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} +` + +// userGlobalNativeIdentityGlobals is the whole runtime-native declaration set of +// its project: neither the DOM libraries nor @types/node is loaded, so Blob and +// File exist only because this file declares them. Blob is the interface-only +// spelling and File is the class spelling, which additionally gives File a +// user-owned constructor binding — the one signal a runtime provider would +// otherwise be recognized by. +const userGlobalNativeIdentityGlobals = `export {}; + +declare global { + interface Blob { + userBlobBrand: string; + } + class File { + userBlobBrand: string; + userFileBrand: string; + } +} +` + +const userGlobalNativeIdentityReexport = `export type ReexportedFile = File; +` + +const userGlobalNativeIdentitySource = `import typia from "typia"; +import type { ReexportedFile } from "./reexport"; + +type FileAlias = File; +type BrandedFile = File & { intersectionBrand: string }; +type FileUnion = BrandedFile | { unionControl: boolean }; +type NestedFile = { nestedHolder: File }; + +export const isFile = (input: unknown): boolean => typia.is(input); +export const assertFile = (input: unknown): File => typia.assert(input); +export const assertGuardFile = (input: unknown): void => typia.assertGuard(input); +export const validateFile = (input: unknown) => typia.validate(input); +export const equalsFile = (input: unknown): boolean => typia.equals(input); +export const randomBlob = (): Blob => typia.random(); + +export const createdIsBlob = typia.createIs(); +export const createdIsFile = typia.createIs(); +export const createdIsAlias = typia.createIs(); +export const createdIsReexported = typia.createIs(); +export const createdIsBranded = typia.createIs(); +export const createdIsUnion = typia.createIs(); +export const createdIsNested = typia.createIs(); +` + +const userGlobalNativeIdentitySchemaSource = `import typia from "typia"; + +export const jsonSchema = typia.json.schema(); +export const jsonStringify = typia.json.createStringify(); +export const jsonIsStringify = typia.json.createIsStringify(); +export const protobufMessage = typia.protobuf.message(); +export const protobufEncode = typia.protobuf.createIsEncode(); +export const llmSchema = typia.llm.schema({}); +` + +const userGlobalNativeIdentityControlSource = `import typia from "typia"; + +interface FileEntry { + nearMissBrand: string; +} +interface Blobby { + prefixBrand: string; +} + +export const createdIsFileEntry = typia.createIs(); +export const createdIsBlobby = typia.createIs(); +` + +// userGlobalNativeIdentityAliasGlobals spells the same collision as a global +// `type` alias, whose symbol is an anonymous type literal rather than a named +// declaration. It is the negative twin that proves the classification is decided +// by declaration provenance rather than by the shape of the declaring node. +const userGlobalNativeIdentityAliasGlobals = `export {}; + +declare global { + type Blob = { userBlobBrand: string }; + type File = { userBlobBrand: string; userFileBrand: string }; +} +` + +const userGlobalNativeIdentityAliasSource = `import typia from "typia"; + +export const isAliasFile = (input: unknown): boolean => typia.is(input); +export const createdIsAliasBlob = typia.createIs(); +` + +// userGlobalNativeIdentityProvidedGlobals is the positive control: @types/node +// bridges the node:buffer constructors to the bare globals, and the user adds +// only an empty augmentation. Merging a declaration into a runtime-provided +// global must not change what provides that global. +const userGlobalNativeIdentityProvidedGlobals = `export {}; + +declare global { + interface Blob {} + interface File {} +} +` + +const userGlobalNativeIdentityProvidedSource = `import typia from "typia"; + +export const createdIsDate = typia.createIs(); +export const createdIsRegExp = typia.createIs(); +export const createdIsBytes = typia.createIs(); +export const createdIsMap = typia.createIs>(); +export const createdIsSet = typia.createIs>(); +export const createdIsWeakMap = typia.createIs>(); +export const createdIsWeakSet = typia.createIs>(); +export const createdIsFile = typia.createIs(); +export const createdIsBlob = typia.createIs(); +` + +const userGlobalNativeIdentityRuntimeRunner = `const validators = require("./input.cjs"); +const controls = require("./controls.cjs"); + +let ran = 0; +const failures = []; +const eq = (name, actual, expected) => { + ran += 1; + if (actual !== expected) { + failures.push(name + ": expected " + expected + " but got " + actual); + } +}; +const throws = (name, run) => { + ran += 1; + try { + run(); + failures.push(name + ": expected a throw but none happened"); + } catch {} +}; + +const userBlob = { userBlobBrand: "user" }; +const userFile = { userBlobBrand: "user", userFileBrand: "user" }; +const realBlob = new Blob(["x"]); +const realFile = new File(["x"], "x.txt"); + +eq("is user File", validators.isFile(userFile), true); +eq("is real File", validators.isFile(realFile), false); +eq("assert user File", validators.assertFile(userFile), userFile); +throws("assert real File", () => validators.assertFile(realFile)); +eq("assertGuard user File", validators.assertGuardFile(userFile), undefined); +throws("assertGuard real File", () => validators.assertGuardFile(realFile)); +eq("validate user File", validators.validateFile(userFile).success, true); +eq("validate real File", validators.validateFile(realFile).success, false); +eq("equals user File", validators.equalsFile(userFile), true); +eq("equals real File", validators.equalsFile(realFile), false); +eq("random Blob brand", typeof validators.randomBlob().userBlobBrand, "string"); + +eq("createIs user Blob", validators.createdIsBlob(userBlob), true); +eq("createIs real Blob", validators.createdIsBlob(realBlob), false); +eq("createIs user File", validators.createdIsFile(userFile), true); +eq("createIs real File", validators.createdIsFile(realFile), false); +eq("alias user File", validators.createdIsAlias(userFile), true); +eq("alias real File", validators.createdIsAlias(realFile), false); +eq("re-export user File", validators.createdIsReexported(userFile), true); +eq("re-export real File", validators.createdIsReexported(realFile), false); +eq("branded user File", validators.createdIsBranded({ ...userFile, intersectionBrand: "x" }), true); +eq("branded user File without brand", validators.createdIsBranded(userFile), false); +eq("union branded arm", validators.createdIsUnion({ ...userFile, intersectionBrand: "x" }), true); +eq("union control arm", validators.createdIsUnion({ unionControl: true }), true); +eq("union rejects real File", validators.createdIsUnion(realFile), false); +eq("nested user File", validators.createdIsNested({ nestedHolder: userFile }), true); +eq("nested real File", validators.createdIsNested({ nestedHolder: realFile }), false); + +eq("near-miss FileEntry", controls.createdIsFileEntry({ nearMissBrand: "x" }), true); +eq("prefix Blobby", controls.createdIsBlobby({ prefixBrand: "x" }), true); + +console.log("RAN " + ran + " USER GLOBAL CASES"); +if (failures.length !== 0) { + throw new Error("MISMATCHES:\n" + failures.join("\n")); +} +` + +const userGlobalNativeIdentityProvidedRuntimeRunner = `const validators = require("./input.cjs"); + +let ran = 0; +const failures = []; +const eq = (name, actual, expected) => { + ran += 1; + if (actual !== expected) { + failures.push(name + ": expected " + expected + " but got " + actual); + } +}; + +eq("Date real", validators.createdIsDate(new Date()), true); +eq("Date plain", validators.createdIsDate({}), false); +eq("RegExp real", validators.createdIsRegExp(/x/), true); +eq("RegExp plain", validators.createdIsRegExp({}), false); +eq("Uint8Array real", validators.createdIsBytes(new Uint8Array(1)), true); +eq("Uint8Array plain", validators.createdIsBytes({}), false); +eq("Map real", validators.createdIsMap(new Map([["x", 1]])), true); +eq("Map plain", validators.createdIsMap({}), false); +eq("Set real", validators.createdIsSet(new Set(["x"])), true); +eq("Set plain", validators.createdIsSet({}), false); +eq("WeakMap real", validators.createdIsWeakMap(new WeakMap()), true); +eq("WeakMap plain", validators.createdIsWeakMap({}), false); +eq("WeakSet real", validators.createdIsWeakSet(new WeakSet()), true); +eq("WeakSet plain", validators.createdIsWeakSet({}), false); +eq("File real", validators.createdIsFile(new File(["x"], "x.txt")), true); +eq("File plain", validators.createdIsFile({ name: "x.txt", size: 1 }), false); +eq("Blob real", validators.createdIsBlob(new Blob(["x"])), true); +eq("Blob plain", validators.createdIsBlob({ size: 1, type: "text/plain" }), false); + +console.log("RAN " + ran + " PROVIDED CASES"); +if (failures.length !== 0) { + throw new Error("MISMATCHES:\n" + failures.join("\n")); +} +` diff --git a/packages/typia/native/core/factories/internal/metadata/MetadataHelper.go b/packages/typia/native/core/factories/internal/metadata/MetadataHelper.go index 09b08e2326..8600d42877 100644 --- a/packages/typia/native/core/factories/internal/metadata/MetadataHelper.go +++ b/packages/typia/native/core/factories/internal/metadata/MetadataHelper.go @@ -326,16 +326,20 @@ func metadata_is_pure_function_interface( } // metadata_symbol_from_default_lib reports whether the symbol's first locatable -// declaration lives in a TypeScript default library file. Default library files -// are always named `lib..d.ts` regardless of the install directory, so -// the base name alone is a stable signal. +// declaration lives in a TypeScript default library file, judged by file name +// alone because its callers answer without a checker. Default library files are +// conventionally named `lib..d.ts`, which is enough for the JSDoc and +// global-`Function` questions that use this: both are about what a library +// declaration means, and neither grants a type an identity a lookalike could +// claim. Runtime-native identity does grant one, so it asks the program instead +// through schemametadata.MetadataDefaultLibrary_is. func metadata_symbol_from_default_lib(symbol *nativeast.Symbol) bool { for _, node := range metadata_node_declarations(symbol) { src := nativeast.GetSourceFileOfNode(node) if src == nil { continue } - return metadata_is_default_lib_file_name(src.FileName()) + return schemametadata.MetadataDefaultLibrary_isFileNamed(src.FileName()) } return false } @@ -353,51 +357,124 @@ func metadata_symbol_is_global_type(checker *nativechecker.Checker, symbol *nati return checker.ResolveName(name, nil, nativeast.SymbolFlagsType, false) == symbol } +// metadata_symbol_is_runtime_global_type reports whether a global name is the +// runtime built-in rather than a user-authored global of the same name. +// +// Global-table resolution alone answers a weaker question than the one native +// classification asks. `declare global { interface File { ... } }` in a project +// with neither DOM nor Node declarations also wins that lookup, so a +// resolution-only gate promotes a purely user-authored type to native identity: +// its members are erased, `input instanceof File` replaces them, and the check +// throws `ReferenceError` wherever no runtime provides that constructor (#2200). +// +// The added question is therefore provenance of the runtime constructor, not the +// existence of a declaration: a global qualifies only when one of the two +// authorities that actually describe a JavaScript runtime — a TypeScript default +// library, or Node's `node:buffer` / `buffer` core module — puts it in scope. +func metadata_symbol_is_runtime_global_type(checker *nativechecker.Checker, symbol *nativeast.Symbol, name string) bool { + return metadata_symbol_is_global_type(checker, symbol, name) && + metadata_symbol_has_runtime_provider(checker, symbol) +} + // metadata_symbol_is_runtime_native_type applies the shared identity boundary -// for simple runtime-native classes. Most natives are the exact symbol resolved -// from the checker's global type table. Node's Blob and File module exports are -// a second symbol for those same runtime constructors, so their declarations -// qualify through the value-bearing shape shared by the supported Node type -// definitions (#1568, #2239). +// for simple runtime-native classes. Most natives are the runtime-provided +// symbol resolved from the checker's global type table. Node's Blob and File +// module exports are a second symbol for those same runtime constructors, so +// their declarations qualify through the value-bearing shape shared by the +// supported Node type definitions (#1568, #2200, #2239). func metadata_symbol_is_runtime_native_type(checker *nativechecker.Checker, symbol *nativeast.Symbol, name string) bool { - return metadata_symbol_is_global_type(checker, symbol, name) || + return metadata_symbol_is_runtime_global_type(checker, symbol, name) || metadata_symbol_is_node_buffer_native_export(symbol, name) } -func metadata_symbol_is_node_buffer_native_export(symbol *nativeast.Symbol, name string) bool { - if symbol == nil || symbol.ValueDeclaration == nil || (name != "Blob" && name != "File") { +// metadata_symbol_has_runtime_provider reports whether a runtime authority is +// what puts this symbol's constructor in scope. +// +// A provider does not have to declare the interface itself. @types/node bridges +// its module classes to the bare globals with `interface Blob extends _Blob {}` +// plus `var Blob: ... typeof buffer.Blob` in an ordinary package `.d.ts`, so the +// interface declaration carries no runtime provenance at all while the value +// binding resolves straight into the ambient `node:buffer` module. Reading the +// constructor binding therefore recognizes that bridge — and every respelling of +// it, including the 18/20 `typeof NodeBlob` class alias and the 25 `typeof +// buffer.Blob` variable alias — without matching declaration text or package +// paths, which node_modules placement, custom typeRoots, symlinks, and +// virtualized package layouts all make unreliable. +// +// A user-authored global fails both halves: its interface lives in the user's +// own file, and any `declare var` it pairs with resolves to the user's own type +// literal rather than into a default library or the Node core module. +func metadata_symbol_has_runtime_provider(checker *nativechecker.Checker, symbol *nativeast.Symbol) bool { + if symbol == nil { return false } - declaration := symbol.ValueDeclaration - if declaration.Kind != nativeast.KindClassDeclaration && declaration.Kind != nativeast.KindVariableDeclaration { + for _, declaration := range metadata_node_declarations(symbol) { + if metadata_declaration_is_runtime_provided(checker, declaration) { + return true + } + } + if checker == nil || symbol.Flags&nativeast.SymbolFlagsValue == 0 { return false } - // @types/node 18/20 declare these constructor values as classes and 25 - // declares merged interface/var pairs. The value declaration and exact core - // module together distinguish those runtime constructors from structural - // same-name interfaces without depending on node_modules placement, custom - // typeRoots, symlink targets, or virtualized package paths. - for node := declaration; node != nil; node = node.Parent { - if node.Kind == nativeast.KindModuleDeclaration && node.Name() != nil { - module := strings.Trim(node.Name().Text(), "\"'") + constructor := checker.GetTypeOfSymbol(symbol) + if constructor == nil { + return false + } + for _, declaration := range metadata_node_declarations(constructor.Symbol()) { + if metadata_declaration_is_runtime_provided(checker, declaration) { + return true + } + } + return false +} + +// metadata_declaration_is_runtime_provided reports whether a declaration belongs +// to one of the two authorities that describe a runtime-native global: a +// TypeScript default library file, or the `node:buffer` / `buffer` core module +// that owns Node's Blob and File constructors. The default-library half comes +// from the program (see schemametadata.MetadataDefaultLibrary) rather than from +// the declaration's file name, which under `libReplacement` is an ordinary +// `node_modules/@typescript/lib-*/index.d.ts` and which any dependency may +// otherwise imitate. +func metadata_declaration_is_runtime_provided(checker *nativechecker.Checker, declaration *nativeast.Node) bool { + if declaration == nil { + return false + } + if schemametadata.MetadataDefaultLibrary_is(checker, nativeast.GetSourceFileOfNode(declaration)) { + return true + } + return metadata_node_in_node_buffer_module(declaration) +} + +func metadata_node_in_node_buffer_module(node *nativeast.Node) bool { + for current := node; current != nil; current = current.Parent { + if current.Kind == nativeast.KindModuleDeclaration && current.Name() != nil { + module := strings.Trim(current.Name().Text(), "\"'") if module == "node:buffer" || module == "buffer" { return true } } - if node.Kind == nativeast.KindSourceFile { - break + if current.Kind == nativeast.KindSourceFile { + return false } } return false } -func metadata_is_default_lib_file_name(fileName string) bool { - slash := strings.ReplaceAll(fileName, "\\", "/") - base := slash - if idx := strings.LastIndex(slash, "/"); idx >= 0 { - base = slash[idx+1:] +func metadata_symbol_is_node_buffer_native_export(symbol *nativeast.Symbol, name string) bool { + if symbol == nil || symbol.ValueDeclaration == nil || (name != "Blob" && name != "File") { + return false + } + declaration := symbol.ValueDeclaration + if declaration.Kind != nativeast.KindClassDeclaration && declaration.Kind != nativeast.KindVariableDeclaration { + return false } - return strings.HasPrefix(base, "lib.") && strings.HasSuffix(base, ".d.ts") + // @types/node 18/20 declare these constructor values as classes and 25 + // declares merged interface/var pairs. The value declaration and exact core + // module together distinguish those runtime constructors from structural + // same-name interfaces without depending on node_modules placement, custom + // typeRoots, symlink targets, or virtualized package paths. + return metadata_node_in_node_buffer_module(declaration) } func metadata_is_internal(symbol *nativeast.Symbol) bool { diff --git a/packages/typia/native/core/factories/internal/metadata/iterate_metadata_map.go b/packages/typia/native/core/factories/internal/metadata/iterate_metadata_map.go index ca23c212bb..b6b777b914 100644 --- a/packages/typia/native/core/factories/internal/metadata/iterate_metadata_map.go +++ b/packages/typia/native/core/factories/internal/metadata/iterate_metadata_map.go @@ -10,9 +10,9 @@ func Iterate_metadata_map(props IMetadataIteratorProps) bool { if metadata_type_symbol_base_name(typ) != "Map" { return false } - // The native Map is the checker-resolved global type, not any same-named - // package declaration (#2212, #2239). - if !metadata_symbol_is_global_type(props.Checker, typ.Symbol(), "Map") { + // The native Map is the runtime-provided global type, not any same-named + // package declaration or user-authored global (#2200, #2212, #2239). + if !metadata_symbol_is_runtime_global_type(props.Checker, typ.Symbol(), "Map") { return false } generic := metadata_get_type_arguments(props.Checker, typ) diff --git a/packages/typia/native/core/factories/internal/metadata/iterate_metadata_native.go b/packages/typia/native/core/factories/internal/metadata/iterate_metadata_native.go index 1e7c3ccac2..8786328d8f 100644 --- a/packages/typia/native/core/factories/internal/metadata/iterate_metadata_native.go +++ b/packages/typia/native/core/factories/internal/metadata/iterate_metadata_native.go @@ -24,12 +24,14 @@ func Iterate_metadata_native(props IMetadataIteratorProps) bool { name = iterate_metadata_native_getNativeName(name) if _, ok := iterate_metadata_native_simples[name]; ok { // Match the built-in only when the checker resolves this exact symbol from - // the global type table. A colliding package declaration then falls through - // to its structural members regardless of whether it was authored in `.ts` - // or published through `.d.ts` (#2200, #2239). Node's authoritative Blob - // and File module exports are distinct symbols for the same runtime globals, - // so admit only their value-bearing exact core-module declaration shape as - // the second identity path (#1568). + // the global type table *and* a runtime authority provides its constructor. + // A colliding package declaration then falls through to its structural + // members regardless of whether it was authored in `.ts` or published + // through `.d.ts` (#2239), and so does a purely user-authored global that + // merely wins the same global lookup (#2200). Node's authoritative Blob and + // File module exports are distinct symbols for the same runtime globals, so + // admit only their value-bearing exact core-module declaration shape as the + // second identity path (#1568). if !metadata_symbol_is_runtime_native_type(props.Checker, symbol, name) { return false } @@ -38,9 +40,9 @@ func Iterate_metadata_native(props IMetadataIteratorProps) bool { } for _, generic := range iterate_metadata_native_generics { if name == generic.Name || strings.HasPrefix(name, generic.Name+"<") { - // Use the same global-symbol identity gate for WeakMap/WeakSet. The + // Use the same runtime-provenance identity gate for WeakMap/WeakSet. The // `+"<"` arity guard (#2181) and generic metadata remain unchanged. - if !metadata_symbol_is_global_type(props.Checker, symbol, generic.Name) { + if !metadata_symbol_is_runtime_global_type(props.Checker, symbol, generic.Name) { return false } iterate_metadata_native_take(props.Metadata, generic.Name) diff --git a/packages/typia/native/core/factories/internal/metadata/iterate_metadata_set.go b/packages/typia/native/core/factories/internal/metadata/iterate_metadata_set.go index c59bd7182e..2d10a5e210 100644 --- a/packages/typia/native/core/factories/internal/metadata/iterate_metadata_set.go +++ b/packages/typia/native/core/factories/internal/metadata/iterate_metadata_set.go @@ -10,9 +10,9 @@ func Iterate_metadata_set(props IMetadataIteratorProps) bool { if metadata_type_symbol_base_name(typ) != "Set" { return false } - // The native Set is the checker-resolved global type, not any same-named - // package declaration (#2212, #2239). - if !metadata_symbol_is_global_type(props.Checker, typ.Symbol(), "Set") { + // The native Set is the runtime-provided global type, not any same-named + // package declaration or user-authored global (#2200, #2212, #2239). + if !metadata_symbol_is_runtime_global_type(props.Checker, typ.Symbol(), "Set") { return false } generic := metadata_get_type_arguments(props.Checker, typ) diff --git a/packages/typia/native/core/schemas/metadata/MetadataDefaultLibrary.go b/packages/typia/native/core/schemas/metadata/MetadataDefaultLibrary.go new file mode 100644 index 0000000000..561ccc4a97 --- /dev/null +++ b/packages/typia/native/core/schemas/metadata/MetadataDefaultLibrary.go @@ -0,0 +1,81 @@ +package metadata + +import ( + "strings" + "sync" + + nativeast "github.com/microsoft/typescript-go/shim/ast" + nativechecker "github.com/microsoft/typescript-go/shim/checker" +) + +// MetadataDefaultLibrary routes the compiler's own default-library +// classification to metadata analysis, which decides runtime-native identity +// from it: a global is the JavaScript built-in only when a runtime authority — +// a TypeScript default library, or Node's `buffer` core module — declares it +// (#2200). +// +// Only the program knows which files those are. `bundled:///libs/lib.es2022.d.ts` +// under the embedded default, an on-disk path under `noembed`, and +// `node_modules/@typescript/lib-es2022/index.d.ts` under `libReplacement` are +// all the same authority, and none of them is recognizable from the file name: +// the last one does not even start with `lib.`, while `lib.d.ts` is an ordinary +// published file name that any dependency or `typeRoots` package may use +// (samchon/typia#2108 recorded the same lesson for dependency reporting). +// +// The registry is keyed by checker for the same reason MetadataDependency is: +// analysis code deep inside the metadata iterators holds the checker but no +// transform context, while one transform host drives one program at a time. A +// sync.Map keeps unrelated programs in the same process (tests) isolated. +// +// When no classifier is registered the file name is the only evidence left, so +// the fallback is the historical `lib.*.d.ts` base-name test. That keeps a host +// that never registers on exactly the behavior this package had before the +// registry existed rather than silently classifying every real native as a +// user declaration. +var metadataDefaultLibrary_classifiers sync.Map + +// MetadataDefaultLibrary_register records the program-level classifier for +// `checker`. Register it once per loaded program, before any transform runs. +func MetadataDefaultLibrary_register(checker *nativechecker.Checker, classifier func(*nativeast.SourceFile) bool) { + if checker == nil || classifier == nil { + return + } + metadataDefaultLibrary_classifiers.Store(checker, classifier) +} + +// MetadataDefaultLibrary_release removes the classifier registered for +// `checker`. +func MetadataDefaultLibrary_release(checker *nativechecker.Checker) { + if checker == nil { + return + } + metadataDefaultLibrary_classifiers.Delete(checker) +} + +// MetadataDefaultLibrary_is reports whether `source` is one of the program's +// default library files. +func MetadataDefaultLibrary_is(checker *nativechecker.Checker, source *nativeast.SourceFile) bool { + if source == nil { + return false + } + if checker != nil { + if stored, ok := metadataDefaultLibrary_classifiers.Load(checker); ok { + if classifier, ok := stored.(func(*nativeast.SourceFile) bool); ok { + return classifier(source) + } + } + } + return MetadataDefaultLibrary_isFileNamed(source.FileName()) +} + +// MetadataDefaultLibrary_isFileNamed is the registry's file-name fallback, +// exported so the one predicate that still answers without a checker keeps +// using the same rule. +func MetadataDefaultLibrary_isFileNamed(fileName string) bool { + slash := strings.ReplaceAll(fileName, "\\", "/") + base := slash + if index := strings.LastIndex(slash, "/"); index >= 0 { + base = slash[index+1:] + } + return strings.HasPrefix(base, "lib.") && strings.HasSuffix(base, ".d.ts") +}