Skip to content

Commit 764474f

Browse files
committed
exploration for
1 parent e3256ba commit 764474f

24 files changed

+364
-29
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ test-syntax-roundtrip:
4343
test-gentype:
4444
make -C tests/gentype_tests/typescript-react-example clean test
4545
make -C tests/gentype_tests/stdlib-no-shims clean test
46+
make -C tests/gentype_tests/genimport-single clean test
4647

4748
test-rewatch:
4849
./rewatch/tests/suite-ci.sh
@@ -82,6 +83,7 @@ checkformat:
8283
clean-gentype:
8384
make -C tests/gentype_tests/typescript-react-example clean
8485
make -C tests/gentype_tests/stdlib-no-shims clean
86+
make -C tests/gentype_tests/genimport-single clean
8587

8688
clean-rewatch:
8789
cargo clean --manifest-path rewatch/Cargo.toml && rm -f rewatch/rewatch

compiler/gentype/EmitJs.ml

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ let propagate_annotation_to_sub_types ~code_items
601601
in
602602
(new_type_map, !annotated_set)
603603

604-
let emit_translation_as_string ~config ~file_name
604+
let emit_translation_as_string ~(config : Config.t) ~file_name
605605
~input_cmt_translate_type_declarations ~output_file_relative ~resolver
606606
(translation : Translation.t) =
607607
let initial_env =
@@ -635,6 +635,17 @@ let emit_translation_as_string ~config ~file_name
635635
|> List.map (fun (type_declaration : CodeItem.type_declaration) ->
636636
type_declaration.export_from_type_declaration)
637637
in
638+
(* Determine if we need to emit the helper alias for $GenTypeImport. *)
639+
let needs_gentype_import_helper =
640+
export_from_type_declarations
641+
|> List.exists
642+
(fun
643+
({CodeItem.export_type} : CodeItem.export_from_type_declaration) ->
644+
match export_type.type_ with
645+
| Ident {name; _} when String.equal name GentypeImportHelper.name ->
646+
true
647+
| _ -> false)
648+
in
638649
let type_name_is_interface ~env =
639650
type_name_is_interface ~export_type_map
640651
~export_type_map_from_other_files:env.export_type_map_from_other_files
@@ -648,8 +659,33 @@ let emit_translation_as_string ~config ~file_name
648659
and env = initial_env in
649660
let env, emitters =
650661
(* imports from type declarations go first to build up type tables *)
651-
import_types_from_type_declarations @ translation.import_types
652-
|> List.sort_uniq Translation.import_type_compare
662+
let all_import_types =
663+
import_types_from_type_declarations @ translation.import_types
664+
|> List.sort_uniq Translation.import_type_compare
665+
in
666+
(* Prefer direct imports of an alias name over aliased imports from other modules.
667+
Use a single pass map: keep first direct (no alias) for a given name,
668+
otherwise keep the first seen. *)
669+
let chosen_by_name =
670+
List.fold_left
671+
(fun acc (it : CodeItem.import_type) ->
672+
let key =
673+
match it.as_type_name with
674+
| Some alias -> alias
675+
| None -> it.type_name
676+
in
677+
match StringMap.find key acc with
678+
| (prev : CodeItem.import_type) -> (
679+
match (prev.as_type_name, it.as_type_name) with
680+
| None, Some _ -> acc (* keep direct over aliased *)
681+
| _ -> acc)
682+
| exception Not_found -> StringMap.add key it acc)
683+
StringMap.empty all_import_types
684+
in
685+
let filtered_import_types =
686+
chosen_by_name |> StringMap.to_seq |> Seq.map snd |> List.of_seq
687+
in
688+
filtered_import_types
653689
|> emit_import_types ~config ~emitters ~env
654690
~input_cmt_translate_type_declarations ~output_file_relative ~resolver
655691
~type_name_is_interface
@@ -702,6 +738,12 @@ let emit_translation_as_string ~config ~file_name
702738
module_items_emitter
703739
|> ExportModule.emit_all_module_items ~config ~emitters ~file_name
704740
in
741+
(* If we used the $GenTypeImport wrapper, emit its helper alias early. *)
742+
let emitters =
743+
if needs_gentype_import_helper then
744+
Emitters.export_early ~emitters GentypeImportHelper.alias
745+
else emitters
746+
in
705747
emitters
706748
|> emit_requires ~imported_value_or_component:false ~early:true ~config
707749
~requires:final_env.requires_early
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
let name = "$GenTypeImport"
2+
3+
let alias = "type $GenTypeImport<Expected, T extends Expected> = T;"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
open GenTypeCommon
2+
3+
let rec fold f acc (t : type_) =
4+
let acc = f acc t in
5+
match t with
6+
| Ident _ | TypeVar _ -> acc
7+
| Array (t1, _) | Dict t1 | Option t1 | Null t1 | Nullable t1 | Promise t1 ->
8+
fold f acc t1
9+
| Tuple ts -> List.fold_left (fold f) acc ts
10+
| Object (_shape, fields) ->
11+
List.fold_left (fun acc {type_} -> fold f acc type_) acc fields
12+
| Function {arg_types; ret_type; _} ->
13+
let acc =
14+
List.fold_left (fun acc {a_type} -> fold f acc a_type) acc arg_types
15+
in
16+
fold f acc ret_type
17+
| Variant {inherits; payloads; _} ->
18+
let acc = List.fold_left (fold f) acc inherits in
19+
List.fold_left (fun acc {t} -> fold f acc t) acc payloads

