oxlint-plugin-vue-sfc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import {
2
+ ELEMENT_COMPONENT,
3
+ NODE_ELEMENT,
4
+ PROP_DIRECTIVE,
5
+ parseSfc,
6
+ reportAtFileOffset,
7
+ walkTemplate,
8
+ } from "../utils/vue-sfc.mjs";
9
+
10
+ /**
11
+ * Replacement for `vue/v-on-event-hyphenation` (no native oxlint equivalent — oxc#15761).
12
+ *
13
+ * In-DOM templates lower-case attribute names, so `@myEvent` becomes `@myevent` and silently
14
+ * stops matching an `emit("myEvent")`. Hyphenated `@my-event` survives that normalization and
15
+ * Vue maps it back to the camelCase emit. Keeping one convention also stops the same event
16
+ * being listened to two ways in different files.
17
+ *
18
+ * Scope, deliberately narrow to avoid false positives:
19
+ * - COMPONENTS only (parser `tagType`, not a casing heuristic). Native DOM events are all
20
+ * lowercase already, and a custom element's events are not Vue's to rename.
21
+ * - Static event names only. `@[dynamic]` cannot be checked without evaluating the expression.
22
+ * - `v-on="obj"` (argument-less) has no single name and is skipped.
23
+ *
24
+ * This is `warn` upstream in our config, and stays `warn`: it is a convention with a real
25
+ * failure mode rather than a guaranteed bug, since SFC templates are not DOM-parsed.
26
+ */
27
+
28
+ /** camelCase / PascalCase detection: any uppercase letter in the event name. */
29
+ const HAS_UPPER = /[A-Z]/;
30
+
31
+ function hyphenate(name) {
32
+ return name.replace(/\B([A-Z])/g, "-$1").toLowerCase();
33
+ }
34
+
35
+ export default {
36
+ meta: {
37
+ type: "suggestion",
38
+ docs: {
39
+ description: "enforce `v-on` event naming style on custom components in template",
40
+ recommended: false,
41
+ },
42
+ schema: [],
43
+ messages: { m: "" },
44
+ },
45
+ create(context) {
46
+ if (!context.filename.endsWith(".vue")) {
47
+ return {};
48
+ }
49
+ return {
50
+ Program() {
51
+ const entry = parseSfc(context.filename);
52
+ const ast = entry.descriptor.template?.ast;
53
+ if (!ast) {
54
+ return;
55
+ }
56
+ walkTemplate(ast, (node) => {
57
+ if (node.type !== NODE_ELEMENT || node.tagType !== ELEMENT_COMPONENT) {
58
+ return;
59
+ }
60
+ for (const prop of node.props ?? []) {
61
+ if (prop.type !== PROP_DIRECTIVE || prop.name !== "on") {
62
+ continue;
63
+ }
64
+ const arg = prop.arg;
65
+ // Argument-less `v-on="obj"`, or a dynamic `@[name]` — nothing static
66
+ // to judge.
67
+ if (!arg || arg.type !== 4 /* SIMPLE_EXPRESSION */ || arg.isStatic === false) {
68
+ continue;
69
+ }
70
+ const name = arg.content;
71
+ if (!HAS_UPPER.test(name)) {
72
+ continue;
73
+ }
74
+ // Anchor on the whole directive so the column lands on the `@`/`v-on`,
75
+ // matching eslint-plugin-vue (verified at exact column parity).
76
+ reportAtFileOffset(
77
+ context,
78
+ entry,
79
+ prop.loc.start.offset,
80
+ prop.loc.end.offset,
81
+ `Use \`@${hyphenate(name)}\` instead of \`@${name}\` on <${node.tag}>: in-DOM templates lower-case attribute names, so the camelCase form silently stops matching the emit.`,
82
+ );
83
+ }
84
+ });
85
+ },
86
+ };
87
+ },
88
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Minimal lexical scope resolution for oxlint JS-plugin rules that work on the script AST.
3
+ *
4
+ * Why this exists: a name-only lookup is not good enough. The first cut of
5
+ * vue-no-ref-as-operand collected ref names globally and flagged every matching identifier,
6
+ * which produced 14 false positives on this repo — all the same shape:
7
+ *
8
+ * const giveTicker = ref(""); // module scope: a ref
9
+ * ...
10
+ * const giveTicker = parseMaybeArrayRouteParam(x); // inner scope: NOT a ref
11
+ * if (giveTicker && !receiveTicker) { … } // was wrongly reported
12
+ *
13
+ * So resolution has to respect shadowing. This is a deliberately small implementation — it
14
+ * models block/function/catch/loop scopes and the declaration forms that appear in SFC scripts,
15
+ * not the full ECMAScript scope algorithm.
16
+ *
17
+ * Known simplifications, all chosen to fail toward FALSE NEGATIVES rather than false positives:
18
+ * - `var` is treated as block-scoped rather than function-scoped. A `var` ref declared inside a
19
+ * block is therefore invisible outside it: missed reports, never wrong ones.
20
+ * - No hoisting. Declarations are visible from the point they are recorded onward within their
21
+ * scope, which matches `const`/`let` semantics (a use before declaration is a TDZ error) and
22
+ * under-reports for hoisted `var`/`function`.
23
+ * - Destructuring binds its names but never marks them as refs, since `const { a } = ref(…)` is
24
+ * not a ref-valued binding.
25
+ */
26
+
27
+ const SCOPE_NODES = new Set([
28
+ "Program",
29
+ "FunctionDeclaration",
30
+ "FunctionExpression",
31
+ "ArrowFunctionExpression",
32
+ "BlockStatement",
33
+ "StaticBlock",
34
+ "ForStatement",
35
+ "ForInStatement",
36
+ "ForOfStatement",
37
+ "CatchClause",
38
+ "ClassDeclaration",
39
+ "ClassExpression",
40
+ ]);
41
+
42
+ /** Collect every name bound by a binding pattern into `sink`. */
43
+ function collectPatternNames(pattern, sink) {
44
+ if (!pattern || typeof pattern !== "object") {
45
+ return;
46
+ }
47
+ switch (pattern.type) {
48
+ case "Identifier":
49
+ sink(pattern.name);
50
+ break;
51
+ case "ObjectPattern":
52
+ for (const property of pattern.properties ?? []) {
53
+ collectPatternNames(property.value ?? property.argument, sink);
54
+ }
55
+ break;
56
+ case "ArrayPattern":
57
+ for (const element of pattern.elements ?? []) {
58
+ collectPatternNames(element, sink);
59
+ }
60
+ break;
61
+ case "AssignmentPattern":
62
+ collectPatternNames(pattern.left, sink);
63
+ break;
64
+ case "RestElement":
65
+ collectPatternNames(pattern.argument, sink);
66
+ break;
67
+ default:
68
+ break;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Walk `program`, tracking lexical scopes, and report identifiers whose nearest binding
74
+ * satisfies `isTracked`.
75
+ *
76
+ * @param program the Program node from oxlint's `Program(node)` visitor
77
+ * @param isMarkedInit (init) => boolean — does this initializer make the binding "tracked"
78
+ * (for vue-no-ref-as-operand: is it a `ref()`/`computed()`/… call)
79
+ * @param onNode (node, scopeLookup) => void — called for every AST node, with a
80
+ * `scopeLookup(name)` that returns `true` when the nearest binding of `name` is tracked,
81
+ * `false` when it is shadowed by an untracked one, and `undefined` when unbound.
82
+ */
83
+ export function walkWithScopes(program, isMarkedInit, onNode) {
84
+ /** Stack of Map<name, boolean /* tracked *\/>, innermost last. */
85
+ const stack = [new Map()];
86
+
87
+ const lookup = (name) => {
88
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
89
+ const scope = stack[i];
90
+ if (scope.has(name)) {
91
+ return scope.get(name);
92
+ }
93
+ }
94
+ return undefined;
95
+ };
96
+
97
+ const declare = (name, tracked) => {
98
+ stack[stack.length - 1].set(name, tracked);
99
+ };
100
+
101
+ /** Record the bindings a node introduces INTO THE CURRENT scope, before descending. */
102
+ const recordDeclarations = (node) => {
103
+ switch (node.type) {
104
+ case "VariableDeclarator": {
105
+ if (node.id?.type === "Identifier") {
106
+ declare(node.id.name, Boolean(isMarkedInit(node.init)));
107
+ } else {
108
+ collectPatternNames(node.id, (name) => declare(name, false));
109
+ }
110
+ break;
111
+ }
112
+ case "FunctionDeclaration":
113
+ case "ClassDeclaration":
114
+ if (node.id?.type === "Identifier") {
115
+ declare(node.id.name, false);
116
+ }
117
+ break;
118
+ case "ImportDeclaration":
119
+ for (const specifier of node.specifiers ?? []) {
120
+ if (specifier.local?.type === "Identifier") {
121
+ declare(specifier.local.name, false);
122
+ }
123
+ }
124
+ break;
125
+ default:
126
+ break;
127
+ }
128
+ };
129
+
130
+ const walk = (node) => {
131
+ if (!node || typeof node !== "object") {
132
+ return;
133
+ }
134
+ if (Array.isArray(node)) {
135
+ for (const item of node) {
136
+ walk(item);
137
+ }
138
+ return;
139
+ }
140
+ if (typeof node.type !== "string") {
141
+ return;
142
+ }
143
+
144
+ const opensScope = SCOPE_NODES.has(node.type);
145
+ if (opensScope) {
146
+ stack.push(new Map());
147
+ // Parameters and the catch binding belong to the scope the node opens.
148
+ for (const param of node.params ?? []) {
149
+ collectPatternNames(param, (name) => declare(name, false));
150
+ }
151
+ if (node.type === "CatchClause") {
152
+ collectPatternNames(node.param, (name) => declare(name, false));
153
+ }
154
+ }
155
+
156
+ recordDeclarations(node);
157
+ onNode(node, lookup);
158
+
159
+ for (const key of Object.keys(node)) {
160
+ if (key === "parent" || key === "loc" || key === "range") {
161
+ continue;
162
+ }
163
+ walk(node[key]);
164
+ }
165
+
166
+ if (opensScope) {
167
+ stack.pop();
168
+ }
169
+ };
170
+
171
+ walk(program);
172
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Positioned extraction of the variables a Vue template DECLARES — `v-for` aliases and `v-slot`
3
+ * destructuring — shared by vue-no-unused-vars and vue-no-template-shadow.
4
+ *
5
+ * `utils/vue-sfc.mjs` already has `extractBindingNames`, but it returns names only and bails on
6
+ * any pattern containing `:` or `=`. Both rules here need the OFFSET of each binding (reports
7
+ * anchor on the identifier itself) and need to handle renames and defaults, so this is a
8
+ * separate, more capable extractor rather than a change to that one.
9
+ *
10
+ * It is a positioned scan, not a real pattern parse: identifiers in the alias text, minus those
11
+ * immediately followed by `:` (object-pattern KEYS, where the binding is the right-hand name).
12
+ * That covers `item`, `(item, i)`, `{ a, b }`, `[a, b]`, `{ a: renamed }` and `{ a = 1 }` —
13
+ * every shape in this codebase.
14
+ */
15
+
16
+ const IDENTIFIER_RE = /[A-Za-z_$][\w$]*/g;
17
+ /** Words that can appear in alias text but are never bindings. */
18
+ const RESERVED = new Set(["in", "of", "true", "false", "null", "undefined"]);
19
+
20
+ /**
21
+ * Identifiers bound by a `v-for` alias / `v-slot` pattern, with offsets inside `text`.
22
+ * @returns {{name: string, offset: number}[]}
23
+ */
24
+ export function bindingsWithOffsets(text) {
25
+ const found = [];
26
+ IDENTIFIER_RE.lastIndex = 0;
27
+ let match = IDENTIFIER_RE.exec(text);
28
+ while (match !== null) {
29
+ const name = match[0];
30
+ const isKey = /^\s*:/.test(text.slice(match.index + name.length));
31
+ if (!isKey && !RESERVED.has(name)) {
32
+ found.push({ name, offset: match.index });
33
+ }
34
+ match = IDENTIFIER_RE.exec(text);
35
+ }
36
+ return found;
37
+ }
38
+
39
+ /** The left-hand alias portion of a `v-for` expression. Returns null if there is no `in`/`of`. */
40
+ export function forAliasPart(content) {
41
+ const match = /\s+(?:in|of)\s+/.exec(content);
42
+ if (!match) {
43
+ return null;
44
+ }
45
+ return { text: content.slice(0, match.index), offset: 0 };
46
+ }
47
+
48
+ /**
49
+ * Split a `v-for` alias list into its positional groups, each with its offset in `text`.
50
+ * `(a, i)` -> two groups; `({ a }, i)` -> two, the first a destructuring pattern.
51
+ * Splits only on top-level commas, so nested `{}` / `[]` / `()` stay intact.
52
+ */
53
+ export function positionalGroups(text) {
54
+ const trimmed = text.trim();
55
+ const lead = text.indexOf(trimmed);
56
+ const wrapped = trimmed.startsWith("(") && trimmed.endsWith(")");
57
+ const inner = wrapped ? trimmed.slice(1, -1) : trimmed;
58
+ const base = lead + (wrapped ? 1 : 0);
59
+
60
+ const groups = [];
61
+ let depth = 0;
62
+ let start = 0;
63
+ for (let i = 0; i < inner.length; i += 1) {
64
+ const ch = inner[i];
65
+ if (ch === "{" || ch === "[" || ch === "(") {
66
+ depth += 1;
67
+ } else if (ch === "}" || ch === "]" || ch === ")") {
68
+ depth -= 1;
69
+ } else if (ch === "," && depth === 0) {
70
+ groups.push({ text: inner.slice(start, i), offset: base + start });
71
+ start = i + 1;
72
+ }
73
+ }
74
+ groups.push({ text: inner.slice(start), offset: base + start });
75
+ return groups;
76
+ }
77
+
78
+ /**
79
+ * The alias text a directive declares, or null if it declares nothing.
80
+ * `v-for` yields its left-hand side; `v-slot` yields its whole expression.
81
+ */
82
+ export function declaredAlias(prop) {
83
+ if (!prop.exp?.content) {
84
+ return null;
85
+ }
86
+ if (prop.name === "for") {
87
+ return forAliasPart(prop.exp.content);
88
+ }
89
+ if (prop.name === "slot") {
90
+ return { text: prop.exp.content, offset: 0 };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Top-level binding names declared by a `<script setup>` program — everything the template can
97
+ * reference. Used by vue-no-template-shadow to know what a template alias would shadow.
98
+ */
99
+ export function scriptTopLevelNames(program) {
100
+ const names = new Set();
101
+ const addPattern = (pattern) => {
102
+ if (!pattern || typeof pattern !== "object") {
103
+ return;
104
+ }
105
+ switch (pattern.type) {
106
+ case "Identifier":
107
+ names.add(pattern.name);
108
+ break;
109
+ case "ObjectPattern":
110
+ for (const property of pattern.properties ?? []) {
111
+ addPattern(property.value ?? property.argument);
112
+ }
113
+ break;
114
+ case "ArrayPattern":
115
+ for (const element of pattern.elements ?? []) {
116
+ addPattern(element);
117
+ }
118
+ break;
119
+ case "AssignmentPattern":
120
+ addPattern(pattern.left);
121
+ break;
122
+ case "RestElement":
123
+ addPattern(pattern.argument);
124
+ break;
125
+ default:
126
+ break;
127
+ }
128
+ };
129
+ for (const statement of program?.body ?? []) {
130
+ switch (statement.type) {
131
+ case "VariableDeclaration":
132
+ for (const declarator of statement.declarations ?? []) {
133
+ addPattern(declarator.id);
134
+ }
135
+ break;
136
+ case "FunctionDeclaration":
137
+ case "ClassDeclaration":
138
+ if (statement.id?.type === "Identifier") {
139
+ names.add(statement.id.name);
140
+ }
141
+ break;
142
+ case "ImportDeclaration":
143
+ for (const specifier of statement.specifiers ?? []) {
144
+ if (specifier.local?.type === "Identifier") {
145
+ names.add(specifier.local.name);
146
+ }
147
+ }
148
+ break;
149
+ default:
150
+ break;
151
+ }
152
+ }
153
+ return names;
154
+ }
@@ -0,0 +1,202 @@
1
+ import fs from "node:fs";
2
+
3
+ import { parse } from "@vue/compiler-sfc";
4
+
5
+ /**
6
+ * Helpers for writing oxlint JS-plugin rules against the Vue TEMPLATE AST.
7
+ *
8
+ * oxlint parses only the script block of an SFC and gives JS-plugin rules a
9
+ * script-only view: `context.sourceCode.text` is the script content and
10
+ * line/column locations passed to `context.report()` are validated against the
11
+ * script's line count (template lines are rejected with a RangeError).
12
+ *
13
+ * Two empirically verified escape hatches make template rules possible anyway:
14
+ * 1. `fs.readFileSync(context.filename)` works inside the sidecar, so a rule
15
+ * can parse the FULL SFC with @vue/compiler-sfc.
16
+ * 2. `context.report({ node: { range: [start, end] } })` accepts raw
17
+ * script-relative byte offsets WITHOUT line validation and maps them to
18
+ * file positions by adding the script block's offset. A template offset is
19
+ * therefore reachable only as `fileOffset - scriptStartOffset` when that is
20
+ * >= 0 — i.e. when <script> precedes <template>.
21
+ *
22
+ * THIS REPO IS ENTIRELY THE OPPOSITE: every SFC is template-first (verified:
23
+ * 1414 template-first, 0 script-first), so template offsets are always
24
+ * NEGATIVE. oxlint rejects negative ranges (and rejects line/column that fall
25
+ * outside the script block), so a template diagnostic CANNOT be rendered on
26
+ * its real template line — the code frame always falls back to the script
27
+ * block's first line (see reportAtFileOffset). This is an inherent limit of
28
+ * oxlint's script-only JS-plugin view of .vue files, not a bug we can fix
29
+ * here; we surface the true template line:column in the message text instead.
30
+ */
31
+
32
+ // One parse per file per lint run, shared across rules (module state lives for
33
+ // the lifetime of oxlint's JS-plugin sidecar process).
34
+ const cache = new Map();
35
+
36
+ export function parseSfc(filename) {
37
+ let entry = cache.get(filename);
38
+ if (!entry) {
39
+ const text = fs.readFileSync(filename, "utf8");
40
+ const { descriptor, errors } = parse(text, { filename });
41
+ entry = { text, descriptor, errors };
42
+ cache.set(filename, entry);
43
+ }
44
+ return entry;
45
+ }
46
+
47
+ /** Script block content start offset in the file (script setup preferred). */
48
+ export function scriptStartOffset(descriptor) {
49
+ const block = descriptor.scriptSetup ?? descriptor.script;
50
+ return block ? block.loc.start.offset : 0;
51
+ }
52
+
53
+ /**
54
+ * Report a diagnostic at an absolute FILE offset range by translating it into
55
+ * the script-relative range oxlint expects. Template positions BEFORE the
56
+ * script block can't be represented as ranges (negative offsets) — the norm in
57
+ * this template-first repo — so those fall back to the script block's first
58
+ * line with the real template line:column PREPENDED to the message, so the
59
+ * editor's problem list and the CLI still point you at the right place even
60
+ * though the squiggle is stuck on the script block.
61
+ */
62
+ export function reportAtFileOffset(context, entry, fileStart, fileEnd, message) {
63
+ const base = scriptStartOffset(entry.descriptor);
64
+ const start = fileStart - base;
65
+ if (start >= 0) {
66
+ context.report({
67
+ node: { type: "TemplateDiagnostic", range: [start, fileEnd - base] },
68
+ message,
69
+ });
70
+ return;
71
+ }
72
+ const before = entry.text.slice(0, fileStart);
73
+ const line = before.split("\n").length;
74
+ const column = fileStart - (before.lastIndexOf("\n") + 1) + 1;
75
+ context.report({
76
+ node: { type: "TemplateDiagnostic", range: [0, 1] },
77
+ message: `[template ${line}:${column}] ${message}`,
78
+ });
79
+ }
80
+
81
+ /** Depth-first walk over @vue/compiler-dom template AST element nodes. */
82
+ export function walkTemplate(node, visit) {
83
+ if (!node) {
84
+ return;
85
+ }
86
+ visit(node);
87
+ if (Array.isArray(node.children)) {
88
+ for (const child of node.children) {
89
+ walkTemplate(child, visit);
90
+ }
91
+ }
92
+ // v-if/v-else chains parsed standalone keep branches as siblings, but walk
93
+ // branches too in case a transformed AST is ever passed in.
94
+ if (Array.isArray(node.branches)) {
95
+ for (const branch of node.branches) {
96
+ walkTemplate(branch, visit);
97
+ }
98
+ }
99
+ }
100
+
101
+ /** @vue/compiler-dom NodeTypes used by rules (avoid importing internals). */
102
+ export const NODE_ELEMENT = 1;
103
+ export const NODE_TEXT = 2;
104
+ export const NODE_INTERPOLATION = 5;
105
+ export const PROP_ATTRIBUTE = 6;
106
+ export const PROP_DIRECTIVE = 7;
107
+ /** @vue/compiler-dom ElementTypes.COMPONENT */
108
+ export const ELEMENT_COMPONENT = 1;
109
+
110
+ /**
111
+ * Visit every expression in the template: interpolations (directiveName null)
112
+ * and directive expressions (directiveName e.g. "model", "bind", "if").
113
+ */
114
+ export function eachTemplateExpression(ast, visit) {
115
+ walkTemplate(ast, (node) => {
116
+ if (node.type === NODE_INTERPOLATION && node.content?.content) {
117
+ visit(node.content, null);
118
+ }
119
+ if (node.type === NODE_ELEMENT) {
120
+ for (const p of node.props ?? []) {
121
+ if (p.type === PROP_DIRECTIVE && p.exp?.content) {
122
+ visit(p.exp, p.name);
123
+ }
124
+ }
125
+ }
126
+ });
127
+ }
128
+
129
+ export function findDirective(el, name) {
130
+ return el.props?.find((p) => p.type === PROP_DIRECTIVE && p.name === name);
131
+ }
132
+
133
+ export function findKeyProp(el) {
134
+ return el.props?.find(
135
+ (p) =>
136
+ (p.type === PROP_ATTRIBUTE && p.name === "key") ||
137
+ (p.type === PROP_DIRECTIVE &&
138
+ p.name === "bind" &&
139
+ p.arg?.type === 4 /* SIMPLE_EXPRESSION */ &&
140
+ p.arg.content === "key"),
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Extract variable names bound by a v-for / v-slot expression ("item",
146
+ * "(item, index)", "{ row }"). Returns null for shapes with renames or
147
+ * defaults (`{ a: b }`, `{ a = 1 }`) — identifying which side is the binding
148
+ * needs a real parser, and a wrong guess produces false positives downstream.
149
+ */
150
+ export function extractBindingNames(exprText) {
151
+ const left = exprText.split(/\s+(?:in|of)\s+/)[0] ?? exprText;
152
+ if (/[:=]/.test(left)) {
153
+ return null;
154
+ }
155
+ const names = [...left.matchAll(/[A-Za-z_$][\w$]*/g)].map((m) => m[0]);
156
+ return names.length ? names : null;
157
+ }
158
+
159
+ /**
160
+ * Collect every identifier bound by a JS destructuring pattern
161
+ * (`a`, `{ a, b }`, `[a, b]`, `a = 1`, `...rest`) into `into`. Script-side rules use
162
+ * this to know which names a declaration / parameter / catch clause introduces.
163
+ */
164
+ export const collectPatternNames = (pattern, into) => {
165
+ if (!pattern) {
166
+ return;
167
+ }
168
+ switch (pattern.type) {
169
+ case "Identifier":
170
+ into.add(pattern.name);
171
+ break;
172
+ case "ObjectPattern":
173
+ for (const p of pattern.properties ?? []) {
174
+ collectPatternNames(p.value ?? p.argument, into);
175
+ }
176
+ break;
177
+ case "ArrayPattern":
178
+ for (const el of pattern.elements ?? []) {
179
+ collectPatternNames(el, into);
180
+ }
181
+ break;
182
+ case "AssignmentPattern":
183
+ collectPatternNames(pattern.left, into);
184
+ break;
185
+ case "RestElement":
186
+ collectPatternNames(pattern.argument, into);
187
+ break;
188
+ default:
189
+ break;
190
+ }
191
+ };
192
+
193
+ /** Set of static `v-on` event names (`@click`, `v-on:click`) on a template element. */
194
+ export function eventNames(el) {
195
+ const names = new Set();
196
+ for (const p of el.props ?? []) {
197
+ if (p.type === PROP_DIRECTIVE && p.name === "on" && p.arg?.type === 4 /* SIMPLE */) {
198
+ names.add(p.arg.content);
199
+ }
200
+ }
201
+ return names;
202
+ }