From bac242f18104044da0d5ed0089c680c183f845fb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 28 Nov 2023 12:02:38 +0100 Subject: [PATCH] fix: handle ts expressions when dealing with runes related to #9639 --- .changeset/twelve-onions-juggle.md | 5 ++ .../src/compiler/phases/2-analyze/index.js | 8 +-- .../compiler/phases/2-analyze/validation.js | 11 ++-- .../client/visitors/javascript-runes.js | 6 ++- .../3-transform/server/transform-server.js | 12 +++-- packages/svelte/src/compiler/utils/ast.js | 53 +++++++++++++++++++ .../typescript-as-expression/_config.js | 5 ++ .../typescript-as-expression/main.svelte | 6 +++ .../typescript-non-null-expression/_config.js | 5 ++ .../main.svelte | 6 +++ 10 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 .changeset/twelve-onions-juggle.md create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte diff --git a/.changeset/twelve-onions-juggle.md b/.changeset/twelve-onions-juggle.md new file mode 100644 index 0000000000..00018fce38 --- /dev/null +++ b/.changeset/twelve-onions-juggle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle ts expressions when dealing with runes diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index a278c89272..4b64b24ee0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -5,6 +5,7 @@ import * as assert from '../../utils/assert.js'; import { extract_identifiers, extract_paths, + get_call_expression, is_event_attribute, is_text_attribute, object @@ -660,10 +661,11 @@ const runes_scope_js_tweaker = { /** @type {import('./types').Visitors} */ const runes_scope_tweaker = { VariableDeclarator(node, { state }) { - if (node.init?.type !== 'CallExpression') return; - if (get_rune(node.init, state.scope) === null) return; + const init = get_call_expression(node); + if (init === null) return; + if (get_rune(init, state.scope) === null) return; - const callee = node.init.callee; + const callee = init.callee; if (callee.type !== 'Identifier') return; const name = callee.name; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index e871808c2c..c044c36023 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1,5 +1,10 @@ import { error } from '../../errors.js'; -import { extract_identifiers, is_text_attribute } from '../../utils/ast.js'; +import { + extract_identifiers, + get_parent, + is_text_attribute, + unwrap_ts_expression +} from '../../utils/ast.js'; import { warn } from '../../warnings.js'; import fuzzymatch from '../1-parse/utils/fuzzymatch.js'; import { binding_properties } from '../bindings.js'; @@ -491,7 +496,7 @@ function validate_call_expression(node, scope, path) { const rune = get_rune(node, scope); if (rune === null) return; - const parent = /** @type {import('#compiler').SvelteNode} */ (path.at(-1)); + const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1)); if (rune === '$props') { if (parent.type === 'VariableDeclarator') return; @@ -703,7 +708,7 @@ export const validation_runes = merge(validation, a11y_validators, { next({ ...state }); }, VariableDeclarator(node, { state }) { - const init = node.init; + const init = unwrap_ts_expression(node.init); const rune = get_rune(init, state.scope); if (rune === null) return; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index f3b0d2b908..1f027dfe02 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -3,6 +3,7 @@ import { is_hoistable_function } from '../../utils.js'; import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; import { create_state_declarators, get_props_method } from '../utils.js'; +import { unwrap_ts_expression } from '../../../../utils/ast.js'; /** @type {import('../types.js').ComponentVisitors} */ export const javascript_visitors_runes = { @@ -133,7 +134,7 @@ export const javascript_visitors_runes = { const declarations = []; for (const declarator of node.declarations) { - const init = declarator.init; + const init = unwrap_ts_expression(declarator.init); const rune = get_rune(init, state.scope); if (!rune || rune === '$effect.active' || rune === '$effect.root') { if (init != null && is_hoistable_function(init)) { @@ -208,7 +209,8 @@ export const javascript_visitors_runes = { // TODO continue; } - const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments; + + const args = /** @type {import('estree').CallExpression} */ (init).arguments; const value = args.length === 0 ? b.id('undefined') diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index a3fff51495..cdb5f70069 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1,6 +1,11 @@ import { walk } from 'zimmerframe'; import { set_scope, get_rune } from '../../scope.js'; -import { extract_identifiers, extract_paths, is_event_attribute } from '../../../utils/ast.js'; +import { + extract_identifiers, + extract_paths, + is_event_attribute, + unwrap_ts_expression +} from '../../../utils/ast.js'; import * as b from '../../../utils/builders.js'; import is_reference from 'is-reference'; import { @@ -568,7 +573,8 @@ const javascript_visitors_runes = { const declarations = []; for (const declarator of node.declarations) { - const rune = get_rune(declarator.init, state.scope); + const init = unwrap_ts_expression(declarator.init); + const rune = get_rune(init, state.scope); if (!rune || rune === '$effect.active') { declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); continue; @@ -579,7 +585,7 @@ const javascript_visitors_runes = { continue; } - const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments; + const args = /** @type {import('estree').CallExpression} */ (init).arguments; const value = args.length === 0 ? b.id('undefined') diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index cf8b859bfc..304c97719a 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -265,3 +265,56 @@ function _extract_paths(assignments = [], param, expression, update_expression) return assignments; } + +/** + * If the init of the declarator is a call expression, returns it. + * + * @param {import('estree').VariableDeclarator} node + */ +export function get_call_expression(node) { + let init = node.init; + if (init) { + init = unwrap_ts_expression(init); + if (init.type === 'CallExpression') { + return init; + } + } + + return null; +} + +/** + * The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and + * `foo as Bar` as a `TSAsExpression` node. This function unwraps those. + * + * @template {import('#compiler').SvelteNode | undefined | null} T + * @param {T} node + * @returns {T} + */ +export function unwrap_ts_expression(node) { + if (!node) { + return node; + } + + // @ts-expect-error these types don't exist on the base estree types + if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') { + // @ts-expect-error + return node.expression; + } + + return node; +} + +/** + * @template {import('#compiler').SvelteNode} T + * @param {T[]} path + * @param {number} at + */ +export function get_parent(path, at) { + let node = path.at(at); + // @ts-expect-error + if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') { + return /** @type {T} */ (path.at(at < 0 ? at - 1 : at + 1)); + } + return /** @type {T} */ (node); +} diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js new file mode 100644 index 0000000000..4fd52f2d54 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '1 2' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte new file mode 100644 index 0000000000..d80746fda9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte @@ -0,0 +1,6 @@ + + +{count} {double} diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js new file mode 100644 index 0000000000..4fd52f2d54 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '1 2' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte new file mode 100644 index 0000000000..b93d1e5ee8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte @@ -0,0 +1,6 @@ + + +{count} {double}