Skip to content

SONARHTML-441 Restrict S1082 to native HTML elements - #777

Merged
erwan-leforestier-sonarsource merged 9 commits into
masterfrom
fix/sonarhtml-441-native-html-s1082
Aug 19, 2026
Merged

SONARHTML-441 Restrict S1082 to native HTML elements#777
erwan-leforestier-sonarsource merged 9 commits into
masterfrom
fix/sonarhtml-441-native-html-s1082

Conversation

@erwan-leforestier-sonarsource

@erwan-leforestier-sonarsource erwan-leforestier-sonarsource commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

S1082 now evaluates mouse-event keyboard equivalence only for known native HTML elements, avoiding false positives on framework and design-system components. A native-element allowlist remains available for explicit opt-outs, and Vue mouse-event shorthands are recognized.
RSPEC PR: https://github.com/SonarSource/rspec/pull/7862

Changes

  • Gate S1082 before role and mouse-event evaluation using the canonical native tag classifier.
  • Retain whitelistedElements for native-element opt-outs and document its full scope.
  • Cover Angular, Vue, and native HTML mouse-handler cases.

Functional Validation

Artifact: SONARHTML-441-fv.zip

Once the file is attached to the PR description, unzip and run:
./run.sh

Expected output:

******************* MASTER *******************
Analyzing "sample.html"...
Results:
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.1
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.2
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.3
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.4
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.5
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.6
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.7
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.8

****** Branch "fix/sonarhtml-441-native-html-s1082" ******
Analyzing "sample.html"...
Results:
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.1
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.2
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.3
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.11
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.12
    - Rule "Web:MouseEventWithoutKeyboardEquivalentCheck" -> L.13

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Aug 12, 2026

Copy link
Copy Markdown

SONARHTML-441

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Ruling Report

The following ruling changes are in this PR:

Rule: Web-MouseEventWithoutKeyboardEquivalentCheck

🔽 Code no longer flagged (70 issues)

Silverpeas-Core-master/war-core/src/main/webapp/form/jsp/editor.jsp:503

   498 | </SCRIPT>
   499 | 
   500 | 
   501 | 
   502 | <DIV class=mode id=tb3><NOBR>
>  503 | <INPUT id=mW onclick=setMode(true) type=radio
   504 | CHECKED name=rMode><LABEL class=current id=modeA for=mw><%=scc.getString("WYSIWYG")%></LABEL>
   505 | 
   506 | <INPUT id=mH onclick=setMode(false) type=radio name=rMode><LABEL id=modeB
   507 | for=mH><%=scc.getString("HTML")%></LABEL>
   508 | 

Silverpeas-Core-master/war-core/src/main/webapp/form/jsp/editor.jsp:506

   501 | 
   502 | <DIV class=mode id=tb3><NOBR>
   503 | <INPUT id=mW onclick=setMode(true) type=radio
   504 | CHECKED name=rMode><LABEL class=current id=modeA for=mw><%=scc.getString("WYSIWYG")%></LABEL>
   505 | 
>  506 | <INPUT id=mH onclick=setMode(false) type=radio name=rMode><LABEL id=modeB
   507 | for=mH><%=scc.getString("HTML")%></LABEL>
   508 | 
   509 | <BR><BR><BR>
   510 | 
   511 | </NOBR></DIV></DIV>