compiler/gentype/TranslateTypeDeclarations.ml

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,24 +151,61 @@ let traslate_declaration_kind ~config ~loc ~output_file_relative ~resolver
151151
| Some as_string -> (as_string, "$$" ^ name_with_module_path)
152152
| None -> (name_with_module_path, "$$" ^ name_with_module_path)
153153
in
154-
let import_types =
155-
[
156-
{
157-
CodeItem.type_name;
158-
as_type_name = Some as_type_name;
159-
import_path = import_string |> ImportPath.from_string_unsafe;
160-
};
161-
]
154+
let import_path = import_string |> ImportPath.from_string_unsafe in
155+
let base_import =
156+
{CodeItem.type_name; as_type_name = Some as_type_name; import_path}
157+
in
158+
(* If the declaration has a manifest type, capture its full translation
159+
so we can both build the Expected type and import its dependencies. *)
160+
let expected_translation_opt =
161+
match declaration_kind with
162+
| GeneralDeclaration (Some core_type) ->
163+
Some
164+
(core_type |> TranslateCoreType.translate_core_type ~config ~type_env)
165+
| GeneralDeclarationFromTypes (Some type_expr) ->
166+
Some
167+
(type_expr
168+
|> TranslateTypeExprFromTypes.translate_type_expr_from_types ~config
169+
~type_env)
170+
| _ -> None
171+
in
172+
173+
(* Import any referenced non-builtin identifiers from the same module as
174+
the base imported type. This ensures shims work (e.g. mapping ReactEvent
175+
types to a local shim module), matching previous behavior. *)
176+
let extra_type_imports =
177+
match expected_translation_opt with
178+
| Some tr ->
179+
GentypeTypeFold.fold
180+
(fun acc (t : GenTypeCommon.type_) ->
181+
match t with
182+
| Ident {builtin = false; name; _} -> name :: acc
183+
| _ -> acc)
184+
[] tr.type_
185+
|> List.sort_uniq String.compare
186+
|> List.filter (fun n -> n <> type_name)
187+
|> List.map (fun n ->
188+
{CodeItem.type_name = n; as_type_name = None; import_path})
189+
| None -> []
162190
in
191+
let import_types = base_import :: extra_type_imports in
163192
let export_from_type_declaration =
164193
(* Make the imported type usable from other modules by exporting it too. *)
194+
let imported_ident =
195+
as_type_name
196+
|> ident ~type_args:(type_vars |> List.map (fun s -> TypeVar s))
197+
in
198+
let export_type_body =
199+
match expected_translation_opt with
200+
| Some tr ->
201+
(* $GenTypeImport<Expected, Imported> *)
202+
ident GentypeImportHelper.name ~type_args:[tr.type_; imported_ident]
203+
| None -> imported_ident
204+
in
165205
typeName_
166206
|> create_export_type_from_type_declaration ~doc_string
167207
~annotation:GenType ~loc ~name_as:None ~opaque:(Some false)
168-
~type_:
169-
(as_type_name
170-
|> ident ~type_args:(type_vars |> List.map (fun s -> TypeVar s)))
171-
~type_env ~type_vars
208+
~type_:export_type_body ~type_env ~type_vars
172209
in
173210
[{CodeItem.import_types; export_from_type_declaration}]
174211
| (GeneralDeclarationFromTypes None | GeneralDeclaration None), None ->
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
SHELL = /bin/bash
2+
3+
test:
4+
yarn workspaces focus @tests/gentype-genimport-single
5+
yarn build
6+
# Helper exists once in single generated file
7+
grep -Fq 'type $$GenTypeImport<Expected, T extends Expected> = T;' src/GenTypeImportExpectedErrors.gen.tsx
8+
# Wrapper lines for each case present
9+
grep -Fq 'export type numberT = $$GenTypeImport<number,' src/GenTypeImportExpectedErrors.gen.tsx
10+
grep -Fq 'export type tupleT = $$GenTypeImport<[number, string],' src/GenTypeImportExpectedErrors.gen.tsx
11+
grep -Fq 'export type arrayT = $$GenTypeImport<number[],' src/GenTypeImportExpectedErrors.gen.tsx
12+
grep -Fq 'export type promiseT = $$GenTypeImport<Promise<number>,' src/GenTypeImportExpectedErrors.gen.tsx
13+
# nested arrays may render as Array<number[]>; accept either style
14+
grep -Fq 'export type nestedArrayT = $$GenTypeImport<Array<number[]>' src/GenTypeImportExpectedErrors.gen.tsx
15+
# Positive wrapper present too
16+
grep -Fq 'export type stringT = $$GenTypeImport<string,' src/GenTypeImportExpectedErrors.gen.tsx
17+
# Now TypeScript typecheck and assert exact errors
18+
yarn typecheck > ts-errors.txt 2>&1 || true
19+
# Expect exactly 5 TS2344 errors (the mismatches), and no others
20+
test $$(grep -c 'error TS2344' ts-errors.txt) -eq 5
21+
grep -Fq "Type 'string' does not satisfy the constraint 'number'." ts-errors.txt
22+
grep -Fq "Type 'string' does not satisfy the constraint '[number, string]'." ts-errors.txt
23+
grep -Fq "Type 'string' does not satisfy the constraint 'number[]'." ts-errors.txt
24+
grep -Fq "Type 'string' does not satisfy the constraint 'Promise<number>'." ts-errors.txt
25+
grep -Fq "Type 'string' does not satisfy the constraint 'number[][]'." ts-errors.txt
26+
# Positive should not produce any error about constraint 'string'
27+
if grep -Fq "constraint 'string'." ts-errors.txt; then \
28+
echo 'Unexpected error for positive string case' ; \
29+
exit 1 ; \
30+
else \
31+
echo 'All expected TS errors present; positive has no error' ; \
32+
fi
33+
34+
clean:
35+
yarn workspaces focus @tests/gentype-genimport-single
36+
yarn clean
37+
38+
.DEFAULT_GOAL := test
39+
40+
.PHONY: clean test
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@tests/gentype-genimport-single",
3+
"private": true,
4+
"scripts": {
5+
"build": "rescript legacy build",
6+
"clean": "rescript clean",
7+
"typecheck": "tsc"
8+
},
9+
"dependencies": {
10+
"rescript": "workspace:^"
11+
},
12+
"devDependencies": {
13+
"typescript": "5.8.2"
14+
}
15+
}
16+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"gentypeconfig": {
3+
"language": "typescript",
4+
"module": "esmodule",
5+
"importPath": "relative",
6+
"debug": { "all": false },
7+
"exportInterfaces": false
8+
},
9+
"name": "@tests/gentype-genimport-single",
10+
"sources": [
11+
{ "dir": "src", "subdirs": true }
12+
],
13+
"package-specs": {
14+
"module": "esmodule",
15+
"in-source": true
16+
},
17+
"suffix": ".res.js"
18+
}
19+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* TypeScript file generated from GenTypeImportExpectedErrors.res by genType. */
2+
3+
/* eslint-disable */
4+
/* tslint:disable */
5+
6+
type $GenTypeImport<Expected, T extends Expected> = T;
7+
8+
import * as GenTypeImportExpectedErrorsJS from './GenTypeImportExpectedErrors.res.js';
9+
10+
import type {Type as $$arrayT} from 'external-module';
11+
12+
import type {Type as $$nestedArrayT} from 'external-module';
13+
14+
import type {Type as $$numberT} from 'external-module';
15+
16+
import type {Type as $$promiseT} from 'external-module';
17+
18+
import type {Type as $$stringT} from 'external-module';
19+
20+
import type {Type as $$tupleT} from 'external-module';
21+
22+
export type numberT = $GenTypeImport<number,$$numberT>;
23+
24+
export type tupleT = $GenTypeImport<[number, string],$$tupleT>;
25+
26+
export type arrayT = $GenTypeImport<number[],$$arrayT>;
27+
28+
export type promiseT = $GenTypeImport<Promise<number>,$$promiseT>;
29+
30+
export type nestedArrayT = $GenTypeImport<Array<number[]>,$$nestedArrayT>;
31+
32+
export type stringT = $GenTypeImport<string,$$stringT>;
33+
34+
export const useNumber: (x:numberT) => numberT = GenTypeImportExpectedErrorsJS.useNumber as any;
35+
36+
export const useTuple: (x:tupleT) => tupleT = GenTypeImportExpectedErrorsJS.useTuple as any;
37+
38+
export const useArray: (x:arrayT) => arrayT = GenTypeImportExpectedErrorsJS.useArray as any;
39+
40+
export const usePromise: (x:promiseT) => promiseT = GenTypeImportExpectedErrorsJS.usePromise as any;
41+
42+
export const useNestedArray: (x:nestedArrayT) => nestedArrayT = GenTypeImportExpectedErrorsJS.useNestedArray as any;
43+
44+
export const useString: (x:stringT) => stringT = GenTypeImportExpectedErrorsJS.useString as any;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* This module intentionally contains cases that should cause TypeScript errors
2+
when the external imported Type mismatches the ReScript manifest type. */
3+
4+
@genType.import(("external-module", "Type"))
5+
type numberT = int
6+
7+
@genType
8+
let useNumber = (x: numberT) => x
9+
10+
@genType.import(("external-module", "Type"))
11+
type tupleT = (int, string)
12+
13+
@genType
14+
let useTuple = (x: tupleT) => x
15+
16+
@genType.import(("external-module", "Type"))
17+
type arrayT = array<int>
18+
19+
@genType
20+
let useArray = (x: arrayT) => x
21+
22+
@genType.import(("external-module", "Type"))
23+
type promiseT = Js.Promise.t<int>
24+
25+
@genType
26+
let usePromise = (x: promiseT) => x
27+
28+
@genType.import(("external-module", "Type"))
29+
type nestedArrayT = array<array<int>>
30+
31+
@genType
32+
let useNestedArray = (x: nestedArrayT) => x
33+
34+
/* Positive case: string matches external Type=string */
35+
@genType.import(("external-module", "Type"))
36+
type stringT = string
37+
38+
@genType
39+
let useString = (x: stringT) => x

0 commit comments

Comments
 (0)