Skip to content

Commit fb6cec0

Browse files
authored
Web IDL study: detect event handlers with no matching event (#785)
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 75ffbc3 commit fb6cec0

File tree

3 files changed

+133
-27
lines changed

3 files changed

+133
-27
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: 'Event handlers with no matching events',
155+
description: 'No matching events were found for the following event handlers'
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

+78-20
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
import study from '../src/lib/study-webidl.js';
88
import { assertNbAnomalies, assertAnomaly } from './util.js';
99

10+
const baseEventIdl = `
11+
[Exposed=*]
12+
interface Event {};
13+
[LegacyTreatNonObjectAsNull]
14+
callback EventHandlerNonNull = any (Event event);
15+
typedef EventHandlerNonNull? EventHandler;
16+
[Exposed=*]
17+
interface EventTarget {};
18+
`;
19+
1020
describe('The Web IDL analyser', () => {
1121
const specUrl = 'https://www.w3.org/TR/spec';
1222
const specUrl2 = 'https://www.w3.org/TR/spec2';
@@ -148,42 +158,90 @@ interface WhereIAm {};
148158
});
149159
});
150160

151-
it('reports no anomaly for valid EventHandler attributes definitions', () => {
152-
const report = analyzeIdl(`
153-
[Exposed=*]
154-
interface Event {};
155-
[LegacyTreatNonObjectAsNull]
156-
callback EventHandlerNonNull = any (Event event);
157-
typedef EventHandlerNonNull? EventHandler;
158-
161+
it('reports EventHandler attributes with no matching events', () => {
162+
const report = analyzeIdl(baseEventIdl + `
159163
[Global=Window,Exposed=*]
160164
interface Carlos : EventTarget {
161165
attribute EventHandler onbigbisous;
162166
};
163-
[Exposed=*]
164-
interface EventTarget {};
165167
`);
168+
assertNbAnomalies(report, 1);
169+
assertAnomaly(report, 0, {
170+
name: 'noEvent',
171+
message: 'The interface `Carlos` defines an event handler `onbigbisous` but no event named "bigbisous" targets it'
172+
});
173+
});
174+
175+
it('reports no anomaly for valid EventHandler attributes definitions', () => {
176+
const crawlResult = toCrawlResult(baseEventIdl +`
177+
[Global=Window,Exposed=*]
178+
interface Carlos : EventTarget {
179+
attribute EventHandler onbigbisous;
180+
};`);
181+
crawlResult[0].events = [{
182+
type: 'bigbisous',
183+
interface: 'Event',
184+
targets: ['Carlos']
185+
}];
186+
const report = study(crawlResult);
166187
assertNbAnomalies(report, 0);
167188
});
168189

169190
it('detects unexpected EventHandler attributes', () => {
170-
const report = analyzeIdl(`
171-
[Exposed=*]
172-
interface Event {};
173-
[LegacyTreatNonObjectAsNull]
174-
callback EventHandlerNonNull = any (Event event);
175-
typedef EventHandlerNonNull? EventHandler;
176-
191+
const report = analyzeIdl(baseEventIdl + `
177192
[Global=Window,Exposed=*]
178193
interface Carlos {
179194
attribute EventHandler onbigbisous;
180-
};
181-
`);
182-
assertNbAnomalies(report, 1);
195+
};`);
196+
assertNbAnomalies(report, 2);
183197
assertAnomaly(report, 0, {
184198
name: 'unexpectedEventHandler',
185199
message: 'The interface `Carlos` defines an event handler `onbigbisous` but does not inherit from `EventTarget`'
186200
});
201+
assertAnomaly(report, 1, { name: 'noEvent' });
202+
});
203+
204+
it('follows the inheritance chain to assess event targets', () => {
205+
const crawlResult = toCrawlResult(baseEventIdl + `
206+
[Global=Window,Exposed=*]
207+
interface Singer : EventTarget {
208+
attribute EventHandler onbigbisous;
209+
};
210+
211+
[Global=Window,Exposed=*]
212+
interface Carlos : Singer {};`);
213+
crawlResult[0].events = [{
214+
type: 'bigbisous',
215+
interface: 'Event',
216+
targets: [
217+
'Carlos'
218+
]
219+
}];
220+
const report = study(crawlResult);
221+
assertNbAnomalies(report, 0);
222+
});
223+
224+
it('follows the bubbling path to assess event targets', () => {
225+
const crawlResult = toCrawlResult(baseEventIdl + `
226+
[Global=Window,Exposed=*]
227+
interface IDBDatabase : EventTarget {
228+
attribute EventHandler onabort;
229+
};
230+
[Global=Window,Exposed=*]
231+
interface IDBTransaction : EventTarget {
232+
attribute EventHandler onabort;
233+
};
234+
[Global=Window,Exposed=*]
235+
interface MyIndexedDB : IDBDatabase {
236+
};`);
237+
crawlResult[0].events = [{
238+
type: 'abort',
239+
interface: 'Event',
240+
targets: ['IDBTransaction'],
241+
bubbles: true
242+
}];
243+
const report = study(crawlResult);
244+
assertNbAnomalies(report, 0);
187245
});
188246

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

0 commit comments

Comments
 (0)