vue3-migration 1.1.1 → 1.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
CHANGED
|
@@ -4,7 +4,7 @@ Mixin analysis — extract members and lifecycle hooks from Vue mixin source cod
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
|
-
from .js_parser import extract_brace_block, extract_property_names
|
|
7
|
+
from .js_parser import extract_brace_block, extract_property_names, skip_non_code
|
|
8
8
|
|
|
9
9
|
VUE_LIFECYCLE_HOOKS = [
|
|
10
10
|
"beforeCreate", "created", "beforeMount", "mounted",
|
|
@@ -46,6 +46,103 @@ def extract_mixin_members(source: str) -> dict[str, list[str]]:
|
|
|
46
46
|
return members
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def find_external_this_refs(
|
|
50
|
+
code: str, own_member_names: list[str]
|
|
51
|
+
) -> list[str]:
|
|
52
|
+
"""Find ``this.X`` references where X is NOT a member of the mixin itself.
|
|
53
|
+
|
|
54
|
+
Skips:
|
|
55
|
+
- Members in *own_member_names* (data, computed, methods, watch of this mixin)
|
|
56
|
+
- ``this.$xxx`` references (handled by the ``this.$`` warning system)
|
|
57
|
+
- Matches inside strings, comments, and template literals
|
|
58
|
+
|
|
59
|
+
Returns a deduplicated list of external member names.
|
|
60
|
+
"""
|
|
61
|
+
own_set = set(own_member_names)
|
|
62
|
+
pattern = re.compile(r"\bthis\.(\w+)")
|
|
63
|
+
seen: list[str] = []
|
|
64
|
+
pos = 0
|
|
65
|
+
while pos < len(code):
|
|
66
|
+
new_pos, skipped = skip_non_code(code, pos)
|
|
67
|
+
if skipped:
|
|
68
|
+
pos = new_pos
|
|
69
|
+
continue
|
|
70
|
+
m = pattern.match(code, pos)
|
|
71
|
+
if m:
|
|
72
|
+
name = m.group(1)
|
|
73
|
+
if name not in own_set and not name.startswith("$") and name not in seen:
|
|
74
|
+
seen.append(name)
|
|
75
|
+
pos = m.end()
|
|
76
|
+
else:
|
|
77
|
+
pos += 1
|
|
78
|
+
return seen
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_external_dep_sources(
|
|
82
|
+
external_deps: list[str],
|
|
83
|
+
sibling_entries: list,
|
|
84
|
+
component_own_members: set[str],
|
|
85
|
+
component_name: str,
|
|
86
|
+
component_members_by_section: dict[str, list[str]] | None = None,
|
|
87
|
+
) -> dict[str, dict]:
|
|
88
|
+
"""Resolve where each external dependency comes from.
|
|
89
|
+
|
|
90
|
+
For each dep, checks sibling mixin entries and the component's own members.
|
|
91
|
+
Returns a dict mapping dep name to source info::
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
"entityId": {
|
|
95
|
+
"kind": "component", # or "sibling", "ambiguous", "unknown"
|
|
96
|
+
"detail": "TaskDetailView.data",
|
|
97
|
+
"sources": ["TaskDetailView.data"],
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
external_deps: Names of external ``this.X`` references.
|
|
103
|
+
sibling_entries: Other MixinEntry objects for the same component.
|
|
104
|
+
component_own_members: Members the component defines itself.
|
|
105
|
+
component_name: Component file stem (for display in warnings).
|
|
106
|
+
component_members_by_section: Optional categorized component members
|
|
107
|
+
(keys: data, computed, methods, watch) for more detailed source info.
|
|
108
|
+
"""
|
|
109
|
+
result: dict[str, dict] = {}
|
|
110
|
+
for dep in external_deps:
|
|
111
|
+
sources: list[str] = []
|
|
112
|
+
|
|
113
|
+
# Check sibling mixin entries
|
|
114
|
+
for sibling in sibling_entries:
|
|
115
|
+
for section in ("data", "computed", "methods", "watch"):
|
|
116
|
+
section_members = getattr(sibling.members, section, [])
|
|
117
|
+
if dep in section_members:
|
|
118
|
+
sources.append(f"{sibling.mixin_stem}.{section}")
|
|
119
|
+
break # one hit per sibling is enough
|
|
120
|
+
|
|
121
|
+
# Check component's own members (with section detail if available)
|
|
122
|
+
if dep in component_own_members:
|
|
123
|
+
comp_section = None
|
|
124
|
+
if component_members_by_section:
|
|
125
|
+
for section in ("data", "computed", "methods", "watch"):
|
|
126
|
+
if dep in component_members_by_section.get(section, []):
|
|
127
|
+
comp_section = section
|
|
128
|
+
break
|
|
129
|
+
if comp_section:
|
|
130
|
+
sources.append(f"{component_name}.{comp_section}")
|
|
131
|
+
else:
|
|
132
|
+
sources.append(f"{component_name}")
|
|
133
|
+
|
|
134
|
+
if len(sources) == 0:
|
|
135
|
+
result[dep] = {"kind": "unknown", "detail": None, "sources": []}
|
|
136
|
+
elif len(sources) == 1:
|
|
137
|
+
src = sources[0]
|
|
138
|
+
kind = "component" if src.startswith(component_name) else "sibling"
|
|
139
|
+
result[dep] = {"kind": kind, "detail": src, "sources": sources}
|
|
140
|
+
else:
|
|
141
|
+
result[dep] = {"kind": "ambiguous", "detail": None, "sources": sources}
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
49
146
|
def extract_lifecycle_hooks(source: str) -> list[str]:
|
|
50
147
|
"""Find Vue lifecycle hooks defined in the mixin source."""
|
|
51
148
|
return [
|
|
@@ -163,6 +163,9 @@ def collect_mixin_warnings(
|
|
|
163
163
|
severity="warning",
|
|
164
164
|
))
|
|
165
165
|
|
|
166
|
+
# External dependencies (this.X where X is not in this mixin)
|
|
167
|
+
warnings.extend(detect_external_dependencies(mixin_source, mixin_members))
|
|
168
|
+
|
|
166
169
|
# this-aliasing (const self = this)
|
|
167
170
|
warnings.extend(detect_this_aliasing(mixin_source, ""))
|
|
168
171
|
|
|
@@ -175,6 +178,47 @@ def collect_mixin_warnings(
|
|
|
175
178
|
return warnings
|
|
176
179
|
|
|
177
180
|
|
|
181
|
+
def detect_external_dependencies(
|
|
182
|
+
mixin_source: str,
|
|
183
|
+
mixin_members: MixinMembers,
|
|
184
|
+
) -> list[MigrationWarning]:
|
|
185
|
+
"""Detect ``this.X`` references where X is not defined in the mixin.
|
|
186
|
+
|
|
187
|
+
These are external dependencies — members that come from the component
|
|
188
|
+
or a sibling mixin via shared ``this`` context. They break in composables
|
|
189
|
+
because composables are isolated functions without ``this``.
|
|
190
|
+
"""
|
|
191
|
+
from .mixin_analyzer import find_external_this_refs
|
|
192
|
+
|
|
193
|
+
external = find_external_this_refs(mixin_source, mixin_members.all_names)
|
|
194
|
+
warnings: list[MigrationWarning] = []
|
|
195
|
+
|
|
196
|
+
for name in external:
|
|
197
|
+
# Find the first line containing this.<name> for line_hint
|
|
198
|
+
match = re.search(rf"\bthis\.{re.escape(name)}\b", mixin_source)
|
|
199
|
+
line_hint = None
|
|
200
|
+
if match:
|
|
201
|
+
line_start = mixin_source.rfind("\n", 0, match.start()) + 1
|
|
202
|
+
line_end = mixin_source.find("\n", match.end())
|
|
203
|
+
if line_end == -1:
|
|
204
|
+
line_end = len(mixin_source)
|
|
205
|
+
line_hint = mixin_source[line_start:line_end].strip()
|
|
206
|
+
|
|
207
|
+
warnings.append(MigrationWarning(
|
|
208
|
+
mixin_stem="",
|
|
209
|
+
category="external-dependency",
|
|
210
|
+
message=(
|
|
211
|
+
f"'{name}' — external dep, not available in composable scope.\n"
|
|
212
|
+
f"// Accept as composable param: this.{name} → {name}.value or {name}."
|
|
213
|
+
),
|
|
214
|
+
action_required=f"Accept '{name}' as a composable parameter and rewrite this.{name}",
|
|
215
|
+
line_hint=line_hint,
|
|
216
|
+
severity="error",
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
return warnings
|
|
220
|
+
|
|
221
|
+
|
|
178
222
|
def detect_this_aliasing(
|
|
179
223
|
mixin_source: str, mixin_stem: str
|
|
180
224
|
) -> list[MigrationWarning]:
|
|
@@ -255,14 +299,20 @@ def inject_inline_warnings(
|
|
|
255
299
|
For each warning with a line_hint, inserts ``// ⚠ MIGRATION: {message}``
|
|
256
300
|
above the first line that contains the hint text.
|
|
257
301
|
|
|
302
|
+
Warnings that cannot be placed inline (no line_hint or no matching line)
|
|
303
|
+
are collected and inserted as a block after the confidence header.
|
|
304
|
+
|
|
258
305
|
Optionally prepends a confidence header comment at the top.
|
|
259
306
|
"""
|
|
260
307
|
if confidence is not None:
|
|
261
308
|
header = f"// Migration confidence: {confidence.value} ({warning_count} warnings — see migration report)\n"
|
|
262
309
|
source = header + source
|
|
263
310
|
|
|
311
|
+
unplaced: list[MigrationWarning] = []
|
|
312
|
+
|
|
264
313
|
for w in warnings:
|
|
265
314
|
if not w.line_hint:
|
|
315
|
+
unplaced.append(w)
|
|
266
316
|
continue
|
|
267
317
|
# Find a line containing the hint text
|
|
268
318
|
lines = source.splitlines(keepends=True)
|
|
@@ -276,6 +326,20 @@ def inject_inline_warnings(
|
|
|
276
326
|
new_lines.append(line)
|
|
277
327
|
if injected:
|
|
278
328
|
source = "".join(new_lines)
|
|
329
|
+
else:
|
|
330
|
+
unplaced.append(w)
|
|
331
|
+
|
|
332
|
+
# Place unmatched warnings as a block after the confidence header
|
|
333
|
+
if unplaced:
|
|
334
|
+
block = "".join(
|
|
335
|
+
f"// ⚠ MIGRATION: {w.message}\n" for w in unplaced
|
|
336
|
+
)
|
|
337
|
+
if confidence is not None:
|
|
338
|
+
# Insert right after the first line (confidence header)
|
|
339
|
+
idx = source.index('\n') + 1
|
|
340
|
+
source = source[:idx] + block + source[idx:]
|
|
341
|
+
else:
|
|
342
|
+
source = block + source
|
|
279
343
|
|
|
280
344
|
return source
|
|
281
345
|
|
package/vue3_migration/models.py
CHANGED
|
@@ -141,6 +141,8 @@ class MixinEntry:
|
|
|
141
141
|
"""Current migration status."""
|
|
142
142
|
warnings: list[MigrationWarning] = field(default_factory=list)
|
|
143
143
|
"""Migration warnings detected during analysis and generation."""
|
|
144
|
+
external_deps: list[str] = field(default_factory=list)
|
|
145
|
+
"""External this.X references not defined in this mixin."""
|
|
144
146
|
|
|
145
147
|
def compute_status(self) -> MigrationStatus:
|
|
146
148
|
"""Determine the migration status based on analysis results."""
|
|
@@ -14,7 +14,10 @@ from ..core.composable_analyzer import (
|
|
|
14
14
|
)
|
|
15
15
|
from ..core.composable_search import find_composable_dirs, search_for_composable
|
|
16
16
|
from ..core.file_resolver import compute_import_path, resolve_import_path
|
|
17
|
-
from ..core.mixin_analyzer import
|
|
17
|
+
from ..core.mixin_analyzer import (
|
|
18
|
+
extract_lifecycle_hooks, extract_mixin_members,
|
|
19
|
+
find_external_this_refs, resolve_external_dep_sources,
|
|
20
|
+
)
|
|
18
21
|
from ..core.warning_collector import collect_mixin_warnings
|
|
19
22
|
from ..models import (
|
|
20
23
|
ComposableCoverage, FileChange, MigrationConfig, MigrationPlan, MigrationStatus,
|
|
@@ -54,6 +57,8 @@ def _analyze_mixin_silent(
|
|
|
54
57
|
hooks = extract_lifecycle_hooks(mixin_source)
|
|
55
58
|
used = find_used_members(component_source, members.all_names)
|
|
56
59
|
|
|
60
|
+
ext_deps = find_external_this_refs(mixin_source, members.all_names)
|
|
61
|
+
|
|
57
62
|
entry = MixinEntry(
|
|
58
63
|
local_name=local_name,
|
|
59
64
|
mixin_path=mixin_file,
|
|
@@ -61,6 +66,7 @@ def _analyze_mixin_silent(
|
|
|
61
66
|
members=members,
|
|
62
67
|
lifecycle_hooks=hooks,
|
|
63
68
|
used_members=used,
|
|
69
|
+
external_deps=ext_deps,
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
if not used:
|
|
@@ -241,6 +247,86 @@ def plan_new_composables(
|
|
|
241
247
|
return changes
|
|
242
248
|
|
|
243
249
|
|
|
250
|
+
def _inject_setup_external_dep_warnings(
|
|
251
|
+
lines: list[str],
|
|
252
|
+
entry: "MixinEntry",
|
|
253
|
+
all_entries: list["MixinEntry"],
|
|
254
|
+
own_members: set[str],
|
|
255
|
+
component_name: str,
|
|
256
|
+
indent: str,
|
|
257
|
+
component_members_by_section: dict[str, list[str]] | None = None,
|
|
258
|
+
) -> list[str]:
|
|
259
|
+
"""Scan lifecycle lines for remaining ``this.X`` and prepend source-resolved warnings.
|
|
260
|
+
|
|
261
|
+
Only processes non-``$`` references that survived ``rewrite_this_refs``
|
|
262
|
+
(i.e. external deps not defined in the current mixin).
|
|
263
|
+
"""
|
|
264
|
+
if not lines:
|
|
265
|
+
return lines
|
|
266
|
+
|
|
267
|
+
all_text = "\n".join(lines)
|
|
268
|
+
external_names: list[str] = []
|
|
269
|
+
for m in re.finditer(r"\bthis\.(\w+)", all_text):
|
|
270
|
+
name = m.group(1)
|
|
271
|
+
if not name.startswith("$") and name not in external_names:
|
|
272
|
+
external_names.append(name)
|
|
273
|
+
|
|
274
|
+
if not external_names:
|
|
275
|
+
return lines
|
|
276
|
+
|
|
277
|
+
siblings = [e for e in all_entries if e is not entry]
|
|
278
|
+
sources = resolve_external_dep_sources(
|
|
279
|
+
external_names, siblings, own_members, component_name,
|
|
280
|
+
component_members_by_section=component_members_by_section,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
result: list[str] = []
|
|
284
|
+
warned: set[str] = set()
|
|
285
|
+
for line in lines:
|
|
286
|
+
for m in re.finditer(r"\bthis\.(\w+)", line):
|
|
287
|
+
name = m.group(1)
|
|
288
|
+
if name.startswith("$") or name in warned:
|
|
289
|
+
continue
|
|
290
|
+
warned.add(name)
|
|
291
|
+
src = sources.get(name, {"kind": "unknown", "detail": None, "sources": []})
|
|
292
|
+
result.extend(
|
|
293
|
+
_format_setup_warning(name, src, entry, indent)
|
|
294
|
+
)
|
|
295
|
+
result.append(line)
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _format_setup_warning(
|
|
300
|
+
name: str, src: dict, entry: "MixinEntry", indent: str
|
|
301
|
+
) -> list[str]:
|
|
302
|
+
"""Format a source-resolved warning comment for a setup() external dep."""
|
|
303
|
+
if src["kind"] == "sibling":
|
|
304
|
+
detail = src["detail"] # e.g. "loadingMixin.data"
|
|
305
|
+
sibling_stem = detail.split(".")[0]
|
|
306
|
+
sibling_composable = mixin_stem_to_composable_name(sibling_stem)
|
|
307
|
+
return [
|
|
308
|
+
f"{indent}// ⚠ MIGRATION: '{name}' — external dep (source: {detail} → {sibling_composable}).",
|
|
309
|
+
f"{indent}// this.{name} is unavailable in setup(). Use return value from {sibling_composable}() instead.",
|
|
310
|
+
]
|
|
311
|
+
elif src["kind"] == "component":
|
|
312
|
+
detail = src["detail"]
|
|
313
|
+
return [
|
|
314
|
+
f"{indent}// ⚠ MIGRATION: '{name}' — external dep (source: {detail}).",
|
|
315
|
+
f"{indent}// this.{name} is unavailable in setup(). Replace with local ref.",
|
|
316
|
+
]
|
|
317
|
+
elif src["kind"] == "ambiguous":
|
|
318
|
+
sources_str = ", ".join(src["sources"])
|
|
319
|
+
return [
|
|
320
|
+
f"{indent}// ⚠ MIGRATION: '{name}' — external dep (sources: {sources_str}).",
|
|
321
|
+
f"{indent}// Ambiguous origin — verify correct source before replacing this.{name}.",
|
|
322
|
+
]
|
|
323
|
+
else:
|
|
324
|
+
return [
|
|
325
|
+
f"{indent}// ⚠ MIGRATION: '{name}' — external dep, source not found in component or sibling mixins.",
|
|
326
|
+
f"{indent}// May come from props, inject, or a parent component. Replace this.{name} manually.",
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
|
|
244
330
|
def plan_component_injections(
|
|
245
331
|
entries_by_component: list[tuple[Path, list[MixinEntry]]],
|
|
246
332
|
composable_patches: list[FileChange],
|
|
@@ -269,6 +355,7 @@ def plan_component_injections(
|
|
|
269
355
|
# R-7: normalize CRLF
|
|
270
356
|
comp_source = comp_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
271
357
|
own_members = extract_own_members(comp_source)
|
|
358
|
+
comp_members_by_section = extract_mixin_members(comp_source)
|
|
272
359
|
ready_entries = []
|
|
273
360
|
|
|
274
361
|
for entry in entries:
|
|
@@ -377,6 +464,19 @@ def plan_component_injections(
|
|
|
377
464
|
mixin_content, entry.lifecycle_hooks, ref_m, plain_m,
|
|
378
465
|
config.indent + config.indent, # double indent to match setup() body level
|
|
379
466
|
)
|
|
467
|
+
|
|
468
|
+
# Inject source-resolved warnings for external deps in lifecycle code
|
|
469
|
+
comp_name = comp_path.stem
|
|
470
|
+
setup_indent = config.indent + config.indent
|
|
471
|
+
inline = _inject_setup_external_dep_warnings(
|
|
472
|
+
inline, entry, entries, own_members, comp_name, setup_indent,
|
|
473
|
+
component_members_by_section=comp_members_by_section,
|
|
474
|
+
)
|
|
475
|
+
wrapped = _inject_setup_external_dep_warnings(
|
|
476
|
+
wrapped, entry, entries, own_members, comp_name, setup_indent,
|
|
477
|
+
component_members_by_section=comp_members_by_section,
|
|
478
|
+
)
|
|
479
|
+
|
|
380
480
|
all_inline_lines.extend(inline)
|
|
381
481
|
all_lifecycle_calls.extend(wrapped)
|
|
382
482
|
|