Silverpeas-Core-master/war-core/src/main/webapp/jobDomainPeas/jsp/userCreate.jsp:238

   233 |         </tr>
   234 |         <% if (userObject.isPasswordAvailable()) { %>
   235 |             <tr>
   236 |                 <td class="txtlibform"><fmt:message key="JDP.silverPassword"/> :</td>
   237 |                 <td>
>  238 |                     <input type="checkbox" name="userPasswordValid" id="userPasswordValid" value="true" <%
   239 |                       if (userObject.isPasswordValid()) {
   240 |                         out.print("checked");
   241 |                       } %> onclick="selectUnselect()"/>&nbsp;<fmt:message key="GML.yes" /><br/>
   242 |                 </td>
   243 |             </tr>

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/axisManager.jsp:283

   278 |         <td><TEXTAREA style="width:100%" name="Description" id="AxisDescription" rows="3"><%=EncodeHelper.javaStringToHtmlString(description)%></TEXTAREA></td>
   279 |      </tr>
   280 |       <tr>
   281 |         <td class="txtlibform" nowrap><%=resource.getString("GML.type")%>&nbsp;:</td>
   282 |         <td nowrap>
>  283 |           <input type="radio" name="Type" value="P" <%=primaryChecked%> onClick="javascript:changeList('P')"><span class="textePetitBold">&nbsp;<%=resource.getString("pdcPeas.primary")%></span><br>
   284 |           <input type="radio" name="Type" value="S" <%=secondaryChecked%> onClick="javascript:changeList('S')"><span class="textePetitBold">&nbsp;<%=resource.getString("pdcPeas.secondary")%></span></td>
   285 |       </tr>
   286 |       <tr> 
   287 |         <td class="txtlibform" valign="top"><%=resource.getString("pdcPeas.position")%>&nbsp;:</td>
   288 |         <td class="textePetitBold"><%=resource.getString("pdcPeas.brothersAxis")%>&nbsp;:<br>

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/axisManager.jsp:284

   279 |      </tr>
   280 |       <tr>
   281 |         <td class="txtlibform" nowrap><%=resource.getString("GML.type")%>&nbsp;:</td>
   282 |         <td nowrap>
   283 |           <input type="radio" name="Type" value="P" <%=primaryChecked%> onClick="javascript:changeList('P')"><span class="textePetitBold">&nbsp;<%=resource.getString("pdcPeas.primary")%></span><br>
>  284 |           <input type="radio" name="Type" value="S" <%=secondaryChecked%> onClick="javascript:changeList('S')"><span class="textePetitBold">&nbsp;<%=resource.getString("pdcPeas.secondary")%></span></td>
   285 |       </tr>
   286 |       <tr> 
   287 |         <td class="txtlibform" valign="top"><%=resource.getString("pdcPeas.position")%>&nbsp;:</td>
   288 |         <td class="textePetitBold"><%=resource.getString("pdcPeas.brothersAxis")%>&nbsp;:<br>
   289 | 			<%

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/globalResult.jsp:593

   588 | 		  <a href="javascript:setSortOrder('DESC')" class="<%=classCSS%>">DESC</a>
   589 | 		  </td>
   590 |         </tr>
   591 |         <c:if test="${activeSelection or exportEnabled}">
   592 | 			<tr id="globalResultSelectAllResult">
>  593 | 				<td class="txtlibform"><fmt:message key="pdcPeas.selectAll" /></td><td><input type="checkbox" name="selectAll" onclick="selectEveryResult(this);"/></td>
   594 | 			</tr>
   595 |         </c:if>
   596 | 		</table>
   597 | <% } %>
   598 |   </view:board>

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/searchContext.jsp:411

   406 | 									<td width="95%" bgcolor="<%=colorBand%>" nowrap>
   407 | 									<a href="javascript:showDaughterValues('<%=axisId%>','/<%=axisRootId%>/')"><img src="<%=resource.getIcon("pdcPeas.plus")%>" border=0 align="absmiddle"></a>
   408 | 									<a href="javascript:showDaughterValues('<%=axisId%>','/<%=axisRootId%>/')"><span class="textePetitBold"><%=axisName%></span></a> (<%=nbPositions%>)
   409 | 									</td>
   410 | 									<td  width="5%" bgcolor="<%=colorBand%>" >
>  411 | 									<input type=radio name=choix onClick="javascript:addCritere(<%=axisId%>,'/<%=axisRootId%>/')">
   412 | 									</td>
   413 | 					<%	
   414 | 								}
   415 | 							} else {// fin du test sur l'axisId du contexte de recherche
   416 | 								nbPrimarySearchCriteria++;

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/searchContext.jsp:557

   552 | 									<td width="95%" bgcolor="<%=colorBand%>" nowrap>
   553 | 									<a href="javascript:showDaughterValues('<%=axisId%>','/<%=axisRootId%>/')"><img src="<%=resource.getIcon("pdcPeas.plus")%>" border=0 align="absmiddle"></a>
   554 | 									<a href="javascript:showDaughterValues('<%=axisId%>','/<%=axisRootId%>/')"><span class="textePetitBold"><%=axisName%></span></a> (<%=nbPositions%>)
   555 | 									</td>
   556 | 									<td  width="5%" bgcolor="<%=colorBand%>" >
>  557 | 									<input type=radio name=choix onClick="javascript:addCritere('<%=axisId%>','/<%=axisRootId%>/')">
   558 | 									</td>
   559 | 							<% } %>
   560 | 							</td>
   561 | 						</tr>
   562 | 						<%

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/searchContextInComponent.jsp:550

   545 | 
   546 | 									out.println(displaySynonymsAxis(activeThesaurus, jargon, axisId));
   547 | 
   548 | 									%>
   549 | 									  (<%=nbPositions%>)</td>
