Skip to content
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
30 changes: 15 additions & 15 deletions packages/action/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54632,7 +54632,7 @@ function attrTextOf(attr) {
}
function unwrapTs(expr) {
let cur = expr;
while (cur?.type === "TSSatisfiesExpression" || cur?.type === "TSAsExpression" || cur?.type === "TSNonNullExpression")
while (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression")
cur = cur.expression;
return cur;
}
Expand All @@ -54642,42 +54642,42 @@ function isLengthOnlyArrayCall(expr) {
if ((e2.type === "CallExpression" || e2.type === "NewExpression") && e2.callee?.type === "Identifier" && e2.callee.name === "Array") {
return (e2.arguments?.length ?? 0) === 1;
}
if (e2.type === "CallExpression" && e2.callee?.type === "MemberExpression" && !e2.callee.computed && e2.callee.object?.type === "Identifier" && e2.callee.object.name === "Array" && e2.callee.property?.name === "from" && e2.arguments?.[0]?.type === "ObjectExpression") {
if (e2.type === "CallExpression" && e2.callee?.type === "MemberExpression" && !e2.callee.computed && e2.callee.object?.type === "Identifier" && e2.callee.object.name === "Array" && e2.callee.property.type === "Identifier" && e2.callee.property.name === "from" && e2.arguments?.[0]?.type === "ObjectExpression") {
return (e2.arguments[0].properties ?? []).some(
(p) => p?.type === "Property" && !p.computed && (p.key?.name === "length" || p.key?.value === "length")
);
}
return false;
}
function isIdentityFreeEach(node) {
const expr = unwrapTs(node?.expression);
if (expr?.type === "ArrayExpression" && Array.isArray(expr.elements)) {
const expr = unwrapTs(node.expression);
if (expr.type === "ArrayExpression" && Array.isArray(expr.elements)) {
return expr.elements.every((el) => el?.type !== "SpreadElement" || isLengthOnlyArrayCall(el.argument));
}
return isLengthOnlyArrayCall(expr);
}
function isIndexExpression(expr, index) {
const e2 = unwrapTs(expr);
if (e2?.type === "Identifier") return e2.name === index;
if (e2?.type === "CallExpression") {
if (e2.type === "Identifier") return e2.name === index;
if (e2.type === "CallExpression") {
const callee = e2.callee;
if (callee?.type === "Identifier" && (callee.name === "String" || callee.name === "Number") && e2.arguments?.length === 1) {
if (callee.type === "Identifier" && (callee.name === "String" || callee.name === "Number") && e2.arguments.length === 1) {
return isIndexExpression(e2.arguments[0], index);
}
if (callee?.type === "MemberExpression" && !callee.computed && callee.property?.name === "toString" && (e2.arguments?.length ?? 0) === 0) {
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "toString" && e2.arguments.length === 0) {
return isIndexExpression(callee.object, index);
}
return false;
}
if (e2?.type === "TemplateLiteral") {
const exprs = e2.expressions ?? [];
if (e2.type === "TemplateLiteral") {
const exprs = e2.expressions;
if (exprs.length !== 1) return false;
const hasText = (e2.quasis ?? []).some((q) => (q?.value?.cooked ?? q?.value?.raw ?? "") !== "");
const hasText = e2.quasis.some((q) => (q.value.cooked ?? q.value.raw) !== "");
if (hasText) return false;
return isIndexExpression(exprs[0], index);
}
if (e2?.type === "BinaryExpression" && e2.operator === "+") {
const emptyString = (n) => n?.type === "Literal" && n.value === "";
if (e2.type === "BinaryExpression" && e2.operator === "+") {
const emptyString = (n) => n.type === "Literal" && n.value === "";
if (emptyString(e2.left)) return isIndexExpression(e2.right, index);
if (emptyString(e2.right)) return isIndexExpression(e2.left, index);
}
Expand Down Expand Up @@ -55766,14 +55766,14 @@ function isParentCall(arg) {
if (e2?.type !== "CallExpression") return false;
const callee = e2.callee;
if (callee?.type === "Identifier" && callee.name === "parent") return true;
return callee?.type === "MemberExpression" && !callee.computed && callee.property?.name === "parent";
return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "parent";
}
var BODY_METHODS = /* @__PURE__ */ new Set(["json", "text", "blob", "arrayBuffer", "formData", "bytes"]);
function isBodyParseCall(arg) {
const e2 = unwrapTs(arg);
if (e2?.type !== "CallExpression" || e2.arguments?.length) return false;
const callee = e2.callee;
return callee?.type === "MemberExpression" && !callee.computed && BODY_METHODS.has(callee.property?.name);
return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && BODY_METHODS.has(callee.property.name);
}
function refsTainted(node, tainted) {
let hit = false;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"dependencies": {
"svelte": "catalog:"
},
"devDependencies": {
"@types/estree": "catalog:"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand Down
82 changes: 55 additions & 27 deletions packages/core/src/component-parse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { parse } from 'svelte/compiler';
import type { Expression } from 'estree';
import type { AST } from 'svelte/compiler';
import type {
BrowserGlobalRefFact,
ComponentFacts,
Expand All @@ -18,16 +20,40 @@ import { CHILD_NODE_KEYS, lineOf, findAttr, attrTextOf } from './svelte-ast.js';
/* oxlint-disable @typescript-eslint/no-explicit-any */
type Node = any;

// TypeScript wrapper expressions the Svelte script parser emits for `x satisfies T` /
// `x as T` / `x!` — not part of estree's own type set, so declared here. `unwrapTs`
// is shared with the Kit-module and Vite-config parsers, hence exported alongside them.
export interface TSSatisfiesExpression {
type: 'TSSatisfiesExpression';
start: number;
end: number;
expression: TsExpression;
}
export interface TSAsExpression {
type: 'TSAsExpression';
start: number;
end: number;
expression: TsExpression;
}
export interface TSNonNullExpression {
type: 'TSNonNullExpression';
start: number;
end: number;
expression: TsExpression;
}
/** An estree `Expression`, optionally wrapped in one or more of the three TS wrappers above. */
export type TsExpression = Expression | TSSatisfiesExpression | TSAsExpression | TSNonNullExpression;

/** Unwrap TS wrapper expressions (`x satisfies T`, `x as T`, `x!`) to the underlying expression. Shared with the Kit-module and Vite-config parsers. */
export function unwrapTs(expr: Node): Node {
export function unwrapTs(expr: TsExpression): Expression {
let cur = expr;
while (cur?.type === 'TSSatisfiesExpression' || cur?.type === 'TSAsExpression' || cur?.type === 'TSNonNullExpression')
while (cur.type === 'TSSatisfiesExpression' || cur.type === 'TSAsExpression' || cur.type === 'TSNonNullExpression')
cur = cur.expression;
return cur;
}

/** Whether `expr` is a length-only list constructor: `Array(n)` / `new Array(n)` (single argument = length semantics) or `Array.from({ length: n }, …)`. */
function isLengthOnlyArrayCall(expr: Node): boolean {
function isLengthOnlyArrayCall(expr: TsExpression): boolean {
const e = unwrapTs(expr);
if (!e) return false;
if (
Expand All @@ -43,7 +69,8 @@ function isLengthOnlyArrayCall(expr: Node): boolean {
!e.callee.computed &&
e.callee.object?.type === 'Identifier' &&
e.callee.object.name === 'Array' &&
e.callee.property?.name === 'from' &&
e.callee.property.type === 'Identifier' &&
e.callee.property.name === 'from' &&
e.arguments?.[0]?.type === 'ObjectExpression'
) {
return (e.arguments[0].properties ?? []).some(
Expand All @@ -61,10 +88,10 @@ function isLengthOnlyArrayCall(expr: Node): boolean {
* length-only list. Such blocks are skipped entirely — neither each-key nor
* each-index-key can give useful advice on them.
*/
function isIdentityFreeEach(node: Node): boolean {
const expr = unwrapTs(node?.expression);
if (expr?.type === 'ArrayExpression' && Array.isArray(expr.elements)) {
return expr.elements.every((el: Node) => el?.type !== 'SpreadElement' || isLengthOnlyArrayCall(el.argument));
function isIdentityFreeEach(node: AST.EachBlock): boolean {
const expr = unwrapTs(node.expression);
if (expr.type === 'ArrayExpression' && Array.isArray(expr.elements)) {
return expr.elements.every((el) => el?.type !== 'SpreadElement' || isLengthOnlyArrayCall(el.argument));
}
return isLengthOnlyArrayCall(expr);
}
Expand All @@ -77,39 +104,40 @@ function isIdentityFreeEach(node: Node): boolean {
* and NOT matched: composite keys may be a deliberate uniqueness workaround for
* duplicate items.
*/
function isIndexExpression(expr: Node, index: string): boolean {
function isIndexExpression(expr: TsExpression, index: string): boolean {
const e = unwrapTs(expr);
if (e?.type === 'Identifier') return e.name === index;
if (e?.type === 'CallExpression') {
if (e.type === 'Identifier') return e.name === index;
if (e.type === 'CallExpression') {
const callee = e.callee;
if (
callee?.type === 'Identifier' &&
callee.type === 'Identifier' &&
(callee.name === 'String' || callee.name === 'Number') &&
e.arguments?.length === 1
e.arguments.length === 1
) {
return isIndexExpression(e.arguments[0], index);
return isIndexExpression(e.arguments[0] as Expression, index);
}
if (
callee?.type === 'MemberExpression' &&
callee.type === 'MemberExpression' &&
!callee.computed &&
callee.property?.name === 'toString' &&
(e.arguments?.length ?? 0) === 0
callee.property.type === 'Identifier' &&
callee.property.name === 'toString' &&
e.arguments.length === 0
) {
return isIndexExpression(callee.object, index);
return isIndexExpression(callee.object as Expression, index);
}
return false;
}
if (e?.type === 'TemplateLiteral') {
const exprs = e.expressions ?? [];
if (e.type === 'TemplateLiteral') {
const exprs = e.expressions;
if (exprs.length !== 1) return false;
const hasText = (e.quasis ?? []).some((q: Node) => (q?.value?.cooked ?? q?.value?.raw ?? '') !== '');
const hasText = e.quasis.some((q) => (q.value.cooked ?? q.value.raw) !== '');
if (hasText) return false;
return isIndexExpression(exprs[0], index);
return isIndexExpression(exprs[0]!, index);
}
if (e?.type === 'BinaryExpression' && e.operator === '+') {
const emptyString = (n: Node) => n?.type === 'Literal' && n.value === '';
if (emptyString(e.left)) return isIndexExpression(e.right, index);
if (emptyString(e.right)) return isIndexExpression(e.left, index);
if (e.type === 'BinaryExpression' && e.operator === '+') {
const emptyString = (n: Expression): boolean => n.type === 'Literal' && n.value === '';
if (emptyString(e.left as Expression)) return isIndexExpression(e.right, index);
if (emptyString(e.right)) return isIndexExpression(e.left as Expression, index);
}
return false;
}
Expand All @@ -122,7 +150,7 @@ function isIndexExpression(expr: Node, index: string): boolean {
* merely CONTAIN the index add uniqueness and are never matched
* (correctness/each-index-key).
*/
function isIndexKey(each: Node): boolean {
function isIndexKey(each: AST.EachBlock): boolean {
if (typeof each.index !== 'string' || each.key == null) return false;
return isIndexExpression(each.key, each.index);
}
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/kit-module-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,12 @@ function isParentCall(arg: Node): boolean {
if (e?.type !== 'CallExpression') return false;
const callee = e.callee;
if (callee?.type === 'Identifier' && callee.name === 'parent') return true;
return callee?.type === 'MemberExpression' && !callee.computed && callee.property?.name === 'parent';
return (
callee?.type === 'MemberExpression' &&
!callee.computed &&
callee.property.type === 'Identifier' &&
callee.property.name === 'parent'
);
}

const BODY_METHODS = new Set(['json', 'text', 'blob', 'arrayBuffer', 'formData', 'bytes']);
Expand All @@ -226,7 +231,12 @@ function isBodyParseCall(arg: Node): boolean {
const e = unwrapTs(arg);
if (e?.type !== 'CallExpression' || e.arguments?.length) return false;
const callee = e.callee;
return callee?.type === 'MemberExpression' && !callee.computed && BODY_METHODS.has(callee.property?.name);
return (
callee?.type === 'MemberExpression' &&
!callee.computed &&
callee.property.type === 'Identifier' &&
BODY_METHODS.has(callee.property.name)
);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.