diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 5f6afa0343..989f9b1207 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -207,7 +207,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Or_pattern_type_clash` | ✓ | `or_pattern_type_clash.res` | | | `Multiply_bound_variable` | ✓ | `multiply_bound_variable.res` | | | `Orpat_vars` | ✓ | `orpat_vars_unbalanced.res` | | -| `Expr_type_clash` | ✓ | many `*.res` | Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures, `object_literal_for_poly_field.res` (object literal against a polymorphic field annotation). | +| `Expr_type_clash` | ✓ | many `*.res` | Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures (including `jsx_preserve_external_function.res` for a plain-function external in a JSX component position), `object_literal_for_poly_field.res` (object literal against a polymorphic field annotation). | | `Apply_non_function` | ✓ | `apply_non_function.res` | | | `Apply_wrong_label` | ✓ | `apply_wrong_label.res` | | | `Label_multiply_defined` | ✓ | `label_multiply_defined_literal.res` | | diff --git a/tests/build_tests/jsx_preserve_external/input.js b/tests/build_tests/jsx_preserve_external/input.js new file mode 100644 index 0000000000..a1e70d13ee --- /dev/null +++ b/tests/build_tests/jsx_preserve_external/input.js @@ -0,0 +1,15 @@ +// @ts-check + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { setup } from "#dev/process"; + +const { execBuildOrThrow, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuildOrThrow(); + +const output = readFileSync(new URL("src/Test.jsx", import.meta.url), "utf8"); +assert.match(output, /import \* as SomeLib from "some-lib"/); +assert.match(output, /\s*\{\}\s*<\/SomeLib\.Head>/); +assert.doesNotMatch(output, /=>/); diff --git a/tests/build_tests/jsx_preserve_external/rescript.json b/tests/build_tests/jsx_preserve_external/rescript.json new file mode 100644 index 0000000000..5b5ae53232 --- /dev/null +++ b/tests/build_tests/jsx_preserve_external/rescript.json @@ -0,0 +1,14 @@ +{ + "name": "jsx-preserve-external", + "sources": ["src"], + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".jsx" + }, + "jsx": { + "module": "Preact", + "preserve": true + }, + "compiler-flags": ["-check-lam"] +} diff --git a/tests/build_tests/jsx_preserve_external/src/Preact.res b/tests/build_tests/jsx_preserve_external/src/Preact.res new file mode 100644 index 0000000000..71aed5121f --- /dev/null +++ b/tests/build_tests/jsx_preserve_external/src/Preact.res @@ -0,0 +1,23 @@ +type element = Jsx.element +// Custom JSX bindings must use the abstract component type, as React does. +// A function alias here lets function-style externals through and can produce +// invalid preserved JSX, as in the original bindings from issue #8047. +type component<'props> = Jsx.component<'props> + +@module("preact/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" + +@module("preact/jsx-runtime") +external jsxs: (component<'props>, 'props) => element = "jsxs" + +type domProps = {children?: element} + +module Elements = { + external someElement: element => option = "%identity" + + @module("preact/jsx-runtime") + external jsx: (string, domProps) => element = "jsx" + + @module("preact/jsx-runtime") + external jsxs: (string, domProps) => element = "jsxs" +} diff --git a/tests/build_tests/jsx_preserve_external/src/Test.res b/tests/build_tests/jsx_preserve_external/src/Test.res new file mode 100644 index 0000000000..afa51b2f11 --- /dev/null +++ b/tests/build_tests/jsx_preserve_external/src/Test.res @@ -0,0 +1,13 @@ +// Regression for https://github.com/rescript-lang/rescript/issues/8047. +// External components must remain imported values, without function wrappers. +module Head = { + type props = {children?: Preact.element} + + @module("some-lib") + external make: Preact.component = "Head" +} + +let test = + +
+ diff --git a/tests/build_tests/super_errors/expected/jsx_preserve_external_function.res.expected b/tests/build_tests/super_errors/expected/jsx_preserve_external_function.res.expected new file mode 100644 index 0000000000..cae450b6a9 --- /dev/null +++ b/tests/build_tests/super_errors/expected/jsx_preserve_external_function.res.expected @@ -0,0 +1,20 @@ + + We've found a bug for you! + /.../fixtures/jsx_preserve_external_function.res:30:4-7 + + 28 │ + 29 │ let test = + 30 │ <Head> + 31 │
+ 32 │ + + This JSX tag has type: Head.props => Preact.element + But JSX component positions require: + Preact.component<'a> (defined as Jsx.component<'a>) + + JSX tags must be React components, not plain functions. + + Possible solutions: + - If this function takes labeled props, annotate it with @react.component + - If this function takes a single props record, annotate it with @react.componentWithProps + - If this is already a valid component-like value, wrap it with React.component(...) \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/jsx_preserve_external_function.res b/tests/build_tests/super_errors/fixtures/jsx_preserve_external_function.res new file mode 100644 index 0000000000..c56d7528e7 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/jsx_preserve_external_function.res @@ -0,0 +1,32 @@ +@@config({flags: ["-bs-jsx-preserve"]}) +@@jsxConfig({version: 4, module_: "Preact"}) + +// With abstract components, the function-style external from #8047 is rejected. +module Preact = { + type element = Jsx.element + type component<'props> = Jsx.component<'props> + + @module("preact/jsx-runtime") + external jsx: (component<'props>, 'props) => element = "jsx" + + type domProps = {children?: element} + + module Elements = { + external someElement: element => option = "%identity" + + @module("preact/jsx-runtime") + external jsx: (string, domProps) => element = "jsx" + } +} + +module Head = { + type props = {children?: Preact.element} + + @module("some-lib") + external make: props => Preact.element = "Head" +} + +let test = + +
+