diff --git a/docs/design.md b/docs/design.md index d7465664..20ddb0be 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1521,53 +1521,53 @@ Automatic conversions at import boundary: - `T | undefined` → `Option` - `T | null | undefined` → `Option` - External `any` → `unknown` (forces narrowing) -- npm imports are callable directly by default (like `useState`, `clsx`) -- `throws` modifier marks imports whose functions may throw — calls are auto-wrapped in `Result` +- npm imports are **untrusted by default** — calls are auto-wrapped in `Result` +- `trusted` modifier marks imports that are safe to call directly (no Result wrapping) - Nullable/optional types at the boundary are converted to `Option` -#### Default npm imports +#### Default npm imports (untrusted) -npm imports work directly — no special syntax needed for safe functions: +npm imports are untrusted by default — calls are auto-wrapped in `Result`: ```floe -import { useState } from "react" -import { clsx } from "clsx" +import { parseYaml } from "yaml-lib" +import { fetchUser } from "api-client" -const [count, setCount] = useState(0) // direct call -const classes = clsx("btn", active) // direct call +const data = parseYaml(input) // Result — auto-wrapped +const user = fetchUser(id) // Result — auto-wrapped ``` -#### throws imports +#### trusted imports -For npm functions that may throw, mark them with `throws`. The compiler auto-wraps calls in `Result`: +For npm functions known to be safe, mark them with `trusted`. The compiler allows direct calls without Result wrapping: ```floe +// Whole module: +import trusted { useState, useEffect } from "react" +useState(0) // direct call, no wrapping + // Per-function: -import { capitalize, throws fetchUser } from "some-ts-lib" +import { trusted capitalize, fetchUser } from "some-ts-lib" capitalize("hello") // string, direct call -fetchUser(id) // Result — auto-wrapped - -// Whole module: -import throws { parseYaml, dumpYaml } from "yaml-lib" -parseYaml(input) // Result — auto-wrapped +fetchUser(id) // Result — auto-wrapped (untrusted) ``` -#### throws with Result and ? +#### Untrusted imports with Result and ? -`throws` calls return `Result` automatically. Use `?` to unwrap: +Untrusted calls return `Result` automatically. Use `?` to unwrap: ```floe -// JSON.parse is stdlib — already returns Result, no throws needed +// JSON.parse is stdlib — already returns Result const result = JSON.parse(input) // result: Result -// throws is for external npm imports that might throw: -import throws { parseYaml } from "yaml-lib" +// npm imports are untrusted by default: +import { parseYaml } from "yaml-lib" const data = parseYaml(input) // data: Result -// Async throws: auto-awaits Promises and catches rejections -import throws { fetchUser } from "api-client" +// Async untrusted: auto-awaits Promises and catches rejections +import { fetchUser } from "api-client" const user = fetchUser(id) // user: Result — auto-awaited + wrapped @@ -1595,7 +1595,7 @@ match findElement("app") { #### Codegen ```typescript -// import throws { parseYaml } from "yaml-lib"; parseYaml(input) → +// import { parseYaml } from "yaml-lib"; parseYaml(input) → (untrusted, auto-wrapped) (() => { try { return { ok: true as const, value: parseYaml(input) }; } catch (_e) { return { ok: false as const, error: _e instanceof Error ? _e : new Error(String(_e)) }; } })() ``` @@ -1614,7 +1614,7 @@ Emits clean, readable `.tsx`. Zero runtime imports. | `Type.Variant` (qualified) | `{ tag: "Variant" }` (same as bare) | | `fn f(x: T) -> U { ... }` | `function f(x: T): U { ... }` | | `fn f(x: T) -> T { ... }` | `function f(x: T): T { ... }` | -| `throws` call (e.g. `parseYaml(input)`) | `(() => { try { return { ok: true, value: parseYaml(input) }; } catch (_e) { return { ok: false, error: _e instanceof Error ? _e : new Error(String(_e)) }; } })()` | +| untrusted npm call (e.g. `parseYaml(input)`) | `(() => { try { return { ok: true, value: parseYaml(input) }; } catch (_e) { return { ok: false, error: _e instanceof Error ? _e : new Error(String(_e)) }; } })()` | | `match x { A -> ..., B -> ... }` | `x.tag === "A" ? ... : x.tag === "B" ? ... : absurd(x)` | | `match x { A(v) when v > 0 -> ... }` | `x.tag === "A" ? (() => { const v = x.value; if (v > 0) { return ...; } ... })()` | | `match url { "/users/{id}" -> f(id) }` | `url.match(/^\/users\/([^/]+)$/) ? (() => { const _m = url.match(...); const id = _m![1]; return f(id); })() : ...` | diff --git a/docs/llms.txt b/docs/llms.txt index c9ae74a6..8ba9b8af 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -405,8 +405,8 @@ const fastest = Promise.race([fetchFromCDN(url), fetchFromOrigin(url)]) |> Promi const results = Promise.allSettled([fetchA(), fetchB()]) |> Promise.await // Array> Promise.delay(1000) |> Promise.await // wait 1 second -// throws imports: auto-wrapped in try/catch, return Result -import throws { npmAsyncFn } from "some-lib" +// npm imports are untrusted by default: auto-wrapped in try/catch, return Result +import { npmAsyncFn } from "some-lib" const result = npmAsyncFn() // Result — auto-wrapped, awaits + catches rejections // parse — compiler built-in for runtime type validation @@ -438,7 +438,7 @@ const vals = ICONS |> Record.values // Array → Object.values(I const has = Record.has(ICONS, key) // boolean → (key in ICONS) const n = ICONS |> Record.size // number → Object.keys(ICONS).length -// Http — pipe-friendly fetch with Result (no throws needed) +// Http — pipe-friendly fetch with Result (already returns Result natively) const data = Http.get("https://api.example.com/users") |> Promise.await? |> Http.json |> Promise.await? const result = Http.post(url, { name: "Alice" }) |> Promise.await? const body = Http.get(url) |> Promise.await? |> Http.text |> Promise.await? @@ -468,22 +468,22 @@ const iso = d |> Date.toIso // d.toISOString() // Standard import (Floe files) import { Todo, Filter } from "./types" -// npm imports — safe by default, call directly -import { useState, useEffect } from "react" -import { clsx } from "clsx" -const [count, setCount] = useState(0) // no special syntax needed +// npm imports — untrusted by default, auto-wrapped in Result +import { fetchUser } from "some-lib" +const user = fetchUser("id") // Result — auto-wrapped -// throws imports — for npm functions that may throw -import throws { parseYaml } from "yaml-lib" -const result = parseYaml(input) // auto-wrapped in Result +// trusted imports — safe to call directly, no wrapping +import trusted { useState, useEffect } from "react" +import { clsx } from "clsx" // untrusted by default +const [count, setCount] = useState(0) // direct call (trusted) -// Per-function throws -import { capitalize, throws fetchData } from "some-lib" -capitalize("hello") // direct call, no wrapping -const data = fetchData() // Result — auto-wrapped +// Per-function trusted +import { trusted capitalize, fetchData } from "some-lib" +capitalize("hello") // direct call, no wrapping (trusted) +const data = fetchData() // Result — auto-wrapped (untrusted) -// Use ? to unwrap throws results -const parsed = parseYaml(input)? // unwraps or returns Err early +// Use ? to unwrap untrusted results +const parsed = fetchData()? // unwraps or returns Err early // Importing for-block extensions import { for User } from "./user-helpers" @@ -674,7 +674,7 @@ test "addition" { ```floe // todo — placeholder, type never, warns at compile time fn processPayment(order: Order) -> Result { - todo // throws "not implemented" at runtime + todo // panics with "not implemented" at runtime } // unreachable — assert impossible, type never @@ -752,7 +752,7 @@ floe lsp # start language server (used by editors) 4. **`?` only works** inside functions returning `Result` or `Option` 5. **`==` is structural equality** — only between same types 6. **No bracket indexing** — use `Map.get(map, key)` for maps, `Record.get(obj, key)` for plain-object records, `Array.get(arr, i)` for arrays (all return `Option`). `obj[key]` is not valid Floe syntax -7. **npm imports that may throw** — mark with `import throws` to auto-wrap in `Result` +7. **npm imports are untrusted by default** — calls auto-wrap in `Result`; use `import trusted` for safe imports 8. **`self` is explicit** in for blocks — no implicit `this` 9. **No mutation** of function parameters 10. **Unused variables/imports are compile errors** (prefix with `_` to suppress) diff --git a/docs/site/src/content/docs/docs/guide/error-handling.md b/docs/site/src/content/docs/docs/guide/error-handling.md index d2a0df66..501ef856 100644 --- a/docs/site/src/content/docs/docs/guide/error-handling.md +++ b/docs/site/src/content/docs/docs/guide/error-handling.md @@ -161,11 +161,14 @@ The boundary wrapping also converts: - `T | undefined` to `Option` - `any` to `unknown` -For npm functions that may throw, use `throws` imports. Calls are auto-wrapped in `Result`: +npm imports are untrusted by default -- calls are auto-wrapped in `Result`. Use `trusted` to mark safe imports that can be called directly: ```floe -import throws { parseYaml } from "yaml-lib" -const data = parseYaml(input)? // Result, ? unwraps +import { parseYaml } from "yaml-lib" // untrusted (default) +const data = parseYaml(input)? // Result, ? unwraps + +import trusted { useState } from "react" // trusted = direct call +const [count, setCount] = useState(0) ``` This means npm libraries work transparently with Floe's type system. diff --git a/docs/site/src/content/docs/docs/guide/from-typescript.md b/docs/site/src/content/docs/docs/guide/from-typescript.md index 6a448e64..b7a928a1 100644 --- a/docs/site/src/content/docs/docs/guide/from-typescript.md +++ b/docs/site/src/content/docs/docs/guide/from-typescript.md @@ -23,7 +23,7 @@ Floe is designed to be familiar to TypeScript developers. | `let` / `const` | `const` only | No mutation | | `===` | `==` | `==` compiles to `===` | | `switch` | `match` | Exhaustive, no fall-through | -| `try/catch` | `throws` imports | `import throws { parseYaml } from "yaml-lib"` | +| `try/catch` | Untrusted imports (default) | `import { parseYaml } from "yaml-lib"` (auto-wrapped in Result) | | `{x && }` | `Option.map` | `{x \|> Option.map((v) => )}` | | `T \| null` | `Option` | `Some(value)` / `None` | | `throw` | `Result` | `Ok(value)` / `Err(error)` | diff --git a/docs/site/src/content/docs/docs/guide/llm-setup.md b/docs/site/src/content/docs/docs/guide/llm-setup.md index 08c1dad2..1d129607 100644 --- a/docs/site/src/content/docs/docs/guide/llm-setup.md +++ b/docs/site/src/content/docs/docs/guide/llm-setup.md @@ -65,5 +65,5 @@ curl -o llms.txt https://floe-lang.dev/llms.txt - Type system (records, unions, newtypes, opaque types, Result/Option) - Compilation rules (what Floe compiles to in TypeScript) - Standard library functions -- Import system (`throws` for error-wrapping, for-blocks) +- Import system (`trusted` for safe imports, untrusted by default, for-blocks) - Common pitfalls and rules diff --git a/docs/site/src/content/docs/docs/guide/tour.md b/docs/site/src/content/docs/docs/guide/tour.md index 40898bbf..45b744e7 100644 --- a/docs/site/src/content/docs/docs/guide/tour.md +++ b/docs/site/src/content/docs/docs/guide/tour.md @@ -109,9 +109,9 @@ export fn Counter() -> JSX.Element { ```floe import { Todo } from "./types" -import throws { parseYaml } from "yaml-lib" +import { parseYaml } from "yaml-lib" // untrusted by default const data = parseYaml(input)? // auto-wrapped in Result, ? unwraps -import { useState } from "react" // npm imports work directly +import trusted { useState } from "react" // trusted = direct call, no wrapping import { for Array } from "./helpers" // import for-block extensions fn fetchUser(id: string) -> Promise> { diff --git a/docs/site/src/content/docs/docs/guide/typescript-interop.md b/docs/site/src/content/docs/docs/guide/typescript-interop.md index ae122b1d..5abd2fc4 100644 --- a/docs/site/src/content/docs/docs/guide/typescript-interop.md +++ b/docs/site/src/content/docs/docs/guide/typescript-interop.md @@ -14,14 +14,14 @@ import { z } from "zod" import { clsx } from "clsx" ``` -The compiler reads `.d.ts` type definitions to understand the types of imported values. These imports are callable directly -- no special syntax needed. +The compiler reads `.d.ts` type definitions to understand the types of imported values. npm imports are **untrusted by default** -- calls are auto-wrapped in `Result`. -## `throws` imports +## Untrusted imports (default) -Some npm functions can throw exceptions at runtime (JSON parsers, API clients, file I/O). Mark these imports with `throws` so the compiler auto-wraps calls in `Result`: +All npm imports are untrusted by default. The compiler auto-wraps calls in `Result`: ```floe -import throws { parseYaml } from "yaml-lib" +import { parseYaml } from "yaml-lib" // parseYaml is auto-wrapped — returns Result const result = parseYaml(input) @@ -37,13 +37,23 @@ Use `?` to unwrap the result concisely: const data = parseYaml(input)? // unwraps or returns Err early ``` -You can mark individual functions as throwing from a module: +## `trusted` imports + +For npm functions known to be safe (like React hooks, utility libraries), mark them with `trusted` so they can be called directly without Result wrapping: + +```floe +import trusted { useState, useEffect } from "react" + +const [count, setCount] = useState(0) // direct call, no wrapping +``` + +You can mark individual functions as trusted from a module: ```floe -import { capitalize, throws fetchData } from "some-lib" +import { trusted capitalize, fetchData } from "some-lib" -capitalize("hello") // direct call, no wrapping -const data = fetchData() // Result — auto-wrapped +capitalize("hello") // direct call, no wrapping (trusted) +const data = fetchData() // Result — auto-wrapped (untrusted) ``` ## Bridge types (`=` syntax) diff --git a/docs/site/src/content/docs/docs/reference/stdlib/http.md b/docs/site/src/content/docs/docs/reference/stdlib/http.md index 8a8bcb11..2557067d 100644 --- a/docs/site/src/content/docs/docs/reference/stdlib/http.md +++ b/docs/site/src/content/docs/docs/reference/stdlib/http.md @@ -4,7 +4,7 @@ sidebar: order: 10 --- -Pipe-friendly HTTP functions that return `Promise>` natively. No `throws` import needed -- errors are captured automatically. +Pipe-friendly HTTP functions that return `Promise>` natively. As a stdlib module, errors are captured automatically. ## Functions diff --git a/docs/site/src/content/docs/docs/reference/stdlib/promise.md b/docs/site/src/content/docs/docs/reference/stdlib/promise.md index 2794ad81..d5ea7d36 100644 --- a/docs/site/src/content/docs/docs/reference/stdlib/promise.md +++ b/docs/site/src/content/docs/docs/reference/stdlib/promise.md @@ -33,13 +33,13 @@ fn fetchUser(id: string) -> Promise { The return type must explicitly use `Promise`, making async behavior visible to callers. -## `throws` with async functions +## Untrusted async imports -When a `throws` import returns a `Promise`, the auto-wrapping handles both sync throws and async rejections. The call is auto-awaited and wrapped in `Result`: +When an untrusted npm import returns a `Promise`, the auto-wrapping handles both sync throws and async rejections. The call is auto-awaited and wrapped in `Result`: ```floe -// npm async function that might reject -import throws { transitionIssue } from "jira-api" +// npm async function that might reject (untrusted by default) +import { transitionIssue } from "jira-api" const result = transitionIssue(id, tid) // Result<(), Error> — auto-awaited, rejections caught @@ -52,7 +52,8 @@ match result { | Tool | For | Does | |---|---|---| | `Promise.await` | Floe async functions | Unwrap `Promise>`, use `?` for errors | -| `throws` imports | npm functions that may throw | Auto-wrap calls in `Result` (auto-awaits if Promise) | +| Untrusted imports (default) | npm functions | Auto-wrap calls in `Result` (auto-awaits if Promise) | +| `trusted` imports | npm functions known to be safe | Direct calls, no wrapping | ## Examples diff --git a/docs/site/src/content/docs/docs/reference/syntax.md b/docs/site/src/content/docs/docs/reference/syntax.md index 17ebd5e1..388c7e4d 100644 --- a/docs/site/src/content/docs/docs/reference/syntax.md +++ b/docs/site/src/content/docs/docs/reference/syntax.md @@ -302,16 +302,16 @@ import { name } from "module" import { name as alias } from "module" import { a, b, c } from "module" -// npm imports work directly -import { useState } from "react" -const [count, setCount] = useState(0) +// npm imports are untrusted by default — auto-wrapped in Result +import { parseYaml } from "yaml-lib" +const result = parseYaml(input) // Result — auto-wrapped -// throws imports — for npm functions that may throw -import throws { parseYaml } from "yaml-lib" -const result = parseYaml(input) // auto-wrapped in Result +// trusted imports — safe to call directly, no wrapping +import trusted { useState } from "react" +const [count, setCount] = useState(0) -// Per-function throws -import { capitalize, throws fetchData } from "some-lib" +// Per-function trusted +import { trusted capitalize, fetchData } from "some-lib" // Import for-block functions by type import { for User } from "./helpers" diff --git a/docs/site/src/pages/playground.astro b/docs/site/src/pages/playground.astro index 1fe9919c..a29f9148 100644 --- a/docs/site/src/pages/playground.astro +++ b/docs/site/src/pages/playground.astro @@ -417,7 +417,7 @@ if (stream.match(/^[a-zA-Z_]\w*/)) { const w = stream.current(); - if (['fn','const','type','match','import','export','from','if','else','return','use','for','trait','opaque','throws','test','assert','pub','when'].includes(w)) return 'keyword'; + if (['fn','const','type','match','import','export','from','if','else','return','use','for','trait','opaque','trusted','test','assert','pub','when'].includes(w)) return 'keyword'; if (['true','false'].includes(w)) return 'atom'; if (['Ok','Err','Some','None','Result','Option','Array','Map','Set','string','number','boolean','JSX'].includes(w)) return 'typeName'; if (w[0] >= 'A' && w[0] <= 'Z') return 'typeName'; diff --git a/editors/neovim/queries/floe/highlights.scm b/editors/neovim/queries/floe/highlights.scm index 9b36dab2..c383feca 100644 --- a/editors/neovim/queries/floe/highlights.scm +++ b/editors/neovim/queries/floe/highlights.scm @@ -7,7 +7,7 @@ "import" @keyword "from" @keyword "export" @keyword -"throws" @keyword +"trusted" @keyword "for" @keyword "trait" @keyword "opaque" @keyword diff --git a/editors/tree-sitter-floe/grammar.js b/editors/tree-sitter-floe/grammar.js index 505ebb72..e8fd4eb0 100644 --- a/editors/tree-sitter-floe/grammar.js +++ b/editors/tree-sitter-floe/grammar.js @@ -67,7 +67,7 @@ module.exports = grammar({ import_declaration: ($) => seq( "import", - optional("throws"), + optional("trusted"), "{", commaSep1(choice($.import_specifier, $.import_for_specifier)), "}", @@ -76,7 +76,7 @@ module.exports = grammar({ ), import_specifier: ($) => - seq(optional("throws"), $.identifier, optional(seq("as", $.identifier))), + seq(optional("trusted"), $.identifier, optional(seq("as", $.identifier))), import_for_specifier: ($) => seq("for", field("type", choice($.type_identifier, $.identifier))), diff --git a/editors/tree-sitter-floe/queries/highlights.scm b/editors/tree-sitter-floe/queries/highlights.scm index 9b36dab2..c383feca 100644 --- a/editors/tree-sitter-floe/queries/highlights.scm +++ b/editors/tree-sitter-floe/queries/highlights.scm @@ -7,7 +7,7 @@ "import" @keyword "from" @keyword "export" @keyword -"throws" @keyword +"trusted" @keyword "for" @keyword "trait" @keyword "opaque" @keyword diff --git a/editors/tree-sitter-floe/src/grammar.json b/editors/tree-sitter-floe/src/grammar.json index 68563738..11d1017d 100644 --- a/editors/tree-sitter-floe/src/grammar.json +++ b/editors/tree-sitter-floe/src/grammar.json @@ -63,7 +63,7 @@ "members": [ { "type": "STRING", - "value": "throws" + "value": "trusted" }, { "type": "BLANK" @@ -139,7 +139,7 @@ "members": [ { "type": "STRING", - "value": "throws" + "value": "trusted" }, { "type": "BLANK" diff --git a/editors/tree-sitter-floe/src/node-types.json b/editors/tree-sitter-floe/src/node-types.json index 79e26a44..3321e126 100644 --- a/editors/tree-sitter-floe/src/node-types.json +++ b/editors/tree-sitter-floe/src/node-types.json @@ -5134,10 +5134,6 @@ "type": "test", "named": false }, - { - "type": "throws", - "named": false - }, { "type": "todo", "named": true @@ -5150,6 +5146,10 @@ "type": "true", "named": false }, + { + "type": "trusted", + "named": false + }, { "type": "type", "named": false diff --git a/editors/tree-sitter-floe/src/parser.c b/editors/tree-sitter-floe/src/parser.c index aee60979..aaea4d06 100644 --- a/editors/tree-sitter-floe/src/parser.c +++ b/editors/tree-sitter-floe/src/parser.c @@ -22,7 +22,7 @@ enum ts_symbol_identifiers { sym_identifier = 1, anon_sym_import = 2, - anon_sym_throws = 3, + anon_sym_trusted = 3, anon_sym_LBRACE = 4, anon_sym_COMMA = 5, anon_sym_RBRACE = 6, @@ -222,7 +222,7 @@ static const char * const ts_symbol_names[] = { [ts_builtin_sym_end] = "end", [sym_identifier] = "identifier", [anon_sym_import] = "import", - [anon_sym_throws] = "throws", + [anon_sym_trusted] = "trusted", [anon_sym_LBRACE] = "{", [anon_sym_COMMA] = ",", [anon_sym_RBRACE] = "}", @@ -422,7 +422,7 @@ static const TSSymbol ts_symbol_map[] = { [ts_builtin_sym_end] = ts_builtin_sym_end, [sym_identifier] = sym_identifier, [anon_sym_import] = anon_sym_import, - [anon_sym_throws] = anon_sym_throws, + [anon_sym_trusted] = anon_sym_trusted, [anon_sym_LBRACE] = anon_sym_LBRACE, [anon_sym_COMMA] = anon_sym_COMMA, [anon_sym_RBRACE] = anon_sym_RBRACE, @@ -631,7 +631,7 @@ static const TSSymbolMetadata ts_symbol_metadata[] = { .visible = true, .named = false, }, - [anon_sym_throws] = { + [anon_sym_trusted] = { .visible = true, .named = false, }, @@ -3846,158 +3846,158 @@ static bool ts_lex_keywords(TSLexer *lexer, TSStateId state) { END_STATE(); case 14: if (lookahead == 'e') ADVANCE(34); - if (lookahead == 'h') ADVANCE(35); - if (lookahead == 'o') ADVANCE(36); - if (lookahead == 'r') ADVANCE(37); - if (lookahead == 'y') ADVANCE(38); + if (lookahead == 'o') ADVANCE(35); + if (lookahead == 'r') ADVANCE(36); + if (lookahead == 'y') ADVANCE(37); END_STATE(); case 15: - if (lookahead == 'n') ADVANCE(39); + if (lookahead == 'n') ADVANCE(38); END_STATE(); case 16: - if (lookahead == 'h') ADVANCE(40); + if (lookahead == 'h') ADVANCE(39); END_STATE(); case 17: ACCEPT_TOKEN(anon_sym_as); - if (lookahead == 's') ADVANCE(41); + if (lookahead == 's') ADVANCE(40); END_STATE(); case 18: - if (lookahead == 'o') ADVANCE(42); + if (lookahead == 'o') ADVANCE(41); END_STATE(); case 19: - if (lookahead == 'l') ADVANCE(43); - if (lookahead == 'n') ADVANCE(44); + if (lookahead == 'l') ADVANCE(42); + if (lookahead == 'n') ADVANCE(43); END_STATE(); case 20: - if (lookahead == 'r') ADVANCE(45); + if (lookahead == 'r') ADVANCE(44); END_STATE(); case 21: - if (lookahead == 'p') ADVANCE(46); + if (lookahead == 'p') ADVANCE(45); END_STATE(); case 22: - if (lookahead == 'l') ADVANCE(47); + if (lookahead == 'l') ADVANCE(46); END_STATE(); case 23: ACCEPT_TOKEN(anon_sym_fn); END_STATE(); case 24: - if (lookahead == 'r') ADVANCE(48); + if (lookahead == 'r') ADVANCE(47); END_STATE(); case 25: - if (lookahead == 'o') ADVANCE(49); + if (lookahead == 'o') ADVANCE(48); END_STATE(); case 26: - if (lookahead == 'p') ADVANCE(50); + if (lookahead == 'p') ADVANCE(49); END_STATE(); case 27: - if (lookahead == 't') ADVANCE(51); + if (lookahead == 't') ADVANCE(50); END_STATE(); case 28: - if (lookahead == 'c') ADVANCE(52); + if (lookahead == 'c') ADVANCE(51); END_STATE(); case 29: - if (lookahead == 'm') ADVANCE(53); + if (lookahead == 'm') ADVANCE(52); END_STATE(); case 30: - if (lookahead == 'a') ADVANCE(54); + if (lookahead == 'a') ADVANCE(53); END_STATE(); case 31: - if (lookahead == 't') ADVANCE(55); + if (lookahead == 't') ADVANCE(54); END_STATE(); case 32: - if (lookahead == 'l') ADVANCE(56); + if (lookahead == 'l') ADVANCE(55); END_STATE(); case 33: - if (lookahead == 'r') ADVANCE(57); + if (lookahead == 'r') ADVANCE(56); END_STATE(); case 34: - if (lookahead == 's') ADVANCE(58); + if (lookahead == 's') ADVANCE(57); END_STATE(); case 35: - if (lookahead == 'r') ADVANCE(59); + if (lookahead == 'd') ADVANCE(58); END_STATE(); case 36: - if (lookahead == 'd') ADVANCE(60); + if (lookahead == 'a') ADVANCE(59); + if (lookahead == 'u') ADVANCE(60); END_STATE(); case 37: - if (lookahead == 'a') ADVANCE(61); - if (lookahead == 'u') ADVANCE(62); + if (lookahead == 'p') ADVANCE(61); END_STATE(); case 38: - if (lookahead == 'p') ADVANCE(63); + if (lookahead == 'k') ADVANCE(62); + if (lookahead == 'r') ADVANCE(63); END_STATE(); case 39: - if (lookahead == 'k') ADVANCE(64); - if (lookahead == 'r') ADVANCE(65); + if (lookahead == 'e') ADVANCE(64); END_STATE(); case 40: - if (lookahead == 'e') ADVANCE(66); + if (lookahead == 'e') ADVANCE(65); END_STATE(); case 41: - if (lookahead == 'e') ADVANCE(67); + if (lookahead == 'l') ADVANCE(66); END_STATE(); case 42: - if (lookahead == 'l') ADVANCE(68); + if (lookahead == 'l') ADVANCE(67); END_STATE(); case 43: - if (lookahead == 'l') ADVANCE(69); + if (lookahead == 's') ADVANCE(68); END_STATE(); case 44: - if (lookahead == 's') ADVANCE(70); + if (lookahead == 'i') ADVANCE(69); END_STATE(); case 45: - if (lookahead == 'i') ADVANCE(71); + if (lookahead == 'o') ADVANCE(70); END_STATE(); case 46: - if (lookahead == 'o') ADVANCE(72); + if (lookahead == 's') ADVANCE(71); END_STATE(); case 47: - if (lookahead == 's') ADVANCE(73); + ACCEPT_TOKEN(anon_sym_for); END_STATE(); case 48: - ACCEPT_TOKEN(anon_sym_for); + if (lookahead == 'm') ADVANCE(72); END_STATE(); case 49: - if (lookahead == 'm') ADVANCE(74); + if (lookahead == 'o') ADVANCE(73); END_STATE(); case 50: - if (lookahead == 'o') ADVANCE(75); + if (lookahead == 'c') ADVANCE(74); END_STATE(); case 51: - if (lookahead == 'c') ADVANCE(76); + if (lookahead == 'k') ADVANCE(75); END_STATE(); case 52: - if (lookahead == 'k') ADVANCE(77); + if (lookahead == 'b') ADVANCE(76); END_STATE(); case 53: - if (lookahead == 'b') ADVANCE(78); + if (lookahead == 'q') ADVANCE(77); END_STATE(); case 54: - if (lookahead == 'q') ADVANCE(79); + if (lookahead == 'u') ADVANCE(78); END_STATE(); case 55: - if (lookahead == 'u') ADVANCE(80); + if (lookahead == 'f') ADVANCE(79); END_STATE(); case 56: - if (lookahead == 'f') ADVANCE(81); + if (lookahead == 'i') ADVANCE(80); END_STATE(); case 57: - if (lookahead == 'i') ADVANCE(82); + if (lookahead == 't') ADVANCE(81); END_STATE(); case 58: - if (lookahead == 't') ADVANCE(83); + if (lookahead == 'o') ADVANCE(82); END_STATE(); case 59: - if (lookahead == 'o') ADVANCE(84); + if (lookahead == 'i') ADVANCE(83); END_STATE(); case 60: - if (lookahead == 'o') ADVANCE(85); + if (lookahead == 'e') ADVANCE(84); + if (lookahead == 's') ADVANCE(85); END_STATE(); case 61: - if (lookahead == 'i') ADVANCE(86); + if (lookahead == 'e') ADVANCE(86); END_STATE(); case 62: - if (lookahead == 'e') ADVANCE(87); + if (lookahead == 'n') ADVANCE(87); END_STATE(); case 63: if (lookahead == 'e') ADVANCE(88); @@ -4006,204 +4006,201 @@ static bool ts_lex_keywords(TSLexer *lexer, TSStateId state) { if (lookahead == 'n') ADVANCE(89); END_STATE(); case 65: - if (lookahead == 'e') ADVANCE(90); + if (lookahead == 'r') ADVANCE(90); END_STATE(); case 66: - if (lookahead == 'n') ADVANCE(91); + if (lookahead == 'e') ADVANCE(91); END_STATE(); case 67: - if (lookahead == 'r') ADVANCE(92); + if (lookahead == 'e') ADVANCE(92); END_STATE(); case 68: - if (lookahead == 'e') ADVANCE(93); + if (lookahead == 't') ADVANCE(93); END_STATE(); case 69: - if (lookahead == 'e') ADVANCE(94); + if (lookahead == 'v') ADVANCE(94); END_STATE(); case 70: - if (lookahead == 't') ADVANCE(95); + if (lookahead == 'r') ADVANCE(95); END_STATE(); case 71: - if (lookahead == 'v') ADVANCE(96); + if (lookahead == 'e') ADVANCE(96); END_STATE(); case 72: - if (lookahead == 'r') ADVANCE(97); + ACCEPT_TOKEN(anon_sym_from); END_STATE(); case 73: - if (lookahead == 'e') ADVANCE(98); + if (lookahead == 'r') ADVANCE(97); END_STATE(); case 74: - ACCEPT_TOKEN(anon_sym_from); + if (lookahead == 'h') ADVANCE(98); END_STATE(); case 75: - if (lookahead == 'r') ADVANCE(99); + ACCEPT_TOKEN(anon_sym_mock); END_STATE(); case 76: - if (lookahead == 'h') ADVANCE(100); + if (lookahead == 'e') ADVANCE(99); END_STATE(); case 77: - ACCEPT_TOKEN(anon_sym_mock); + if (lookahead == 'u') ADVANCE(100); END_STATE(); case 78: - if (lookahead == 'e') ADVANCE(101); + if (lookahead == 'r') ADVANCE(101); END_STATE(); case 79: - if (lookahead == 'u') ADVANCE(102); + ACCEPT_TOKEN(sym_self); END_STATE(); case 80: - if (lookahead == 'r') ADVANCE(103); + if (lookahead == 'n') ADVANCE(102); END_STATE(); case 81: - ACCEPT_TOKEN(sym_self); + ACCEPT_TOKEN(anon_sym_test); END_STATE(); case 82: - if (lookahead == 'n') ADVANCE(104); + ACCEPT_TOKEN(sym_todo); END_STATE(); case 83: - ACCEPT_TOKEN(anon_sym_test); + if (lookahead == 't') ADVANCE(103); END_STATE(); case 84: - if (lookahead == 'w') ADVANCE(105); + ACCEPT_TOKEN(anon_sym_true); END_STATE(); case 85: - ACCEPT_TOKEN(sym_todo); + if (lookahead == 't') ADVANCE(104); END_STATE(); case 86: - if (lookahead == 't') ADVANCE(106); + ACCEPT_TOKEN(anon_sym_type); END_STATE(); case 87: - ACCEPT_TOKEN(anon_sym_true); + if (lookahead == 'o') ADVANCE(105); END_STATE(); case 88: - ACCEPT_TOKEN(anon_sym_type); + if (lookahead == 'a') ADVANCE(106); END_STATE(); case 89: - if (lookahead == 'o') ADVANCE(107); + ACCEPT_TOKEN(anon_sym_when); END_STATE(); case 90: - if (lookahead == 'a') ADVANCE(108); + if (lookahead == 't') ADVANCE(107); END_STATE(); case 91: - ACCEPT_TOKEN(anon_sym_when); + if (lookahead == 'a') ADVANCE(108); END_STATE(); case 92: - if (lookahead == 't') ADVANCE(109); + if (lookahead == 'c') ADVANCE(109); END_STATE(); case 93: - if (lookahead == 'a') ADVANCE(110); + ACCEPT_TOKEN(anon_sym_const); END_STATE(); case 94: - if (lookahead == 'c') ADVANCE(111); + if (lookahead == 'i') ADVANCE(110); END_STATE(); case 95: - ACCEPT_TOKEN(anon_sym_const); + if (lookahead == 't') ADVANCE(111); END_STATE(); case 96: - if (lookahead == 'i') ADVANCE(112); + ACCEPT_TOKEN(anon_sym_false); END_STATE(); case 97: - if (lookahead == 't') ADVANCE(113); + if (lookahead == 't') ADVANCE(112); END_STATE(); case 98: - ACCEPT_TOKEN(anon_sym_false); + ACCEPT_TOKEN(anon_sym_match); END_STATE(); case 99: - if (lookahead == 't') ADVANCE(114); + if (lookahead == 'r') ADVANCE(113); END_STATE(); case 100: - ACCEPT_TOKEN(anon_sym_match); + if (lookahead == 'e') ADVANCE(114); END_STATE(); case 101: - if (lookahead == 'r') ADVANCE(115); + if (lookahead == 'n') ADVANCE(115); END_STATE(); case 102: - if (lookahead == 'e') ADVANCE(116); + if (lookahead == 'g') ADVANCE(116); END_STATE(); case 103: - if (lookahead == 'n') ADVANCE(117); + ACCEPT_TOKEN(anon_sym_trait); END_STATE(); case 104: - if (lookahead == 'g') ADVANCE(118); + if (lookahead == 'e') ADVANCE(117); END_STATE(); case 105: - if (lookahead == 's') ADVANCE(119); + if (lookahead == 'w') ADVANCE(118); END_STATE(); case 106: - ACCEPT_TOKEN(anon_sym_trait); + if (lookahead == 'c') ADVANCE(119); END_STATE(); case 107: - if (lookahead == 'w') ADVANCE(120); + ACCEPT_TOKEN(anon_sym_assert); END_STATE(); case 108: - if (lookahead == 'c') ADVANCE(121); + if (lookahead == 'n') ADVANCE(120); END_STATE(); case 109: - ACCEPT_TOKEN(anon_sym_assert); + if (lookahead == 't') ADVANCE(121); END_STATE(); case 110: if (lookahead == 'n') ADVANCE(122); END_STATE(); case 111: - if (lookahead == 't') ADVANCE(123); + ACCEPT_TOKEN(anon_sym_export); END_STATE(); case 112: - if (lookahead == 'n') ADVANCE(124); + ACCEPT_TOKEN(anon_sym_import); END_STATE(); case 113: - ACCEPT_TOKEN(anon_sym_export); + ACCEPT_TOKEN(anon_sym_number); END_STATE(); case 114: - ACCEPT_TOKEN(anon_sym_import); + ACCEPT_TOKEN(anon_sym_opaque); END_STATE(); case 115: - ACCEPT_TOKEN(anon_sym_number); + ACCEPT_TOKEN(anon_sym_return); END_STATE(); case 116: - ACCEPT_TOKEN(anon_sym_opaque); + ACCEPT_TOKEN(anon_sym_string); END_STATE(); case 117: - ACCEPT_TOKEN(anon_sym_return); + if (lookahead == 'd') ADVANCE(123); END_STATE(); case 118: - ACCEPT_TOKEN(anon_sym_string); + if (lookahead == 'n') ADVANCE(124); END_STATE(); case 119: - ACCEPT_TOKEN(anon_sym_throws); + if (lookahead == 'h') ADVANCE(125); END_STATE(); case 120: - if (lookahead == 'n') ADVANCE(125); + ACCEPT_TOKEN(anon_sym_boolean); END_STATE(); case 121: - if (lookahead == 'h') ADVANCE(126); + ACCEPT_TOKEN(anon_sym_collect); END_STATE(); case 122: - ACCEPT_TOKEN(anon_sym_boolean); + if (lookahead == 'g') ADVANCE(126); END_STATE(); case 123: - ACCEPT_TOKEN(anon_sym_collect); + ACCEPT_TOKEN(anon_sym_trusted); END_STATE(); case 124: - if (lookahead == 'g') ADVANCE(127); + ACCEPT_TOKEN(anon_sym_unknown); END_STATE(); case 125: - ACCEPT_TOKEN(anon_sym_unknown); + if (lookahead == 'a') ADVANCE(127); END_STATE(); case 126: - if (lookahead == 'a') ADVANCE(128); + ACCEPT_TOKEN(anon_sym_deriving); END_STATE(); case 127: - ACCEPT_TOKEN(anon_sym_deriving); + if (lookahead == 'b') ADVANCE(128); END_STATE(); case 128: - if (lookahead == 'b') ADVANCE(129); + if (lookahead == 'l') ADVANCE(129); END_STATE(); case 129: - if (lookahead == 'l') ADVANCE(130); + if (lookahead == 'e') ADVANCE(130); END_STATE(); case 130: - if (lookahead == 'e') ADVANCE(131); - END_STATE(); - case 131: ACCEPT_TOKEN(sym_unreachable); END_STATE(); default: @@ -5402,7 +5399,7 @@ static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { [ts_builtin_sym_end] = ACTIONS(1), [sym_identifier] = ACTIONS(1), [anon_sym_import] = ACTIONS(1), - [anon_sym_throws] = ACTIONS(1), + [anon_sym_trusted] = ACTIONS(1), [anon_sym_LBRACE] = ACTIONS(1), [anon_sym_COMMA] = ACTIONS(1), [anon_sym_RBRACE] = ACTIONS(1), @@ -43097,7 +43094,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(1686), 1, sym_identifier, ACTIONS(1688), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(1690), 1, anon_sym_for, STATE(654), 1, @@ -43526,7 +43523,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(1686), 1, sym_identifier, ACTIONS(1688), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(1690), 1, anon_sym_for, STATE(681), 1, @@ -43924,7 +43921,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(1686), 1, sym_identifier, ACTIONS(1688), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(1690), 1, anon_sym_for, STATE(707), 1, @@ -44169,7 +44166,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(1686), 1, sym_identifier, ACTIONS(1688), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(1690), 1, anon_sym_for, STATE(722), 1, @@ -44185,7 +44182,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(1686), 1, sym_identifier, ACTIONS(1688), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(1690), 1, anon_sym_for, STATE(723), 1, @@ -47629,7 +47626,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(5), 1, anon_sym_SLASH_STAR, ACTIONS(2434), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(2436), 1, anon_sym_LBRACE, STATE(991), 1, @@ -48186,7 +48183,7 @@ static const uint16_t ts_small_parse_table[] = { ACTIONS(5), 1, anon_sym_SLASH_STAR, ACTIONS(2512), 1, - anon_sym_throws, + anon_sym_trusted, ACTIONS(2514), 1, anon_sym_LBRACE, STATE(1044), 1, diff --git a/editors/vscode/syntaxes/floe.tmLanguage.json b/editors/vscode/syntaxes/floe.tmLanguage.json index 7ba3eb22..c7ed51f7 100644 --- a/editors/vscode/syntaxes/floe.tmLanguage.json +++ b/editors/vscode/syntaxes/floe.tmLanguage.json @@ -92,7 +92,7 @@ }, { "name": "keyword.control.import.floe", - "match": "\\b(import|from|export|throws)\\b" + "match": "\\b(import|from|export|trusted)\\b" }, { "name": "keyword.other.floe", diff --git a/examples/store/src/pages/cart.fl b/examples/store/src/pages/cart.fl index ab479a3a..aa7b62e2 100644 --- a/examples/store/src/pages/cart.fl +++ b/examples/store/src/pages/cart.fl @@ -1,4 +1,4 @@ -import { useState } from "react" +import trusted { useState } from "react" import { CartItem, Product, ProductId, OrderStatus, OrderId } from "../types" import { for Array, formatPrice } from "../product" diff --git a/examples/store/src/pages/catalog.fl b/examples/store/src/pages/catalog.fl index c7205700..0c714993 100644 --- a/examples/store/src/pages/catalog.fl +++ b/examples/store/src/pages/catalog.fl @@ -1,7 +1,7 @@ -import { useState, Suspense } from "react" -import { useSuspenseQuery, QueryErrorResetBoundary } from "@tanstack/react-query" -import { ErrorBoundary } from "react-error-boundary" -import { Link } from "@tanstack/react-router" +import trusted { useState, Suspense } from "react" +import trusted { useSuspenseQuery, QueryErrorResetBoundary } from "@tanstack/react-query" +import trusted { ErrorBoundary } from "react-error-boundary" +import trusted { Link } from "@tanstack/react-router" import { Product, SortOrder, PriceRange, ApiError } from "../types" import { for Product, for Array, sortProducts, matchesPrice, formatPrice } from "../product" import { for ApiError } from "../errors" diff --git a/examples/store/src/pages/product-detail.fl b/examples/store/src/pages/product-detail.fl index 4361f9a2..a35524cf 100644 --- a/examples/store/src/pages/product-detail.fl +++ b/examples/store/src/pages/product-detail.fl @@ -1,7 +1,7 @@ -import { Suspense } from "react" -import { useSuspenseQuery, QueryErrorResetBoundary } from "@tanstack/react-query" -import { ErrorBoundary } from "react-error-boundary" -import { Link } from "@tanstack/react-router" +import trusted { Suspense } from "react" +import trusted { useSuspenseQuery, QueryErrorResetBoundary } from "@tanstack/react-query" +import trusted { ErrorBoundary } from "react-error-boundary" +import trusted { Link } from "@tanstack/react-router" import { Product, ProductId, Review } from "../types" import { for Product, formatPrice } from "../product" import { fetchProduct } from "../api" diff --git a/examples/todo-app/src/pages/home.fl b/examples/todo-app/src/pages/home.fl index b7f68345..62113268 100644 --- a/examples/todo-app/src/pages/home.fl +++ b/examples/todo-app/src/pages/home.fl @@ -1,5 +1,5 @@ -import { useState } from "react" -import { v4 } from "uuid" +import trusted { useState } from "react" +import trusted { v4 } from "uuid" import { Todo, Filter, Validation } from "../types" import { for string, for Array } from "../todo" diff --git a/examples/todo-app/src/pages/posts.fl b/examples/todo-app/src/pages/posts.fl index b5390e75..88d94a38 100644 --- a/examples/todo-app/src/pages/posts.fl +++ b/examples/todo-app/src/pages/posts.fl @@ -1,6 +1,6 @@ -import { useState, Suspense } from "react" -import { useSuspenseQuery, QueryClient, QueryClientProvider, QueryErrorResetBoundary } from "@tanstack/react-query" -import { ErrorBoundary } from "react-error-boundary" +import trusted { useState, Suspense } from "react" +import trusted { useSuspenseQuery, QueryClient, QueryClientProvider, QueryErrorResetBoundary } from "@tanstack/react-query" +import trusted { ErrorBoundary } from "react-error-boundary" type Post { id: number, diff --git a/src/checker.rs b/src/checker.rs index c15456c8..5074ebf1 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -32,16 +32,20 @@ pub fn annotate_types(program: &mut Program, types: &ExprTypeMap) { /// Walk the AST and set `async_fn = true` on functions/arrows whose bodies /// contain `Promise.await` calls or whose return type is `Promise`. -/// Also collects throwing import names from the program for detection. +/// Also collects untrusted import names from the program for detection. pub fn mark_async_functions(program: &mut Program) { - // Collect throwing import names + // Collect untrusted import names (npm imports not marked `trusted`) let mut throwing: HashSet = HashSet::new(); for item in &program.items { if let ItemKind::Import(decl) = &item.kind { - for spec in &decl.specifiers { - if decl.throws || spec.throws { - let name = spec.alias.as_ref().unwrap_or(&spec.name); - throwing.insert(name.clone()); + // Only track npm imports (source doesn't start with "./" or "../") + let is_npm = !decl.source.starts_with("./") && !decl.source.starts_with("../"); + if is_npm { + for spec in &decl.specifiers { + if !decl.trusted && !spec.trusted { + let name = spec.alias.as_ref().unwrap_or(&spec.name); + throwing.insert(name.clone()); + } } } } @@ -230,9 +234,9 @@ pub struct Checker { pub(crate) unused: UnusedTracker, /// Trait declarations and implementations. pub(crate) traits: TraitRegistry, - /// Names of throwing (external TS) imports marked with `throws`. - throwing_imports: HashSet, - /// Names from npm imports — used for determining if `try` wrapping was valid. + /// Names of untrusted (external TS) imports — npm imports not marked `trusted`. + untrusted_imports: HashSet, + /// Names from npm imports (both trusted and untrusted). npm_imports: HashSet, /// Whether we are in the type registration pass (suppress unknown type errors). registering_types: bool, @@ -434,9 +438,9 @@ impl Checker { env.define(name, ty.clone()); } - // Browser globals that can throw - let mut throwing_globals = HashSet::new(); - throwing_globals.insert("fetch".to_string()); + // Browser globals that can throw (untrusted by default) + let mut untrusted_globals = HashSet::new(); + untrusted_globals.insert("fetch".to_string()); Self { env, @@ -447,7 +451,7 @@ impl Checker { ctx: CheckContext::default(), unused: UnusedTracker::default(), traits: TraitRegistry::default(), - throwing_imports: throwing_globals, + untrusted_imports: untrusted_globals, npm_imports: HashSet::new(), registering_types: false, resolved_imports: HashMap::new(), diff --git a/src/checker/expr.rs b/src/checker/expr.rs index 6784e95d..99b3286a 100644 --- a/src/checker/expr.rs +++ b/src/checker/expr.rs @@ -78,8 +78,8 @@ impl Checker { args, } => { let ret = self.check_call(callee, type_args, args, expr.span); - // Auto-wrap throws import calls in Result - if self.is_throwing_call(callee) { + // Auto-wrap untrusted import calls in Result + if self.is_untrusted_call(callee) { match &ret { Type::Promise(inner) => Type::Promise(Box::new(Type::result_of( *inner.clone(), @@ -264,13 +264,13 @@ impl Checker { } } - /// Check if a call expression targets a `throws` import (direct or member access). - fn is_throwing_call(&self, callee: &Expr) -> bool { + /// Check if a call expression targets an untrusted import (direct or member access). + fn is_untrusted_call(&self, callee: &Expr) -> bool { match &callee.kind { - ExprKind::Identifier(name) => self.throwing_imports.contains(name.as_str()), + ExprKind::Identifier(name) => self.untrusted_imports.contains(name.as_str()), ExprKind::Member { object, .. } => { if let ExprKind::Identifier(obj_name) = &object.kind { - self.throwing_imports.contains(obj_name.as_str()) + self.untrusted_imports.contains(obj_name.as_str()) } else { false } diff --git a/src/checker/imports.rs b/src/checker/imports.rs index c201d31f..9236c340 100644 --- a/src/checker/imports.rs +++ b/src/checker/imports.rs @@ -70,9 +70,9 @@ impl Checker { // Track npm imports (resolved.is_none() means not a .fl file). if resolved.is_none() { self.npm_imports.insert(effective_name.to_string()); - // Track throwing imports (marked with `throws` at module or specifier level). - if decl.throws || spec.throws { - self.throwing_imports.insert(effective_name.to_string()); + // Track untrusted imports (not marked `trusted` at module or specifier level). + if !decl.trusted && !spec.trusted { + self.untrusted_imports.insert(effective_name.to_string()); } } } diff --git a/src/checker/tests.rs b/src/checker/tests.rs index 1c673ca6..ec9ed27c 100644 --- a/src/checker/tests.rs +++ b/src/checker/tests.rs @@ -401,7 +401,7 @@ fn call_site_type_args_infer_return() { let program = crate::parser::Parser::new( r#" -import { useState } from "react" +import trusted { useState } from "react" type Todo { text: string } const [todos, _setTodos] = useState>([]) const _x = todos @@ -471,11 +471,11 @@ const _x = _user |> display // (Inline for-declaration tests removed — only block form is supported) -// ── npm imports (no enforcement needed, throws are auto-wrapped) ── +// ── Untrusted Import Auto-wrapping ─────────────────────────── #[test] -fn npm_import_callable_without_throws() { - // Regular npm imports can be called freely (no try needed) +fn untrusted_import_auto_wraps_to_result() { + // Untrusted npm imports auto-wrap return type to Result let diags = check( r#" import { capitalize } from "some-lib" @@ -484,7 +484,38 @@ const _x = capitalize("hello") ); assert!( diags.iter().all(|d| d.severity != Severity::Error), - "npm import should be callable without throws, got: {:?}", + "untrusted npm import should be callable (auto-wrapped), got: {:?}", + diags.iter().map(|d| &d.message).collect::>() + ); +} + +#[test] +fn trusted_specifier_no_auto_wrap() { + let diags = check( + r#" +import { trusted capitalize } from "some-lib" +const _x = capitalize("hello") +"#, + ); + assert!( + diags.iter().all(|d| d.severity != Severity::Error), + "trusted import should be callable directly, got: {:?}", + diags.iter().map(|d| &d.message).collect::>() + ); +} + +#[test] +fn trusted_module_no_auto_wrap() { + let diags = check( + r#" +import trusted { capitalize, slugify } from "string-utils" +const _x = capitalize("hello") +const _y = slugify("hello world") +"#, + ); + assert!( + diags.iter().all(|d| d.severity != Severity::Error), + "trusted module import should be callable directly, got: {:?}", diags.iter().map(|d| &d.message).collect::>() ); } @@ -1600,7 +1631,7 @@ fn dispatch_generic_converts_to_function() { let program = crate::parser::Parser::new( r#" -import { useState } from "react" +import trusted { useState } from "react" type Todo { text: string } const [todos, setTodos] = useState>([]) fn handler() { @@ -1675,7 +1706,7 @@ fn calling_dispatch_type_is_callable() { let program = crate::parser::Parser::new( r#" -import { useState } from "react" +import trusted { useState } from "react" type Todo { text: string } const [todos, setTodos] = useState>([]) fn handler() { @@ -1819,7 +1850,7 @@ fn object_destructure_from_trusted_import_gets_field_types() { let program = crate::parser::Parser::new( r#" -import { useQuery } from "react-query" +import trusted { useQuery } from "react-query" const { data, isLoading } = useQuery("key") const _x = data const _y = isLoading @@ -2654,7 +2685,7 @@ const doubled = len + 1 fn npm_import_used_as_constructor_no_error() { let diags = check( r#" -import { QueryClient } from "@tanstack/react-query" +import trusted { QueryClient } from "@tanstack/react-query" const _qc = QueryClient(defaultOptions: {}) "#, ); @@ -2718,7 +2749,7 @@ fn timer_globals_are_recognized() { fn narrowing_unknown_to_concrete_type_is_error() { let diags = check( r#" -import { getData } from "some-lib" +import trusted { getData } from "some-lib" const data = getData() const x: number = data "#, @@ -2734,7 +2765,7 @@ const x: number = data fn unknown_to_unknown_annotation_is_ok() { let diags = check( r#" -import { getData } from "some-lib" +import trusted { getData } from "some-lib" const data = getData() const x: unknown = data "#, @@ -2804,7 +2835,7 @@ export fn bad() -> Promise { fn member_access_on_unknown_is_error() { let diags = check( r#" -import { getData } from "some-lib" +import trusted { getData } from "some-lib" const data = getData() const x = data.name "#, @@ -2820,7 +2851,7 @@ const x = data.name fn method_call_on_unknown_is_error() { let diags = check( r#" -import { getData } from "some-lib" +import trusted { getData } from "some-lib" const data = getData() const x = data.toJSON() "#, @@ -3507,7 +3538,7 @@ fn imported_optional_params_allow_omission() { // useQueryClient(queryClient?: QueryClient): QueryClient let program = crate::parser::Parser::new( r#" -import { useQueryClient } from "@tanstack/react-query" +import trusted { useQueryClient } from "@tanstack/react-query" const _client = useQueryClient() "#, ) @@ -3545,7 +3576,7 @@ fn imported_optional_params_still_validates_max_args() { // fn(a: string, b?: number): void — max 2 args let program = crate::parser::Parser::new( r#" -import { doStuff } from "some-lib" +import trusted { doStuff } from "some-lib" const _x = doStuff("hi", 1, true) "#, ) @@ -3591,7 +3622,7 @@ fn ts_union_accepts_compatible_member() { // format(date: Date | number | string, fmt: string): string let program = crate::parser::Parser::new( r#" -import { format } from "date-fns" +import trusted { format } from "date-fns" const _x = format("2024-01-01", "PPpp") "#, ) @@ -3639,7 +3670,7 @@ fn ts_union_rejects_incompatible_type() { // doStuff(x: number | string): void let program = crate::parser::Parser::new( r#" -import { doStuff } from "some-lib" +import trusted { doStuff } from "some-lib" const _x = doStuff(true) "#, ) @@ -3681,7 +3712,7 @@ fn ts_union_compatible_with_itself() { // calling with return value of same type should work let program = crate::parser::Parser::new( r#" -import { identity, consume } from "some-lib" +import trusted { identity, consume } from "some-lib" const x = identity() const _y = consume(x) "#, @@ -3745,7 +3776,7 @@ fn foreign_rejects_primitive_string() { // takesClient(c: QueryClient): void — should reject a string let program = crate::parser::Parser::new( r#" -import { takesClient } from "some-lib" +import trusted { takesClient } from "some-lib" const _x = takesClient("hello") "#, ) @@ -3783,7 +3814,7 @@ fn foreign_accepts_same_foreign() { // getClient(): QueryClient, takesClient(c: QueryClient): void let program = crate::parser::Parser::new( r#" -import { getClient, takesClient } from "some-lib" +import trusted { getClient, takesClient } from "some-lib" const client = getClient() const _x = takesClient(client) "#, @@ -4410,7 +4441,7 @@ fn member_access_on_imported_type_valid_fields_ok() { let program = crate::parser::Parser::new( r#" -import { UserRow } from "db" +import trusted { UserRow } from "db" const row = UserRow(id: 1, name: "test") const _id = row.id @@ -4461,8 +4492,8 @@ fn foreign_type_member_access_resolves_via_record_definition() { let program = crate::parser::Parser::new( r#" -import { useState } from "react" -import { Transition } from "api" +import trusted { useState } from "react" +import trusted { Transition } from "api" fn test(id: string) -> () { () } @@ -5349,7 +5380,7 @@ fn jsx_callback_param_inferred_from_probe() { // Source: NavLink with a callback className prop let program = crate::parser::Parser::new( r#" -import { NavLink } from "react-router-dom" +import trusted { NavLink } from "react-router-dom" fn page() { "active"} /> @@ -5419,7 +5450,7 @@ fn jsx_children_render_prop_params_inferred_from_probe() { // Source: Draggable with a render prop child (function-as-child pattern) let program = crate::parser::Parser::new( r#" -import { Draggable } from "@hello-pangea/dnd" +import trusted { Draggable } from "@hello-pangea/dnd" fn page() { @@ -5512,7 +5543,7 @@ fn jsx_children_render_prop_named_type_shows_in_name_types() { let program = crate::parser::Parser::new( r#" -import { Draggable } from "@hello-pangea/dnd" +import trusted { Draggable } from "@hello-pangea/dnd" fn page() { @@ -5751,7 +5782,7 @@ fn tsgo_function_with_unknown_return_uses_checker_return() { // tsgo returns (IssueDto) => any, checker infers (IssueDto) => () let program = crate::parser::Parser::new( r#" -import { useCallback } from "react" +import trusted { useCallback } from "react" type Item { id: string } const handler = useCallback((item: Item) => { const _x = item.id diff --git a/src/codegen.rs b/src/codegen.rs index 70382ab0..23910594 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -117,8 +117,8 @@ pub struct Codegen { for_block_type_names: HashSet, /// Type names used as runtime constructors (e.g. `User(name: "x")`). constructor_used_names: HashSet, - /// Names from `throws` imports — calls to these get auto-wrapped in try/catch IIFE. - throwing_imports: HashSet, + /// Names of untrusted npm imports — calls to these get auto-wrapped in try/catch IIFE. + untrusted_imports: HashSet, } impl Codegen { @@ -141,7 +141,7 @@ impl Codegen { for_block_fns: HashMap::new(), for_block_type_names: HashSet::new(), constructor_used_names: HashSet::new(), - throwing_imports: HashSet::new(), + untrusted_imports: HashSet::new(), } } @@ -236,9 +236,12 @@ impl Codegen { for spec in &decl.specifiers { let name = spec.alias.as_ref().unwrap_or(&spec.name); self.local_names.insert(name.clone()); - // Track throws imports for auto-wrapping - if decl.throws || spec.throws { - self.throwing_imports.insert(name.clone()); + // Track untrusted imports for auto-wrapping + // npm imports (not .fl files) that aren't marked trusted + let is_npm = + !decl.source.starts_with("./") && !decl.source.starts_with("../"); + if is_npm && !decl.trusted && !spec.trusted { + self.untrusted_imports.insert(name.clone()); } } // Register for-block functions from imports @@ -399,7 +402,7 @@ impl Codegen { for_block_fns: self.for_block_fns.clone(), for_block_type_names: self.for_block_type_names.clone(), constructor_used_names: self.constructor_used_names.clone(), - throwing_imports: self.throwing_imports.clone(), + untrusted_imports: self.untrusted_imports.clone(), } } diff --git a/src/codegen/expr.rs b/src/codegen/expr.rs index 18badd0b..f2f2607a 100644 --- a/src/codegen/expr.rs +++ b/src/codegen/expr.rs @@ -1,13 +1,13 @@ use super::*; impl Codegen { - /// Check if a callee targets a `throws` import. - fn is_throwing_call(&self, callee: &Expr) -> bool { + /// Check if a callee targets an untrusted import. + fn is_untrusted_call(&self, callee: &Expr) -> bool { match &callee.kind { - ExprKind::Identifier(name) => self.throwing_imports.contains(name.as_str()), + ExprKind::Identifier(name) => self.untrusted_imports.contains(name.as_str()), ExprKind::Member { object, .. } => { if let ExprKind::Identifier(obj_name) = &object.kind { - self.throwing_imports.contains(obj_name.as_str()) + self.untrusted_imports.contains(obj_name.as_str()) } else { false } @@ -107,8 +107,8 @@ impl Codegen { } ExprKind::Call { callee, args, .. } => { - // Auto-wrap throws import calls in try/catch IIFE - if self.is_throwing_call(callee) { + // Auto-wrap untrusted import calls in try/catch IIFE + if self.is_untrusted_call(callee) { self.push(&format!( "await (async () => {{ try {{ return {{ {OK_FIELD}: true as const, {VALUE_FIELD}: await " )); diff --git a/src/codegen/tests.rs b/src/codegen/tests.rs index 7e887a63..1dc9b2f2 100644 --- a/src/codegen/tests.rs +++ b/src/codegen/tests.rs @@ -149,7 +149,7 @@ fn import_named() { // Both names used in value positions → regular import assert_eq!( emit( - r#"import { useState, useEffect } from "react" + r#"import trusted { useState, useEffect } from "react" const x = useState(0) const y = useEffect"# ), diff --git a/src/cst/items.rs b/src/cst/items.rs index c01668c4..a895f8fd 100644 --- a/src/cst/items.rs +++ b/src/cst/items.rs @@ -92,9 +92,9 @@ impl<'src> CstParser<'src> { self.expect(TokenKind::Import); self.eat_trivia(); - // `import throws { ... }` — module-level throws - if self.at(TokenKind::Throws) { - self.bump(); // throws + // `import trusted { ... }` — module-level trusted + if self.at(TokenKind::Trusted) { + self.bump(); // trusted self.eat_trivia(); } @@ -133,9 +133,9 @@ impl<'src> CstParser<'src> { fn parse_import_specifier(&mut self) { self.builder.start_node(SyntaxKind::IMPORT_SPECIFIER.into()); - // `throws foo` — per-specifier throws - if self.at(TokenKind::Throws) && self.peek_is_ident() { - self.bump(); // throws + // `trusted foo` — per-specifier trusted + if self.at(TokenKind::Trusted) && self.peek_is_ident() { + self.bump(); // trusted self.eat_trivia(); } self.expect_ident(); diff --git a/src/formatter/items.rs b/src/formatter/items.rs index 2cfaaccc..d4fd2bbd 100644 --- a/src/formatter/items.rs +++ b/src/formatter/items.rs @@ -32,13 +32,13 @@ impl Formatter<'_> { pub(crate) fn fmt_import(&mut self, node: &SyntaxNode) { self.write("import "); - // Check for module-level `throws` keyword - let has_throws = node.children_with_tokens().any(|t| { + // Check for module-level `trusted` keyword + let has_trusted = node.children_with_tokens().any(|t| { t.as_token() - .is_some_and(|tok| tok.kind() == SyntaxKind::KW_THROWS) + .is_some_and(|tok| tok.kind() == SyntaxKind::KW_TRUSTED) }); - if has_throws { - self.write("throws "); + if has_trusted { + self.write("trusted "); } let specifiers: Vec<_> = node @@ -82,11 +82,11 @@ impl Formatter<'_> { .filter(|t| t.kind() == SyntaxKind::IDENT || t.kind() == SyntaxKind::BANNED) .collect(); - // Check for per-specifier `throws` — KW_THROWS token before the idents - let has_throws = self.has_token(node, SyntaxKind::KW_THROWS); + // Check for per-specifier `trusted` — KW_TRUSTED token before the idents + let has_trusted = self.has_token(node, SyntaxKind::KW_TRUSTED); - if has_throws { - self.write("throws "); + if has_trusted { + self.write("trusted "); if let Some(name) = idents.first() { self.write(name.text()); } diff --git a/src/formatter/tests.rs b/src/formatter/tests.rs index 50e3766c..0a5eb5ce 100644 --- a/src/formatter/tests.rs +++ b/src/formatter/tests.rs @@ -472,27 +472,27 @@ fn format_preserves_blank_line_after_match_block() { assert_fmt(src, src); } -// ── Import throws ────────────────────────────────────────── +// ── Import trusted ───────────────────────────────────────── #[test] -fn format_import_throws_module() { +fn format_import_trusted_module() { assert_fmt( - r#"import throws {parseYaml,parseJson} from "yaml-lib""#, - r#"import throws { parseYaml, parseJson } from "yaml-lib""#, + r#"import trusted {useState,Suspense} from "react""#, + r#"import trusted { useState, Suspense } from "react""#, ); } #[test] -fn format_import_throws_specifier() { +fn format_import_trusted_specifier() { assert_fmt( - r#"import {throws parseYaml,capitalize} from "some-lib""#, - r#"import { throws parseYaml, capitalize } from "some-lib""#, + r#"import {trusted capitalize,fetchUser} from "some-lib""#, + r#"import { trusted capitalize, fetchUser } from "some-lib""#, ); } #[test] -fn format_import_throws_roundtrip() { - let src = r#"import throws { parseYaml, parseJson } from "yaml-lib""#; +fn format_import_trusted_roundtrip() { + let src = r#"import trusted { useState, useEffect } from "react""#; assert_fmt(src, src); } diff --git a/src/lexer/token.rs b/src/lexer/token.rs index b4da81af..48cf1a44 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -45,8 +45,8 @@ pub enum TokenKind { For, /// `self` — explicit receiver parameter in for blocks SelfKw, - /// `throws` — marks an import whose functions may throw (auto-wrapped in Result) - Throws, + /// `trusted` — marks an import as safe to call without Result wrapping + Trusted, /// `trait` — trait declaration keyword Trait, /// `assert` — assertion (only valid inside test blocks) @@ -266,7 +266,7 @@ pub fn lookup_keyword(word: &str) -> Option { "opaque" => Some(TokenKind::Opaque), "for" => Some(TokenKind::For), "self" => Some(TokenKind::SelfKw), - "throws" => Some(TokenKind::Throws), + "trusted" => Some(TokenKind::Trusted), "trait" => Some(TokenKind::Trait), "assert" => Some(TokenKind::Assert), "when" => Some(TokenKind::When), @@ -317,7 +317,7 @@ mod tests { assert_eq!(lookup_keyword("fn"), Some(TokenKind::Fn)); assert_eq!(lookup_keyword("match"), Some(TokenKind::Match)); assert_eq!(lookup_keyword("opaque"), Some(TokenKind::Opaque)); - assert_eq!(lookup_keyword("throws"), Some(TokenKind::Throws)); + assert_eq!(lookup_keyword("trusted"), Some(TokenKind::Trusted)); assert_eq!(lookup_keyword("trait"), Some(TokenKind::Trait)); assert_eq!(lookup_keyword("Ok"), None); assert_eq!(lookup_keyword("Err"), None); diff --git a/src/lower/items.rs b/src/lower/items.rs index 998ca08f..67a3f3e1 100644 --- a/src/lower/items.rs +++ b/src/lower/items.rs @@ -98,15 +98,15 @@ impl<'src> Lowerer<'src> { } } - // Check for module-level `throws` keyword - let module_throws = node.children_with_tokens().any(|child| { + // Check for module-level `trusted` keyword + let module_trusted = node.children_with_tokens().any(|child| { child .as_token() - .is_some_and(|t| t.kind() == SyntaxKind::KW_THROWS) + .is_some_and(|t| t.kind() == SyntaxKind::KW_TRUSTED) }); Some(ImportDecl { - throws: module_throws, + trusted: module_trusted, specifiers, for_specifiers, source, @@ -125,8 +125,8 @@ impl<'src> Lowerer<'src> { let span = self.node_span(node); let idents = self.collect_idents(node); - // Check for per-specifier `throws` — appears as KW_THROWS token before the ident - let per_throws = self.has_keyword(node, SyntaxKind::KW_THROWS); + // Check for per-specifier `trusted` — appears as KW_TRUSTED token before the ident + let per_trusted = self.has_keyword(node, SyntaxKind::KW_TRUSTED); let name = idents.first()?.clone(); let alias = idents.get(1).cloned(); @@ -134,7 +134,7 @@ impl<'src> Lowerer<'src> { Some(ImportSpecifier { name, alias, - throws: per_throws, + trusted: per_trusted, span, }) } diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 0833aedb..6ccc091b 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -75,8 +75,8 @@ pub enum ItemKind { #[derive(Debug, Clone, PartialEq)] pub struct ImportDecl { - /// Whether the entire import throws: `import throws { ... } from "..."` - pub throws: bool, + /// Whether the entire import is trusted: `import trusted { ... } from "..."` + pub trusted: bool, pub specifiers: Vec, /// For-import specifiers: `import { for User, for Array } from "..."` pub for_specifiers: Vec, @@ -87,8 +87,8 @@ pub struct ImportDecl { pub struct ImportSpecifier { pub name: String, pub alias: Option, - /// Whether this specific import throws: `import { throws parseYaml } from "..."` - pub throws: bool, + /// Whether this specific import is trusted: `import { trusted capitalize } from "..."` + pub trusted: bool, pub span: Span, } diff --git a/src/parser/tests.rs b/src/parser/tests.rs index f41a8e16..d0ee2ba4 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -768,28 +768,28 @@ fn import_named() { } #[test] -fn import_throws_all() { - match first_item(r#"import throws { parseYaml, parseJson } from "yaml-lib""#) { +fn import_trusted_all() { + match first_item(r#"import trusted { capitalize, slugify } from "string-utils""#) { ItemKind::Import(decl) => { - assert!(decl.throws); + assert!(decl.trusted); assert_eq!(decl.specifiers.len(), 2); - assert_eq!(decl.specifiers[0].name, "parseYaml"); - assert_eq!(decl.specifiers[1].name, "parseJson"); + assert_eq!(decl.specifiers[0].name, "capitalize"); + assert_eq!(decl.specifiers[1].name, "slugify"); } other => panic!("expected import, got {other:?}"), } } #[test] -fn import_throws_per_specifier() { - match first_item(r#"import { throws parseYaml, capitalize } from "some-lib""#) { +fn import_trusted_per_specifier() { + match first_item(r#"import { trusted capitalize, fetchUser } from "some-lib""#) { ItemKind::Import(decl) => { - assert!(!decl.throws); + assert!(!decl.trusted); assert_eq!(decl.specifiers.len(), 2); - assert!(decl.specifiers[0].throws); - assert_eq!(decl.specifiers[0].name, "parseYaml"); - assert!(!decl.specifiers[1].throws); - assert_eq!(decl.specifiers[1].name, "capitalize"); + assert!(decl.specifiers[0].trusted); + assert_eq!(decl.specifiers[0].name, "capitalize"); + assert!(!decl.specifiers[1].trusted); + assert_eq!(decl.specifiers[1].name, "fetchUser"); } other => panic!("expected import, got {other:?}"), } diff --git a/src/syntax.rs b/src/syntax.rs index 40d76c87..6968aabd 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -29,7 +29,7 @@ pub enum SyntaxKind { KW_OPAQUE, KW_FOR, KW_SELF, - KW_THROWS, + KW_TRUSTED, KW_TRAIT, KW_ASSERT, KW_WHEN, @@ -258,7 +258,7 @@ pub fn token_kind_to_syntax(kind: &TokenKind) -> SyntaxKind { TokenKind::Opaque => SyntaxKind::KW_OPAQUE, TokenKind::For => SyntaxKind::KW_FOR, TokenKind::SelfKw => SyntaxKind::KW_SELF, - TokenKind::Throws => SyntaxKind::KW_THROWS, + TokenKind::Trusted => SyntaxKind::KW_TRUSTED, TokenKind::Trait => SyntaxKind::KW_TRAIT, TokenKind::Assert => SyntaxKind::KW_ASSERT, TokenKind::When => SyntaxKind::KW_WHEN, diff --git a/tests/fixtures/imports.fl b/tests/fixtures/imports.fl index a194b7a7..69f18313 100644 --- a/tests/fixtures/imports.fl +++ b/tests/fixtures/imports.fl @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react" +import trusted { useState, useEffect } from "react" import { fetchUser } from "./api" const _state = useState(0) diff --git a/tests/fixtures/jsx_component.fl b/tests/fixtures/jsx_component.fl index 3958dff8..ad2fbda8 100644 --- a/tests/fixtures/jsx_component.fl +++ b/tests/fixtures/jsx_component.fl @@ -1,4 +1,4 @@ -import { useState, JSX } from "react" +import trusted { useState, JSX } from "react" export fn Counter() -> JSX.Element { const [_count, setCount] = useState(0) diff --git a/tests/fixtures/trusted_import.fl b/tests/fixtures/trusted_import.fl index 30f62193..d2c821e6 100644 --- a/tests/fixtures/trusted_import.fl +++ b/tests/fixtures/trusted_import.fl @@ -1,5 +1,5 @@ -import { capitalize, slugify } from "string-utils" +import trusted { capitalize, slugify } from "string-utils" -import { trim, fetchUser } from "some-lib" +import { trusted trim, fetchUser } from "some-lib" const name = capitalize("hello") diff --git a/tests/lsp/fixtures.py b/tests/lsp/fixtures.py index d43b0282..1bfaaa2a 100644 --- a/tests/lsp/fixtures.py +++ b/tests/lsp/fixtures.py @@ -188,7 +188,7 @@ class Foo {} COMPLETION_PIPE = "const nums = [1, 2, 3]\nconst result = nums |> \n" JSX_COMPONENT = """\ -import { useState } from "react" +import trusted { useState } from "react" export fn Counter() -> JSX.Element { const [count, setCount] = useState(0) @@ -337,7 +337,7 @@ class Foo {} """ DEEPLY_NESTED_JSX = """\ -import { useState } from "react" +import trusted { useState } from "react" export fn App() -> JSX.Element { const [items, setItems] = useState>([]) @@ -843,7 +843,7 @@ class Foo {} """ JSX_RENDER_PROP_PARAM = """\ -import { Draggable } from "@hello-pangea/dnd" +import trusted { Draggable } from "@hello-pangea/dnd" type Props { id: string } diff --git a/tests/lsp/test_completion.py b/tests/lsp/test_completion.py index 5a1fe210..e8f680e1 100644 --- a/tests/lsp/test_completion.py +++ b/tests/lsp/test_completion.py @@ -52,7 +52,7 @@ def test_match_arms_show_variants(self, lsp): class TestCompletionJsx: def test_jsx_attributes(self, lsp): - source = 'import { useState } from "react"\nexport fn App() -> JSX.Element {\n