>  550 | 									<td width="5%" align=center><input type=radio name=choix onClick="javascript:addCritere('<%=axisId%>','/<%=axisRootId%>/')"></td></tr>
   551 | 							<%
   552 | 									}
   553 | 								}
   554 | 							} else {// fin du test sur l'axisId du contexte de recherche
   555 | 								nbPrimarySearchCriteria++;

Silverpeas-Core-master/war-core/src/main/webapp/pdcPeas/jsp/searchContextInComponent.jsp:710

   705 | 									out.println(displaySynonymsAxis(activeThesaurus, jargon, axisId));
   706 | 
   707 | 									%>
   708 | 
   709 | 									   (<%=nbPositions%>)</td>
>  710 | 									<td width="5%" align=center><input type=radio name=choix onClick="javascript:addCritere('<%=axisId%>','/<%=axisRootId%>/')"></td></tr>
   711 | 							<%	}
   712 | 							 } %>
   713 | 							</td>
   714 | 						</tr>
   715 | 						<%

...and 60 more (see ruling JSON files for full list)

🔼 New issues flagged (58 issues)

voten/resources/assets/js/components/AdminPanelSuggestedChannels.vue:85

    80 |                         {{ item.channel.subscribers_count }}
    81 |                     </td>
    82 | 
    83 |                     <td>
    84 |                         <div class="display-flex">
>   85 |                             <i class="v-icon v-trash h-red pointer" @click="destroy(item.id)"></i>
    86 |                         </div>
    87 |                     </td>
    88 |                 </tr>
    89 |                 </tbody>
    90 |             </table>

voten/resources/assets/js/components/Announcement.vue:11

     6 | 			     v-for="(value, index) in announcements"
     7 | 			     :key="value.id">
     8 | 				<markdown :text="value.content"
     9 | 				          v-if="value.content"></markdown>
    10 | 
>   11 | 				<i class="v-icon block-before v-cancel pointer"
    12 | 				   aria-hidden="true"
    13 | 				   @click="close(value.id)"></i>
    14 | 			</div>
    15 | 		</transition-group>
    16 | 	</div>

voten/resources/assets/js/components/BannedUser.vue:13

     8 |                 </router-link>
     9 |             </div>
    10 | 
    11 |             <div class="actions">
    12 |                 <el-tooltip content="Reason" placement="top" transition="false" :open-delay="500">
>   13 |                     <i class="pointer v-icon go-gray v-attention-alt h-yellow" :class="list.description ? '' : 'display-hidden'"
    14 |                        @click="showDescription = !showDescription"></i>
    15 |                 </el-tooltip>
    16 | 
    17 |                 <el-tooltip content="Unban" placement="top" transition="false" :open-delay="500">
    18 |                     <i class="pointer v-icon go-gray v-delete h-red" @click="$emit('unban', list.user_id)"></i>

voten/resources/assets/js/components/BannedUser.vue:18

    13 |                     <i class="pointer v-icon go-gray v-attention-alt h-yellow" :class="list.description ? '' : 'display-hidden'"
    14 |                        @click="showDescription = !showDescription"></i>
    15 |                 </el-tooltip>
    16 | 
    17 |                 <el-tooltip content="Unban" placement="top" transition="false" :open-delay="500">
>   18 |                     <i class="pointer v-icon go-gray v-delete h-red" @click="$emit('unban', list.user_id)"></i>
    19 |                 </el-tooltip>
    20 | 
    21 |                 <el-tooltip :content="'Unban ' + date" placement="top" transition="false" :open-delay="500">
    22 |                     <i class="pointer v-icon go-gray v-calendar-1 h-green"></i>
    23 |                 </el-tooltip>

voten/resources/assets/js/components/BlockedDomain.vue:12

     7 |                 </a>
     8 |             </div>
     9 | 
    10 |             <div class="actions">
    11 |                 <el-tooltip content="Reason for being blocked" placement="top" transition="false" :open-delay="500">
>   12 |                     <i class="pointer v-icon go-gray v-attention-alt h-yellow"
    13 |                        :class="list.description ? '' : 'display-hidden'"
    14 |                        @click="showDescription = !showDescription"></i>
    15 |                 </el-tooltip>
    16 | 
    17 |                 <el-tooltip content="Unblock" placement="top" transition="false" :open-delay="500">

