Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: unified expression metadata #12644

Merged
merged 11 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions packages/svelte/src/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,23 @@ export function parse(source, { filename, rootDir, modern } = {}) {
*/
function to_public_ast(source, ast, modern) {
if (modern) {
const clean = (/** @type {any} */ node) => {
delete node.metadata;
delete node.parent;
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
};

ast.options?.attributes.forEach((attribute) => {
clean(attribute);
clean(attribute.value);
if (Array.isArray(attribute.value)) {
attribute.value.forEach(clean);
}
});

// remove things that we don't want to treat as public API
return zimmerframe_walk(ast, null, {
_(node, { next }) {
// @ts-ignore
delete node.parent;
// @ts-ignore
delete node.metadata;
clean(node);
next();
}
});
Expand Down
28 changes: 9 additions & 19 deletions packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute } from '../../nodes.js';
import { create_attribute, create_expression_metadata } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';

Expand Down Expand Up @@ -127,7 +127,7 @@ export default function element(parser) {

const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
: regex_capital_letter.test(name[0])
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
Expand All @@ -140,7 +140,7 @@ export default function element(parser) {
const element =
type === 'RegularElement'
? {
type: type,
type,
start,
end: -1,
name,
Expand All @@ -163,7 +163,7 @@ export default function element(parser) {
fragment: create_fragment(true),
parent: null,
metadata: {
svg: false
// unpopulated at first, differs between types
}
});

Expand Down Expand Up @@ -508,8 +508,7 @@ function read_attribute(parser) {
expression,
parent: null,
metadata: {
contains_call_expression: false,
dynamic: false
expression: create_expression_metadata()
}
};

Expand Down Expand Up @@ -538,8 +537,7 @@ function read_attribute(parser) {
},
parent: null,
metadata: {
dynamic: false,
contains_call_expression: false
expression: create_expression_metadata()
}
};

Expand Down Expand Up @@ -584,7 +582,7 @@ function read_attribute(parser) {
value,
parent: null,
metadata: {
dynamic: false
expression: create_expression_metadata()
}
};
}
Expand Down Expand Up @@ -616,17 +614,10 @@ function read_attribute(parser) {
modifiers,
expression,
metadata: {
dynamic: false,
contains_call_expression: false
expression: create_expression_metadata()
}
};

if (directive.type === 'ClassDirective') {
directive.metadata = {
dynamic: false
};
}

if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
Expand Down Expand Up @@ -789,8 +780,7 @@ function read_sequence(parser, done, location) {
expression,
parent: null,
metadata: {
contains_call_expression: false,
dynamic: false
expression: create_expression_metadata()
}
};

Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/compiler/phases/1-parse/state/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
import { create_fragment } from '../utils/create.js';
import { walk } from 'zimmerframe';
import { parse_expression_at } from '../acorn.js';
import { create_expression_metadata } from '../../nodes.js';

const regex_whitespace_with_closing_curly_brace = /^\s*}/;

Expand Down Expand Up @@ -39,8 +40,7 @@ export default function tag(parser) {
end: parser.index,
expression,
metadata: {
contains_call_expression: false,
dynamic: false
expression: create_expression_metadata()
}
});
}
Expand Down
56 changes: 32 additions & 24 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1178,20 +1178,19 @@ const common_visitors = {

context.next();

node.metadata.dynamic = get_attribute_chunks(node.value).some((chunk) => {
if (chunk.type !== 'ExpressionTag') {
return false;
}
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;

if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
return false;
continue;
}

return chunk.metadata.dynamic || chunk.metadata.contains_call_expression;
});
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}

if (is_event_attribute(node)) {
const parent = context.path.at(-1);
Expand All @@ -1211,10 +1210,10 @@ const common_visitors = {
}
},
ClassDirective(node, context) {
context.next({ ...context.state, expression: node });
context.next({ ...context.state, expression: node.metadata.expression });
},
SpreadAttribute(node, context) {
context.next({ ...context.state, expression: node });
context.next({ ...context.state, expression: node.metadata.expression });
},
SlotElement(node, context) {
let name = 'default';
Expand All @@ -1230,17 +1229,21 @@ const common_visitors = {
if (node.value === true) {
const binding = context.state.scope.get(node.name);
if (binding?.kind !== 'normal') {
node.metadata.dynamic = true;
node.metadata.expression.has_state = true;
}
} else {
context.next();
node.metadata.dynamic = get_attribute_chunks(node.value).some(
(node) => node.type === 'ExpressionTag' && node.metadata.dynamic
);

for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;

node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
}
},
ExpressionTag(node, context) {
context.next({ ...context.state, expression: node });
context.next({ ...context.state, expression: node.metadata.expression });
},
Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
Expand All @@ -1261,10 +1264,18 @@ const common_visitors = {

const binding = context.state.scope.get(node.name);

if (binding && context.state.expression) {
context.state.expression.dependencies.add(binding);

if (binding.kind !== 'normal') {
context.state.expression.has_state = true;
}
}

// if no binding, means some global variable
if (binding && binding.kind !== 'normal') {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
context.state.expression.has_state = true;
}

// TODO it would be better to just bail out when we hit the ExportSpecifier node but that's
Expand Down Expand Up @@ -1297,13 +1308,10 @@ const common_visitors = {
},
CallExpression(node, context) {
const { expression, render_tag } = context.state;
if (
(expression?.type === 'ExpressionTag' ||
expression?.type === 'SpreadAttribute' ||
expression?.type === 'OnDirective') &&
!is_known_safe_call(node, context)
) {
expression.metadata.contains_call_expression = true;

if (expression && !is_known_safe_call(node, context)) {
expression.has_call = true;
expression.has_state = true;
}

if (render_tag) {
Expand Down Expand Up @@ -1354,7 +1362,7 @@ const common_visitors = {
},
MemberExpression(node, context) {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
context.state.expression.has_state = true;
}

if (!is_safe_identifier(node, context.state.scope)) {
Expand All @@ -1368,7 +1376,7 @@ const common_visitors = {
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
state.analysis.event_directive_node ??= node;
}
next({ ...state, expression: node });
next({ ...state, expression: node.metadata.expression });
},
BindDirective(node, context) {
let i = context.path.length;
Expand Down
5 changes: 3 additions & 2 deletions packages/svelte/src/compiler/phases/2-analyze/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type {
ClassDirective,
ExpressionMetadata,
ExpressionTag,
OnDirective,
RenderTag,
Expand All @@ -20,8 +21,8 @@ export interface AnalysisState {
has_props_rune: boolean;
/** Which slots the current parent component has */
component_slots: Set<string>;
/** The current {expression}, if any */
expression: ExpressionTag | ClassDirective | OnDirective | SpreadAttribute | null;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
/** The current {@render ...} tag, if any */
render_tag: null | RenderTag;
private_derived_state: string[];
Expand Down
Loading
Loading