oxlint-plugin-vue-sfc 0.1.2 → 0.2.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.
- package/package.json +3 -2
- package/rules/no-child-content.mjs +1 -1
- package/rules/no-dupe-v-else-if.mjs +1 -1
- package/rules/no-duplicate-attributes.mjs +1 -1
- package/rules/no-lone-template.mjs +1 -1
- package/rules/no-mutating-props.mjs +1 -1
- package/rules/no-parsing-error.mjs +1 -1
- package/rules/no-raw-text.mjs +1 -1
- package/rules/no-template-key.mjs +1 -1
- package/rules/no-template-shadow.mjs +1 -1
- package/rules/no-textarea-mustache.mjs +1 -1
- package/rules/no-unused-components.mjs +1 -1
- package/rules/no-unused-vars.mjs +1 -1
- package/rules/no-use-v-if-with-v-for.mjs +1 -1
- package/rules/no-useless-template-attributes.mjs +1 -1
- package/rules/no-v-text-v-html-on-component.mjs +1 -1
- package/rules/require-component-is.mjs +1 -1
- package/rules/require-explicit-emits.mjs +1 -1
- package/rules/require-toggle-inside-transition.mjs +1 -1
- package/rules/require-v-for-key.mjs +1 -1
- package/rules/this-in-template.mjs +1 -1
- package/rules/use-v-on-exact.mjs +1 -1
- package/rules/v-on-event-hyphenation.mjs +1 -1
- package/utils/vue-bindings.mjs +1 -1
- package/utils/vue-sfc.mjs +0 -192
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-vue-sfc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vue SFC template rules for oxlint, as a JS plugin. Ports of eslint-plugin-vue rules oxlint has no native equivalent for.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"test": "node --test \"rules/*.spec.mjs\""
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@vue/compiler-sfc": "^3.5.0"
|
|
45
|
+
"@vue/compiler-sfc": "^3.5.0",
|
|
46
|
+
"oxlint-vue-sfc-harness": "^0.1.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"oxc-parser": "^0.90.0",
|
package/rules/no-raw-text.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NODE_ELEMENT, NODE_TEXT, parseSfc, reportAtFileOffset } from "
|
|
1
|
+
import { NODE_ELEMENT, NODE_TEXT, parseSfc, reportAtFileOffset } from "oxlint-vue-sfc-harness";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Replacement for `@intlify/vue-i18n/no-raw-text` (no native oxlint equivalent —
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NODE_ELEMENT, PROP_DIRECTIVE, parseSfc, walkTemplate } from "
|
|
1
|
+
import { NODE_ELEMENT, PROP_DIRECTIVE, parseSfc, walkTemplate } from "oxlint-vue-sfc-harness";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Replacement for `vue/no-unused-components` (no native oxlint equivalent — oxc#15761).
|
package/rules/no-unused-vars.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eachTemplateExpression, parseSfc, reportAtFileOffset } from "
|
|
1
|
+
import { eachTemplateExpression, parseSfc, reportAtFileOffset } from "oxlint-vue-sfc-harness";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Replacement for `vue/this-in-template` (no native oxlint equivalent — oxc#15761).
|
package/rules/use-v-on-exact.mjs
CHANGED
package/utils/vue-bindings.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Positioned extraction of the variables a Vue template DECLARES — `v-for` aliases and `v-slot`
|
|
3
3
|
* destructuring — shared by vue-no-unused-vars and vue-no-template-shadow.
|
|
4
4
|
*
|
|
5
|
-
* `
|
|
5
|
+
* `oxlint-vue-sfc-harness` already has `extractBindingNames`, but it returns names only and bails on
|
|
6
6
|
* any pattern containing `:` or `=`. Both rules here need the OFFSET of each binding (reports
|
|
7
7
|
* anchor on the identifier itself) and need to handle renames and defaults, so this is a
|
|
8
8
|
* separate, more capable extractor rather than a change to that one.
|
package/utils/vue-sfc.mjs
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
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
|
-
|