voten/resources/assets/js/components/BlockedDomain.vue:18

    13 |                        :class="list.description ? '' : 'display-hidden'"
    14 |                        @click="showDescription = !showDescription"></i>
    15 |                 </el-tooltip>
    16 | 
    17 |                 <el-tooltip content="Unblock" placement="top" transition="false" :open-delay="500">
>   18 |                     <i class="pointer v-icon go-gray v-delete h-red" @click="$emit('unblock', list.domain)"></i>
    19 |                 </el-tooltip>
    20 |             </div>
    21 |         </div>
    22 | 
    23 |         <div class="banned-user-description" v-if="showDescription">

voten/resources/assets/js/components/BookmarkedChannel.vue:25

    20 |                     <div class="flex-align-center">
    21 |                         <el-tooltip :content="bookmarked ? 'Unbookmark' : 'Bookmark'" placement="top" 
    22 |                             transition="false"
    23 |                             :open-delay="500"
    24 |                         >
>   25 |                             <i class="v-icon h-yellow pointer" v-if="!isNewbie"
    26 |                                :class="bookmarked ? 'v-unbookmark go-yellow' : 'v-bookmark go-gray'" @click="bookmark"></i>
    27 |                         </el-tooltip>
    28 | 
    29 |                         <el-tooltip :content="subscribed ? 'Unsubscribe' : 'Subscribe'" placement="top" 
    30 |                             transition="false"

voten/resources/assets/js/components/BookmarkedChannel.vue:33

    28 | 
    29 |                         <el-tooltip :content="subscribed ? 'Unsubscribe' : 'Subscribe'" placement="top" 
    30 |                             transition="false"
    31 |                             :open-delay="500"
    32 |                         >
>   33 |                             <i 
    34 |                                 class="subscribe-icon"
    35 |                                 :class="subscribed ? 'go-red el-icon-remove' : 'go-green el-icon-circle-plus-outline'"
    36 |                                 @click="subscribe"
    37 |                             ></i>
    38 |                         </el-tooltip>

voten/resources/assets/js/components/BookmarkedUser.vue:21

    16 |                     </h2>
    17 | 
    18 |                     <div class="flex-align-center">
    19 |                         <el-tooltip :content="bookmarked ? 'Unbookmark' : 'Bookmark'" placement="top"
    20 |                                     transition="false" :open-delay="500">
>   21 |                             <i class="v-icon h-yellow pointer"
    22 |                                :class="bookmarked ? 'go-yellow v-unbookmark' : 'v-bookmark go-gray'" @click="bookmark"></i>
    23 |                         </el-tooltip>
    24 |                         
    25 |                         <el-tooltip content="Start a private conversation" placement="top"
    26 |                                     transition="false" :open-delay="500">

voten/resources/assets/js/components/BookmarkedUser.vue:27

    22 |                                :class="bookmarked ? 'go-yellow v-unbookmark' : 'v-bookmark go-gray'" @click="bookmark"></i>
    23 |                         </el-tooltip>
    24 |                         
    25 |                         <el-tooltip content="Start a private conversation" placement="top"
    26 |                                     transition="false" :open-delay="500">
>   27 |                             <i class="v-icon go-green v-chat pointer" @click="sendMessage(list)"></i>
    28 |                         </el-tooltip>
    29 |                     </div>
    30 |                 </div>
    31 | 
    32 |                 <p>

...and 48 more (see ruling JSON files for full list)


Ruling tests passed.

@erwan-leforestier-sonarsource
erwan-leforestier-sonarsource marked this pull request as ready for review August 12, 2026 10:19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job. I left some comments

