fix(language-core): detect duplicate event listeners across name formats#6094
Conversation
|
The language service plugin is not working for tsc environment. Could you try resolving this issue during the codegen phase in the |
…ving to the same event" This reverts commit 9e0bfbd.
|
Hello, Thanks for revising and merging! Definitely an elegant solution. |
|
In the past, I often did this by triggering an additional event while binding data. This change causes errors in the current version. May I ask why this is not recommended? <MyPicker v-model="result" @update:model-value="action" /> |
|
I guess you have |
|
With vue-tsc 3.3.4, which includes this change, the following are detected as With vue-tsc 3.3.3, they were not detected as errors. Any hint would be appreciated. Thanks. |
|
The fix will be released at next version. |
|
@adube Oh my God this has been driving me insane today! |
Problem
Vue normalizes event listener names when merging props. Both
@create-postand@createPostresolve to the same handler keyonCreatePostat runtime viamergeProps():The same applies to
update:events:Neither
vue(2)(duplicate attribute) norts-plugin(1117)(duplicate object property) catch this.vue(2)compares names literally, and the TypeScript codegen produces two different string keys ("onCreate-post"vs"onCreatePost"), so TypeScript does not see the conflict either.Root cause
mergeProps()normalizes allonXxxhandler keys viacamelize()before merging:However, in a single JSX/template object literal the compiler does not call
mergeProps— it emits a plain object where the keys come from the template verbatim. If both@create-postand@createPostappear on the same element, the codegen produces:TypeScript sees two different string keys and raises no error. At runtime Vue processes the vnode props through
mergePropswhich normalizes them, sohandlerAandhandlerBboth end up underonCreatePost, and both are called. This looks correct on the surface but is almost certainly a typo: two different handlers registered for what the author thought were two different events.What this PR adds
A new
@vue/language-serviceplugin (vue-duplicate-event-listeners) that detects this class of bug statically, at edit time.When two or more
v-onlisteners on the same component resolve to the same normalized key, the plugin highlights the duplicate(s) as an error:Duplicate event listener. vue(duplicate-event-listener).Only the second (and further) occurrence is flagged, consistent with how
vue(2)andts-plugin(1117)behave.Normalization rules
The plugin mirrors the exact normalization that
mergeProps()applies at runtime. All cases below are detected:@create-postonCreatePost@createPostonCreatePost(duplicate)@update:post-titleonUpdate:postTitle@update:postTitleonUpdate:postTitle(duplicate)@vue:before-mountonVnodeBeforeMount@vue:beforeMountonVnodeBeforeMount(duplicate)Design decisions
Only events, not props
Duplicate static props and
v-modelbindings are already covered:ts-plugin(1117)— fires when two props produce the same key in the codegen object literal (e.g.post-title="a" postTitle="b")ts-plugin(2783)— fires when a static prop conflicts with av-bindspread (e.g.post-title="a" v-bind="{ postTitle: 'b' }")Adding a third diagnostic for the same cases would produce redundant errors on the same attribute. This plugin covers only the gap that existing tools leave:
v-onlisteners.Only components, not native elements
The
tagType !== COMPONENTguard is intentional. On native HTML elements,@create-postand@createPostmap to two separateaddEventListenercalls with literally different event names. The browser does not normalize them: aCustomEventdispatched ascreate-postwill not trigger acreatePostlistener. The normalization is a Vue-only mechanism that only applies to component prop passing.Only static arguments
Dynamic event arguments (
@[eventName]="fn") are skipped. Their value is unknown at static analysis time, so no false positives are possible.v-onwithout argument is not coveredv-on="expr"stores an arbitrary JavaScript expression as an unparsed string in the AST (isStatic: false). Extracting keys from it would require a JS parser and would still fail for non-literal expressions (v-on="handlers",v-on="isAdmin ? a : b"). This is an intentional limitation consistent with how all other static analysis tools treat this syntax.First occurrence is the canonical one
When duplicates are found, only
entries[1..n]are flagged — the first listener by source position is treated as the original. This matches the convention ofvue(2)andts-plugin(1117).Severity is Error, not Warning
Other duplicate-attribute diagnostics in the same toolchain (
vue(2),ts-plugin(1117),ts-plugin(2783)) all use Error severity.Does not overlap with
vue(2)vue(2)(DUPLICATE_ATTRIBUTE from@vue/compiler-dom) fires only on literally identical attribute names. It does not normalize —@create-postand@createPostproduce novue(2)error. This plugin covers the orthogonal case: same normalized key, different literal names.The only apparent overlap appears when three listeners are present, e.g.:
Here
vue(2)fires on@createPost(third, literally duplicates the second), and this plugin fires on@createPost(second, normalized duplicate of@create-post) and@createPost(third). The second@createPostappears to have two errors. This is technically correct — it is simultaneously a normalized duplicate of@create-postand an exact duplicate of the third@createPost— and is not worth special-casing since three listeners for the same event on one component is an extreme edge case.Files changed
Test cases
Positive (must flag):
@create-post+@createPost— basic kebab vs camel@create-post+@create-post— identical names (regression: must not break existing behavior)@update:post-title+@update:postTitle— update events@vue:before-mount+@vue:beforeMount— vnode lifecycle hooksNegative (must not flag):
@create-post+@update:title— different events:title+@update:title— prop and event, different namespaces@[eventName]+@[eventName]— dynamic arguments<div @click @click>— native element