feat: function called as tagged template literal is reactively called (#12692)

* feat: function called as tagged template literal is reactively called

Co-authored-by: Oscar Dominguez <dominguez.celada@gmail.com>

* chore: re-organize import of visitors

* simplify

---------

Co-authored-by: Oscar Dominguez <dominguez.celada@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12707/head
Paolo Ricciuti 5 months ago committed by GitHub
parent e4e66e237f
commit 3286617e3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: function called as tagged template literal is reactively called

@ -57,6 +57,7 @@ import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js'; import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js'; import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteSelf } from './visitors/SvelteSelf.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js'; import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js'; import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
@ -160,6 +161,7 @@ const visitors = {
SvelteFragment, SvelteFragment,
SvelteComponent, SvelteComponent,
SvelteSelf, SvelteSelf,
TaggedTemplateExpression,
Text, Text,
TitleElement, TitleElement,
UpdateExpression, UpdateExpression,

@ -4,7 +4,7 @@
import { get_rune } from '../../scope.js'; import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { get_parent, unwrap_optional } from '../../../utils/ast.js'; import { get_parent, unwrap_optional } from '../../../utils/ast.js';
import { is_safe_identifier } from './shared/utils.js'; import { is_known_safe_call, is_safe_identifier } from './shared/utils.js';
/** /**
* @param {CallExpression} node * @param {CallExpression} node
@ -150,7 +150,7 @@ export function CallExpression(node, context) {
break; break;
} }
if (context.state.expression && !is_known_safe_call(node, context)) { if (context.state.expression && !is_known_safe_call(node.callee, context)) {
context.state.expression.has_call = true; context.state.expression.has_call = true;
context.state.expression.has_state = true; context.state.expression.has_state = true;
} }
@ -182,28 +182,3 @@ export function CallExpression(node, context) {
context.next(); context.next();
} }
} }
/**
* @param {CallExpression} node
* @param {Context} context
* @returns {boolean}
*/
function is_known_safe_call(node, context) {
const callee = node.callee;
// String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') {
const name = callee.name;
const binding = context.state.scope.get(name);
if (
binding === null &&
(name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean')
) {
return true;
}
}
// TODO add more cases
return false;
}

@ -0,0 +1,23 @@
/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */
/** @import { Context } from '../types' */
import { is_known_safe_call } from './shared/utils.js';
/**
* @param {TaggedTemplateExpression} node
* @param {Context} context
*/
export function TaggedTemplateExpression(node, context) {
if (context.state.expression && !is_known_safe_call(node.tag, context)) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
}
if (node.tag.type === 'Identifier') {
const binding = context.state.scope.get(node.tag.name);
if (binding !== null) {
binding.is_called = true;
}
}
context.next();
}

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { AssignmentExpression, CallExpression, Expression, Pattern, PrivateIdentifier, Super, TaggedTemplateExpression, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { Fragment } from '#compiler' */ /** @import { Fragment } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */ /** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
@ -165,3 +165,26 @@ export function is_safe_identifier(expression, scope) {
binding.kind !== 'rest_prop' binding.kind !== 'rest_prop'
); );
} }
/**
* @param {Expression | Super} callee
* @param {Context} context
* @returns {boolean}
*/
export function is_known_safe_call(callee, context) {
// String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') {
const name = callee.name;
const binding = context.state.scope.get(name);
if (
binding === null &&
(name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean')
) {
return true;
}
}
// TODO add more cases
return false;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target, ok }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `0 <button></button>`);
flushSync(() => {
button?.click();
});
assert.htmlEqual(target.innerHTML, `1 <button></button>`);
}
});

@ -0,0 +1,8 @@
<script>
let count = $state(0);
function showCount() {
return count;
}
</script>
{showCount``} <button onclick={() => count++}></button>
Loading…
Cancel
Save