|| hasAttribute(node, "V-ON:" + eventName);
// Vue long form v-on:eventname, plus the @eventname shorthand the parser strips to a bare name
|| hasAttribute(node, "V-ON:" + eventName)
|| hasAttribute(node, eventName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line makes bare event names count as handlers for every event, but modifiers are still only
understood by the two KEY_*_WITH_KEY_NAME patterns. That asymmetry cuts both ways — a modifier on
the satisfying side hides a real handler, and one on the triggering side hides the trigger:

Case Current behaviour
<div @mouseover="a" @focus.capture="b"> raises Add a 'onFocus'…@focus.capture not recognised
<div @mouseout="a" @blur.once="b"> raises Add a 'onBlur'…@blur.once not recognised
<div @click="a" @keydown.arrow-up="b"> raises — \w{1,10} rejects Vue's kebab-case key modifiers
<div @click.prevent="a"> raises nothing — @click.prevent not recognised as a click handler
<div v-on:click.prevent="a"> raises nothing — same

Generalising the modifier grammar across all events fixes all five. The validation in the existing
patterns has to be preserved though — key names capped at 10 characters and 5 combined modifiers is
what keeps (keydown.enter,shift) and (keydown.undefinedkeyname) correctly reported (fixture lines
68, 70, 71). A naive "strip everything after the first dot" accepts those and breaks detected().

// Event modifiers / key names: Angular's (keydown.enter) pseudo-events and Vue's @click.prevent,
// @keydown.arrow-up. Key names are capped at 10 characters and 5 combined modifiers so that malformed
// bindings — (keydown.enter,shift), (keydown.undefinedkeyname) — are not mistaken for real handlers.
private static final String MODIFIERS = "(?:\\.[\\w-]{1,10}){0,5}";
private static final Map<String, Pattern> EVENT_PATTERNS = new ConcurrentHashMap<>();

private static boolean hasEventHandlerAttribute(TagNode node, String eventName) {
  return hasAttribute(node, "ON" + eventName)
    || hasAttribute(node, "ON-" + eventName)
    || hasAttribute(node, "NG-" + eventName)
    // Angular (eventname), Vue long form v-on:eventname, and the @eventname shorthand the parser
    // strips to a bare name — each optionally carrying modifiers (.prevent, .enter, .arrow-up)
    || hasEventBinding(node, eventName);
}

private static boolean hasEventBinding(TagNode node, String eventName) {
  Pattern pattern = EVENT_PATTERNS.computeIfAbsent(eventName, name -> Pattern.compile(
    "\\(" + name + MODIFIERS + "\\)"
      + "|(?:v-on:)?" + name + MODIFIERS,
    Pattern.CASE_INSENSITIVE));
  return node.getAttributes().stream().anyMatch(a -> pattern.matcher(a.getName()).matches());
}

This subsumes KEY_DOWN_WITH_KEY_NAME, KEY_UP_WITH_KEY_NAME, hasKeyDownWithKeyName,
hasKeyUpWithKeyName and the (eventName) branch, so all five can go — net −13 lines.

Applied locally at this branch's head: all five rows above behave correctly, the malformed key-name
cases stay reported, <a href="#" @click.prevent="x"> stays exempt, and the full module passes
(657 tests, 0 failures, this check's four tests unchanged).

It also looks ruling-neutral: of the 52 @click.<modifier> sites in its/sources, 40 are on
<el-dropdown-item> (custom element, excluded by the new gate) and 12 on <a @click.prevent>
(exempt via isHyperlink — and that exemption only engages properly once the modifier form is
recognised). Worth a ruling run to confirm, but no churn expected on top of the +59.

Suggest adding those five rows as fixture cases either way; none are covered today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 33ee23f. Event bindings now accept valid Angular/Vue modifiers consistently for every event, while retaining the existing modifier count and key-name length limits. The fixture covers all five cases from the review.

130
],
"project:voten/resources/assets/js/components/passport/PersonalAccessTokens.vue": [
101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entry is a false positive. PersonalAccessTokens.vue:101 is:

<input type="checkbox" @click="toggleScope(scope.id)" :checked="scopeIsAssigned(scope.id)">

A native checkbox is keyboard-operable by definition — Space activates it and the HTML activation
behaviour fires a synthetic click, so the @click handler is the keyboard path. Asking for
onKeyDown/onKeyUp here is wrong.

The cause is isAnInteractiveRole, which contains only "textbox", so <textarea> and text inputs
are exempted via their implicit role while checkbox, radio, combobox and listbox fall through:

Input Result
<input type="checkbox" @click="x"> flagged (FP)
<input type="checkbox" onclick="x"> flagged — pre-existing on master
<input type="radio" @click="x"> flagged (FP)
<select @click="x"> flagged (debatable)
<textarea @click="x"> clean (textbox role)

Since onclick= already behaves this way the gap predates this PR — the Vue support just gives it a
first corpus hit. Either widen isAnInteractiveRole to the natively-activatable roles, or split it
into its own ticket; what I'd avoid is accepting this line into the expectation file as though it
were correct.

For the record, the other 58 additions all check out — <i>, <div>, <li>, <span>, <h1>,
<img> with @click and no keyboard equivalent, no role, no tabindex. I reconstructed each
flagged tag from the pinned corpus commit ef27916 to confirm.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 33ee23f. Native implicit checkbox, radio, and listbox controls are now exempted; explicit ARIA roles still require keyboard handlers. Added native-control regression cases and removed the PersonalAccessTokens false positive from the ruling expectation.

@erwan-leforestier-sonarsource
erwan-leforestier-sonarsource force-pushed the fix/sonarhtml-441-native-html-s1082 branch from 210d690 to 079ba21 Compare August 18, 2026 06:42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I left one question.

// combinations to five modifiers so malformed bindings are not mistaken for event handlers.
private static final String EVENT_MODIFIERS = "(?:\\.[\\w-]{1,10}){0,5}";
private static final Map<String, Pattern> EVENT_PATTERNS = new ConcurrentHashMap<>();
private static final Set<String> NATIVELY_ACTIVATABLE_ROLES = Set.of("textbox", "checkbox", "radio", "listbox");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need combobox?

@erwan-leforestier-sonarsource erwan-leforestier-sonarsource Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. A native single-select has an implicit combobox role, while multiple or size > 1 selects have listbox. I corrected the implicit-role mapping, included combobox among the natively keyboard-activatable roles, and added coverage for both forms. I also updated the rule description in RSPEC PR #7862 to cover the implicit combobox role and its listbox variants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: I also updated the rule description in RSPEC PR #7862 to cover the implicit combobox role and its listbox variants.

…nt shorthands

Restore the whitelistedElements rule property so a native element can be
opted out of the check, now with an empty default since the native-HTML
gate already excludes custom elements.

Recognize the bare @eventname Vue shorthand for click, mouseover, mouseout,
focus and blur by handling it in the shared event-handler helper, fixing the
asymmetry where only keydown/keyup accepted it.
🤖 Generated with GitHub Actions
🤖 Generated with GitHub Actions
@erwan-leforestier-sonarsource
erwan-leforestier-sonarsource force-pushed the fix/sonarhtml-441-native-html-s1082 branch from 91a2f72 to d9ae36d Compare August 19, 2026 13:38
@sonarqube-next

Copy link
Copy Markdown

@erwan-leforestier-sonarsource
erwan-leforestier-sonarsource merged commit b323f3a into master Aug 19, 2026
12 checks passed
@erwan-leforestier-sonarsource
erwan-leforestier-sonarsource deleted the fix/sonarhtml-441-native-html-s1082 branch August 19, 2026 13:49
@gitar-bot

gitar-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Restricts rule S1082 to native HTML elements, addressing the redundant local-name check and whitelistedElements documentation findings while adding support for Vue mouse-event shorthands.

✅ 2 resolved
Quality: Redundant local-name check before hasKnownHTMLTag gate

📄 sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/sonar/MouseEventWithoutKeyboardEquivalentCheck.java:53
In startElement, node.getLocalName().equals(node.getNodeName()) only fails when the node name contains a namespace prefix (e.g. x:div), but such prefixed names can never match KNOWN_HTML_TAGS (which are unprefixed and compared with equalsIgnoreCase), so HtmlConstants.hasKnownHTMLTag(node) already excludes them. The first clause adds no behavioral effect and can be dropped to leave a single, clearer gate: if (HtmlConstants.hasKnownHTMLTag(node)).

Quality: whitelistedElements docs contradict its actual behavior

📄 sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/sonar/MouseEventWithoutKeyboardEquivalentCheck.java:59-63 📄 sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/sonar/MouseEventWithoutKeyboardEquivalentCheck.java:74 📄 sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/sonar/MouseEventWithoutKeyboardEquivalentCheck.java:121-123 📄 sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/sonar/MouseEventWithoutKeyboardEquivalentCheck.java:194-201
The whitelistedElements RuleProperty description says it ignores "custom elements" that "expose an onClick attribute", but the implementation contradicts both claims. First, startElement now gates all evaluation on HtmlConstants.hasKnownHTMLTag(node), and custom elements are never in KNOWN_HTML_TAGS, so isException/isClickableButtonLikeElement is unreachable for custom elements — only native tags (e.g. div, as used in the new test) can actually be whitelisted. Second, isClickableButtonLikeElement short-circuits the entire check via isException, so a whitelisted element is also exempted from the mouseover/focus and mouseout/blur rules, not just the onClick case the doc describes. Update the RuleProperty description to reflect that it whitelists native HTML elements from the whole S1082 check (or narrow the implementation to the onClick case if that was the intent).

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants