Skip to content

Commit b0302b6

Browse files
committed
Web IDL study: detect event handlers with no matching even
When an interface defines an event handler, there should always be an event named after the event handler that targets the interface. The new `noEvent` anomaly detects situations where the event is missing. The analysis reports the two anomalies mentioned in: w3c/webref#1216 (comment) (The analysis reports additional anomalies when run on the raw extracts. That's normal and due to missing event targets that get added during curation)
1 parent a00f72f commit b0302b6

File tree

3 files changed

+147
-9
lines changed

3 files changed

+147
-9
lines changed

src/lib/study-webidl.js

+50-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121

2222
import * as WebIDL2 from 'webidl2';
23+
import { getInterfaceTreeInfo } from 'reffy';
2324

2425
const getSpecs = list => [...new Set(list.map(({ spec }) => spec))];
2526
const specName = spec => spec.shortname ?? spec.url;
@@ -174,6 +175,15 @@ function studyWebIdl (specs, { crawledResults = [], curatedResults = [] } = {})
174175
const usedTypes = {}; // Index of types used in the IDL
175176
const usedExtAttrs = {}; // Index of extended attributes
176177

178+
// We need to run the analysis on all specs, even if caller is only
179+
// interested in a few of them, because types may be defined in specs that
180+
// the caller is not interested in.
181+
const allSpecs = (crawledResults.length > 0) ? crawledResults : specs;
182+
const allEvents = allSpecs
183+
.filter(spec => Array.isArray(spec.events))
184+
.map(spec => spec.events.map(e => Object.assign({ spec }, e)))
185+
.flat();
186+
177187
// Record an anomaly for the given spec(s),
178188
// provided we are indeed interested in the results
179189
function recordAnomaly (spec, name, message) {
@@ -193,17 +203,54 @@ function studyWebIdl (specs, { crawledResults = [], curatedResults = [] } = {})
193203
function inheritsFrom (iface, ancestor) {
194204
if (!iface.inheritance) return false;
195205
if (iface.inheritance === ancestor) return true;
206+
if (!dfns[iface.inheritance]) return false;
196207
const parentInterface = dfns[iface.inheritance].find(({ idl }) => !idl.partial)?.idl;
197208
if (!parentInterface) return false;
198209
return inheritsFrom(parentInterface, ancestor);
199210
}
200211

212+
function getBubblingPath(iface) {
213+
const interfaces = Object.values(dfns)
214+
.map(dfn => dfn.find(d => d.idl.type === 'interface'))
215+
.filter(dfn => !!dfn)
216+
.map(dfn => dfn.idl);
217+
const { bubblingPath } = getInterfaceTreeInfo(iface, interfaces);
218+
return bubblingPath;
219+
}
220+
201221
// TODO: a full check of event handlers would require:
202222
// - checking if the interface doesn't include a mixin with eventhandlers
203223
function checkEventHandlers (spec, memberHolder, iface = memberHolder) {
204-
const eventHandler = memberHolder.members.find(m => m?.name?.startsWith('on') && m.type === 'attribute' && m.idlType?.idlType === 'EventHandler');
205-
if (eventHandler && !inheritsFrom(iface, 'EventTarget')) {
206-
recordAnomaly(spec, 'unexpectedEventHandler', `The interface \`${iface.name}\` defines an event handler \`${eventHandler.name}\` but does not inherit from \`EventTarget\``);
224+
const eventHandlers = memberHolder.members.filter(m => m?.name?.startsWith('on') && m.type === 'attribute' && m.idlType?.idlType === 'EventHandler');
225+
if (eventHandlers.length > 0 && !inheritsFrom(iface, 'EventTarget')) {
226+
recordAnomaly(spec, 'unexpectedEventHandler', `The interface \`${iface.name}\` defines an event handler \`${eventHandlers[0].name}\` but does not inherit from \`EventTarget\``);
227+
}
228+
229+
for (const eventHandler of eventHandlers) {
230+
const eventType = eventHandler.name.substring(2);
231+
const events = allEvents.filter(e => e.type === eventType);
232+
let event = events.find(e => e.targets?.find(target =>
233+
target === iface.name));
234+
if (!event) {
235+
// No event found with the same type that targets the interface
236+
// directly, but one the events may target an interface that
237+
// inherits from the interface, or may bubble on the interface.
238+
event = events.find(e => e.targets?.find(target => {
239+
const inheritanceFound = dfns[target]?.find(dfn =>
240+
dfn.idl.type === 'interface' &&
241+
inheritsFrom(dfn.idl, iface.name));
242+
if (inheritanceFound) return true;
243+
if (!e.bubbles) return false;
244+
const bubblingPath = getBubblingPath(target);
245+
if (!bubblingPath) return false;
246+
return bubblingPath.find(bubblingIface =>
247+
iface.name === bubblingIface ||
248+
inheritsFrom(iface, bubblingIface));
249+
}));
250+
}
251+
if (!event) {
252+
recordAnomaly(spec, 'noEvent', `The interface \`${iface.name}\` defines an event handler \`${eventHandler.name}\` but no event named "${eventType}" targets it`);
253+
}
207254
}
208255
}
209256

@@ -384,10 +431,6 @@ function studyWebIdl (specs, { crawledResults = [], curatedResults = [] } = {})
384431
}
385432
}
386433

387-
// We need to run the analysis on all specs, even if caller is only
388-
// interested in a few of them, because types may be defined in specs that
389-
// the caller is not interested in.
390-
const allSpecs = (crawledResults.length > 0) ? crawledResults : specs;
391434
allSpecs
392435
// We're only interested in specs that define Web IDL content
393436
.filter(spec => !!spec.idl)

src/lib/study.js

+5
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ const anomalyGroups = [
149149
guidance: 'See the [`[Exposed]`](https://webidl.spec.whatwg.org/#Exposed) extended attribute section in Web IDL for requirements.'
150150
},
151151
{ name: 'invalid', title: 'Invalid Web IDL' },
152+
{
153+
name: 'noEvent',
154+
title: 'Suspicious event handlers',
155+
description: 'The following suspicious event handlers were found'
156+
},
152157
{ name: 'noExposure', title: 'Missing `[Exposed]` attributes' },
153158
{ name: 'noOriginalDefinition', title: 'Missing base interfaces' },
154159
{ name: 'overloaded', title: 'Invalid overloaded operations' },

test/study-webidl.js

+92-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ interface WhereIAm {};
148148
});
149149
});
150150

151-
it('reports no anomaly for valid EventHandler attributes definitions', () => {
151+
it('reports EventHandler attributes with no matching events', () => {
152152
const report = analyzeIdl(`
153153
[Exposed=*]
154154
interface Event {};
@@ -163,6 +163,34 @@ interface Carlos : EventTarget {
163163
[Exposed=*]
164164
interface EventTarget {};
165165
`);
166+
assertNbAnomalies(report, 1);
167+
assertAnomaly(report, 0, {
168+
name: 'noEvent',
169+
message: 'The interface `Carlos` defines an event handler `onbigbisous` but no event named "bigbisous" targets it'
170+
});
171+
});
172+
173+
it('reports no anomaly for valid EventHandler attributes definitions', () => {
174+
const crawlResult = toCrawlResult(`
175+
[Exposed=*]
176+
interface Event {};
177+
[LegacyTreatNonObjectAsNull]
178+
callback EventHandlerNonNull = any (Event event);
179+
typedef EventHandlerNonNull? EventHandler;
180+
181+
[Global=Window,Exposed=*]
182+
interface Carlos : EventTarget {
183+
attribute EventHandler onbigbisous;
184+
};
185+
[Exposed=*]
186+
interface EventTarget {};
187+
`);
188+
crawlResult[0].events = [{
189+
type: 'bigbisous',
190+
interface: 'Event',
191+
targets: ['Carlos']
192+
}];
193+
const report = study(crawlResult);
166194
assertNbAnomalies(report, 0);
167195
});
168196

@@ -179,11 +207,73 @@ interface Carlos {
179207
attribute EventHandler onbigbisous;
180208
};
181209
`);
182-
assertNbAnomalies(report, 1);
210+
assertNbAnomalies(report, 2);
183211
assertAnomaly(report, 0, {
184212
name: 'unexpectedEventHandler',
185213
message: 'The interface `Carlos` defines an event handler `onbigbisous` but does not inherit from `EventTarget`'
186214
});
215+
assertAnomaly(report, 1, { name: 'noEvent' });
216+
});
217+
218+
it('follows the inheritance chain to assess event targets', () => {
219+
const crawlResult = toCrawlResult(`
220+
[Exposed=*]
221+
interface Event {};
222+
[LegacyTreatNonObjectAsNull]
223+
callback EventHandlerNonNull = any (Event event);
224+
typedef EventHandlerNonNull? EventHandler;
225+
226+
[Global=Window,Exposed=*]
227+
interface Singer : EventTarget {
228+
attribute EventHandler onbigbisous;
229+
};
230+
231+
[Global=Window,Exposed=*]
232+
interface Carlos : Singer {};
233+
[Exposed=*]
234+
interface EventTarget {};
235+
`);
236+
crawlResult[0].events = [{
237+
type: 'bigbisous',
238+
interface: 'Event',
239+
targets: [
240+
'Carlos'
241+
]
242+
}];
243+
const report = study(crawlResult);
244+
assertNbAnomalies(report, 0);
245+
});
246+
247+
it('follows the bubbling path to assess event targets', () => {
248+
const crawlResult = toCrawlResult(`
249+
[Exposed=*]
250+
interface Event {};
251+
[LegacyTreatNonObjectAsNull]
252+
callback EventHandlerNonNull = any (Event event);
253+
typedef EventHandlerNonNull? EventHandler;
254+
255+
[Global=Window,Exposed=*]
256+
interface IDBDatabase : EventTarget {
257+
attribute EventHandler onabort;
258+
};
259+
[Global=Window,Exposed=*]
260+
interface IDBTransaction : EventTarget {
261+
attribute EventHandler onabort;
262+
};
263+
[Global=Window,Exposed=*]
264+
interface MyIndexedDB : IDBDatabase {
265+
};
266+
267+
[Exposed=*]
268+
interface EventTarget {};`);
269+
crawlResult[0].events = [{
270+
type: 'abort',
271+
interface: 'Event',
272+
targets: ['IDBTransaction'],
273+
bubbles: true
274+
}];
275+
const report = study(crawlResult);
276+
assertNbAnomalies(report, 0);
187277
});
188278

189279
it('detects incompatible partial exposure issues', () => {

0 commit comments

Comments
 (0)