diff --git a/src/lib/study-webidl.js b/src/lib/study-webidl.js index fd6f3ed7..d18bd84c 100644 --- a/src/lib/study-webidl.js +++ b/src/lib/study-webidl.js @@ -30,14 +30,17 @@ const WebIDL2 = require("webidl2"); const getSpecs = list => [...new Set(list.map(({spec}) => spec))]; const specName = spec => spec.shortname ?? spec.url; +const dfnName = dfn => `${dfn.idl.partial ? 'partial ' : ''}${dfn.idl.type} "${dfn.idl.name}"`; const possibleAnomalies = [ "incompatiblePartialIdlExposure", "invalid", "noExposure", "noOriginalDefinition", + "overloaded", "redefined", "redefinedIncludes", + "redefinedMember", "redefinedWithDifferentTypes", "singleEnumValue", "unexpectedEventHandler", @@ -135,6 +138,61 @@ const listIdlTypes = type => { return listIdlTypes(type.idlType); }; +// Helper to test if two members define the same thing, such as the same +// attribute or the same method. Should match requirements in spec: +// https://heycam.github.io/webidl/#idl-overloading +function isOverloadedOperation(a, b) { + if (a.type !== "constructor" && a.type !== "operation") { + return false; + } + if (a.type !== b.type) { + return false; + } + // Note that |name| or |special| could be null/undefined, but even then + // they have to be the same for both members. + if (a.name !== b.name) { + return false; + } + if (a.special !== b.special) { + return false; + } + return true; +} + +// Helper to test if two members define an operation with the same identifier, +// one of them being a static operation and the other a regular one. This is +// allowed in Web IDL, see https://github.com/whatwg/webidl/issues/1097 +function isAllowedOperationWithSameIdentifier(a, b) { + if (a.type !== "operation") { + return false; + } + if (a.type !== b.type) { + return false; + } + if (a.name !== b.name) { + return false; + } + if (a.special !== "static" && b.special !== "static") { + return false; + } + if (a.special === b.special) { + return false; + } + return true; +} + +function describeMember(member) { + let desc = member.type; + if (member.name) { + desc += " " + member.name; + } + if (member.special) { + desc = member.special + " " + desc; + } + return desc; +} + + async function studyWebIdl(edResults, curatedResults) { const report = []; // List of anomalies to report const dfns = {}; // Index of IDL definitions (save includes) @@ -262,13 +320,13 @@ async function studyWebIdl(edResults, curatedResults) { dfn = dfn ?? node; for (const [key, value] of Object.entries(node)) { if (key === "idlType") { - const idlTypes = listIdlTypes(value); - for (const idlType of idlTypes) { - if (!usedTypes[idlType]) { - usedTypes[idlType] = []; - } - usedTypes[idlType].push({ spec, idl: dfn }); - } + const idlTypes = listIdlTypes(value); + for (const idlType of idlTypes) { + if (!usedTypes[idlType]) { + usedTypes[idlType] = []; + } + usedTypes[idlType].push({ spec, idl: dfn }); + } } else if (key === "extAttrs" && Array.isArray(value)) { for (const extAttr of value) { @@ -285,6 +343,80 @@ async function studyWebIdl(edResults, curatedResults) { } } + function checkMembers(target, source) { + source = source ?? target; + const selfCheck = source === target; + const knownDuplicates = []; + if (!target.idl.members || !source.idl.members) { + return; + } + for (const targetMember of target.idl.members) { + if (!targetMember.name) { + continue; + } + + for (const sourceMember of source.idl.members) { + if (!sourceMember.name) { + continue; + } + if (targetMember === sourceMember) { + // Self check and same member, skip + continue; + } + if (targetMember.name !== sourceMember.name) { + continue; + } + + // Prepare anomaly parameters + let targetName = dfnName(target); + let sourceName = dfnName(source); + const specs = [target.spec]; + if (sourceName === targetName) { + // TODO: find a better way to disambiguate between both definitions + sourceName = 'another ' + sourceName; + } + if (target.spec !== source.spec) { + sourceName += ` (in ${specName(source.spec)})`; + // Should we also blame the "source" spec if we report an anomaly? + // We will if we're looking at two partial mixins or two partial + // interface definitions, since there's no way to tell which of them + // is supposed to pay attention to the other. We won't blame the + // "source" spec otherwise + if (target.idl.partial && source.idl.partial && + target.idl.type === source.idl.type) { + targetName += ` (in ${specName(target.spec)})`; + specs.push(source.spec); + } + } + + if (isOverloadedOperation(targetMember, sourceMember)) { + if (!selfCheck) { + recordAnomaly(specs, "overloaded", `"${describeMember(targetMember)}" in ${targetName} overloads an operation defined in ${sourceName}`); + } + break; + } + + // A static operation that has the same identifier as a regular one is OK + if (isAllowedOperationWithSameIdentifier(targetMember, sourceMember)) { + continue; + } + + if (!knownDuplicates.includes(targetMember.name)) { + if (selfCheck) { + recordAnomaly(specs, "redefinedMember", `"${targetMember.name}" in ${targetName} is defined more than once`); + } + else { + recordAnomaly(specs, "redefinedMember", `"${targetMember.name}" in ${targetName} duplicates a member defined in ${sourceName}`); + } + } + // No need to report the same redefined member twice + knownDuplicates.push(targetMember.name); + break; + } + } + } + + edResults // We're only interested in specs that define Web IDL content .filter(spec => !!spec.idl) @@ -298,14 +430,14 @@ async function studyWebIdl(edResults, curatedResults) { } catch (e) { recordAnomaly(spec, "invalid", e.message); - // Use fallback from curated version if available - // This avoids reporting errors e.g. of not-really unknown interfaces - try { - const ast = WebIDL2.parse(curatedResults.find(s => s.url === spec.url).idl); - return { spec, ast }; - } catch (e) { + // Use fallback from curated version if available + // This avoids reporting errors e.g. of not-really unknown interfaces + try { + const ast = WebIDL2.parse(curatedResults.find(s => s.url === spec.url).idl); + return { spec, ast }; + } catch (e) { return { spec }; - } + } } }) @@ -393,10 +525,25 @@ async function studyWebIdl(edResults, curatedResults) { if (dfns[name].length > 1) { recordAnomaly(dfns[name].map(({spec}) => spec), "redefined", `"${name}" is defined multiple times (with type ${type}) in ${specs.map(specName).join(', ')}`); } - else { - mainDef = dfns[name][0].idl; - } + mainDef = dfns[name][0].idl; } + + // If Web IDL adds new kinds of definitions, we will very likely need to + // adjust our logic. Let's not pretend that we understand new kinds + const knownDfnKinds = new Set([ + "callback interface", + "callback", + "dictionary", + "enum", + "interface", + "interface mixin", + "namespace", + "typedef" + ]); + if (!knownDfnKinds.has(mainDef.type)) { + throw new Error(`Unknown definition kind "${mainDef.type}" in parsed Web IDL: ${JSON.stringify(mainDef)}`); + } + for (let {spec, idl} of dfns[name]) { switch(idl.type) { case "dictionary": @@ -422,33 +569,32 @@ async function studyWebIdl(edResults, curatedResults) { const specs = getSpecs(statements); recordAnomaly(specs, "redefinedIncludes", `The includes statement "${key}" is defined more than once in ${specs.map(specName).join(', ')}`); } - else { - const statement = statements[0]; - includesStatements[key] = statement; - // Check target exists and is an interface - const target = dfns[statement.idl.target]; - if (!target) { - recordAnomaly(statement.spec, "unknownType", `Target "${statement.idl.target}" in includes statement "${key}" is not defined anywhere`); - } - // In theory, target is defined only once, but IDL may redefine it by - // mistake (already reported as an anomaly, no need to report it again). - // Let's just make sure that there is an "interface" definition. - else if (!target.find(({idl}) => idl.type === "interface")) { - recordAnomaly(statement.spec, "wrongKind", `Target "${statement.idl.target}" in includes statement "${key}" must be of kind "interface"`); - } + const statement = statements[0]; + includesStatements[key] = statement; - // Check mixin exists and is an interface mixin - const mixin = dfns[statement.idl.includes]; - if (!mixin) { - recordAnomaly(statement.spec, "unknownType", `Mixin "${statement.idl.includes}" in includes statement "${key}" is not defined anywhere`); - } - // In theory, mixin is defined only once, but IDL may redefine it by - // mistake (already reported as an anomaly, no need to report it again). - // let's just make sure that there is an "interface mixin" definition. - else if (!mixin.find(({idl}) => idl.type === "interface mixin")) { - recordAnomaly(statement.spec, "wrongKind", `Mixin "${statement.idl.includes}" in includes statement "${key}" must be of kind "interface mixin"`); - } + // Check target exists and is an interface + const target = dfns[statement.idl.target]; + if (!target) { + recordAnomaly(statement.spec, "unknownType", `Target "${statement.idl.target}" in includes statement "${key}" is not defined anywhere`); + } + // In theory, target is defined only once, but IDL may redefine it by + // mistake (already reported as an anomaly, no need to report it again). + // Let's just make sure that there is an "interface" definition. + else if (!target.find(({idl}) => idl.type === "interface")) { + recordAnomaly(statement.spec, "wrongKind", `Target "${statement.idl.target}" in includes statement "${key}" must be of kind "interface"`); + } + + // Check mixin exists and is an interface mixin + const mixin = dfns[statement.idl.includes]; + if (!mixin) { + recordAnomaly(statement.spec, "unknownType", `Mixin "${statement.idl.includes}" in includes statement "${key}" is not defined anywhere`); + } + // In theory, mixin is defined only once, but IDL may redefine it by + // mistake (already reported as an anomaly, no need to report it again). + // let's just make sure that there is an "interface mixin" definition. + else if (!mixin.find(({idl}) => idl.type === "interface mixin")) { + recordAnomaly(statement.spec, "wrongKind", `Mixin "${statement.idl.includes}" in includes statement "${key}" must be of kind "interface mixin"`); } } @@ -489,6 +635,62 @@ async function studyWebIdl(edResults, curatedResults) { } } + // Check duplicate/overloaded type members across partial and mixins + // Note: When the IDL is correct, there is only one non-partial dfn per type + // defined in the IDL. In practice, there may be re-definitions, which have + // already been reported as anomalies. Here we'll consider that all + // non-partial dfns are correct and we'll handle them separately. + for (const name in dfns) { + const mainDfns = dfns[name].filter(({idl}) => !idl.partial); + for (const mainDfn of mainDfns) { + // Check duplicate members within the main dfn itself + checkMembers(mainDfn); + + // Find all the partials and mixins, including partial mixins, that apply + // to the main dfn (note mixins are only for "interface" dfns) + const partials = dfns[name].filter((({idl}) => idl.partial && idl.type === mainDfn.idl.type)); + const mixins = mainDfn.idl.type !== "interface" ? [] : + Object.keys(includesStatements) + .filter(key => key.startsWith(`${name} includes `)) + .map(key => dfns[includesStatements[key].idl.includes]?.filter(({idl}) => idl.type === 'interface mixin')) + .filter(mixins => mixins?.length > 0) + .flat(); + // Compare members of partials, mixins and main dfn separately to be able + // to report more fine-grained anomalies + for (const partial of partials) { + // Check that the partial dfn itself does not define duplicate members + checkMembers(partial); + + // Check partial members against the main dfn + checkMembers(partial, mainDfn); + + // Check partial members against partials before it + for (const otherPartial of partials) { + if (otherPartial === partial) { + break; + } + checkMembers(partial, otherPartial); + } + } + + // Note we don't need to compare mixins and partial mixins with other + // mixins and partial mixins because the loop already takes care of that + // when it goes through the mixin dfn as a main dfn. + // Also note that, in case of a duplicated member, we're going to blame + // the main (or partial) dfn, and not the mixin (or partial mixin), on + // the grounds that an interface decides to include a mixin, and not the + // other way round. + for (const mixin of mixins) { + // Check mixin members against the main dfn + checkMembers(mainDfn, mixin); + + // Check mixin members against partials + for (const partial of partials) { + checkMembers(partial, mixin); + } + } + } + } return report; } diff --git a/test/study-webidl.js b/test/study-webidl.js index 458bd0c4..829e54d1 100644 --- a/test/study-webidl.js +++ b/test/study-webidl.js @@ -5,27 +5,59 @@ const assert = require('assert').strict; const { studyWebIdl } = require('../src/lib/study-webidl'); -describe('The Web IDL analyser', async _ => { +describe('The Web IDL analyser', async () => { const specUrl = 'https://www.w3.org/TR/spec'; const specUrl2 = 'https://www.w3.org/TR/spec2'; - function toCrawlResult(idl, url) { - url = url ?? specUrl; - return [ { url, idl } ]; + function toCrawlResult(idl, idlSpec2) { + const crawlResult = [ { url: specUrl, idl } ]; + if (idlSpec2) { + crawlResult.push({ url: specUrl2, idl: idlSpec2 }); + } + return crawlResult; } - async function analyzeIdl(idl) { - const crawlResult = toCrawlResult(idl); + function analyzeIdl(idl, idlSpec2) { + const crawlResult = toCrawlResult(idl, idlSpec2); return studyWebIdl(crawlResult); } + function assertNbAnomalies(report, length) { + assert.deepEqual(report.length, length, + `Expected ${length} anomalies but got ${report.length}. Full report received:\n` + + JSON.stringify(report, null, 2)); + } + + function assertAnomaly(report, idx, value) { + const msg = `Mismatch for anomaly at index ${idx}. Full anomaly received:\n` + + JSON.stringify(report[idx], null, 2); + function assertMatch(actual, expected) { + if (Array.isArray(expected)) { + assert(Array.isArray(actual), msg); + assert.deepEqual(actual.length, expected.length, msg); + for (let i=0; i { const report = await analyzeIdl(` [Global=Window,Exposed=*] interface Valid {}; `); - assert.deepEqual(report, []); + assertNbAnomalies(report, 0); }); @@ -34,30 +66,34 @@ interface Valid {}; [Global=Window,Exposed=*] interface Invalid; `); - assert.deepEqual(report[0]?.name, 'invalid'); - assert.deepEqual(report[0].message, `Syntax error at line 3, since \`interface Invalid\`: + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + category: 'webidl', + name: 'invalid', + message: `Syntax error at line 3, since \`interface Invalid\`: interface Invalid; - ^ Bodyless interface`); - assert.deepEqual(report[0].category, 'webidl'); - assert.deepEqual(report[0].specs?.length, 1); - assert.deepEqual(report[0].specs[0].url, specUrl); + ^ Bodyless interface`, + specs: [ { url: specUrl }] + }); }); it('reports invalid IDL and uses fallback from curated', async () => { const crawlResult = toCrawlResult(` +// IDL in first spec [Global=Window,Exposed=*] interface Invalid; -`).concat(toCrawlResult(` +`, ` +// IDL in second spec [Global=Window,Exposed=*] interface Valid: Invalid {}; -`, specUrl2)); +`); const curatedResult = toCrawlResult(` [Global=Window,Exposed=*] interface Invalid{}; `); const report = await studyWebIdl(crawlResult, curatedResult); - assert.deepEqual(report.length, 1); - assert.deepEqual(report[0]?.name, 'invalid'); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { name: 'invalid' }); }); @@ -70,7 +106,7 @@ interface Invalid; [Global=Window,Exposed=*] interface Valid {}; `); - assert.deepEqual(report, []); + assertNbAnomalies(report, 0); }); @@ -78,8 +114,11 @@ interface Valid {}; const report = await analyzeIdl(` interface Unexposed {}; `); - assert.deepEqual(report[0]?.name, 'noExposure'); - assert.deepEqual(report[0].message, 'The interface "Unexposed" has no [Exposed] extended attribute'); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'noExposure', + message: 'The interface "Unexposed" has no [Exposed] extended attribute' + }); }); @@ -88,8 +127,11 @@ interface Unexposed {}; [Exposed=Unknown] interface WhereIAm {}; `); - assert.deepEqual(report[0]?.name, 'unknownExposure'); - assert.deepEqual(report[0].message, 'The [Exposed] extended attribute of the interface "WhereIAm" references unknown global(s): Unknown'); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'unknownExposure', + message: 'The [Exposed] extended attribute of the interface "WhereIAm" references unknown global(s): Unknown' + }); }); it('reports no anomaly for valid EventHandler attributes definitions', async () => { @@ -107,19 +149,28 @@ interface Carlos : EventTarget { [Exposed=*] interface EventTarget {}; `); - assert.deepEqual(report, []); + assertNbAnomalies(report, 0); }); it('detects unexpected EventHandler attributes', async () => { const report = await analyzeIdl(` +[Exposed=*] +interface Event {}; +[LegacyTreatNonObjectAsNull] +callback EventHandlerNonNull = any (Event event); +typedef EventHandlerNonNull? EventHandler; + [Global=Window,Exposed=*] interface Carlos { attribute EventHandler onbigbisous; }; `); - assert.deepEqual(report[0]?.name, 'unexpectedEventHandler'); - assert.deepEqual(report[0].message, 'The interface "Carlos" defines an event handler "onbigbisous" but does not inherit from EventTarget'); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'unexpectedEventHandler', + message: 'The interface "Carlos" defines an event handler "onbigbisous" but does not inherit from EventTarget' + }); }); @@ -142,9 +193,11 @@ partial interface Somewhere {}; [Exposed=Elsewhere] partial interface MyPlace {}; `); - assert.deepEqual(report[0]?.name, 'incompatiblePartialIdlExposure'); - assert.deepEqual(report[0].message, 'The [Exposed] extended attribute of the partial interface "MyPlace" references globals on which the original interface is not exposed: Elsewhere (original exposure: Somewhere)'); - assert.deepEqual(report.length, 1); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'incompatiblePartialIdlExposure', + message: 'The [Exposed] extended attribute of the partial interface "MyPlace" references globals on which the original interface is not exposed: Elsewhere (original exposure: Somewhere)' + }); }); @@ -158,50 +211,53 @@ interface Somewhere {}; [Exposed=*] partial interface Somewhere {}; `); - assert.deepEqual(report[0]?.name, 'incompatiblePartialIdlExposure'); - assert.deepEqual(report[0].message, 'The partial interface "Somewhere" is exposed on all globals but the original interface is not (Somewhere)'); - assert.deepEqual(report.length, 1); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'incompatiblePartialIdlExposure', + message: 'The partial interface "Somewhere" is exposed on all globals but the original interface is not (Somewhere)' + }); }); it('detects IDL names that are redefined across specs', async () => { - const crawlResult = toCrawlResult(` + const report = await analyzeIdl(` +// IDL in first spec dictionary GrandBob { required boolean complete; }; -`).concat(toCrawlResult(` +`, ` +// IDL in second spec dictionary GrandBob { required boolean incomplete; }; -`, specUrl2)); - const report = await studyWebIdl(crawlResult); - - assert.deepEqual(report[0]?.name, 'redefined'); - assert.deepEqual(report[0].message, `"GrandBob" is defined as a non-partial dictionary mutiple times in ${specUrl}, ${specUrl2}`); - assert.deepEqual(report[0].specs?.length, 2); - assert.deepEqual(report[0].specs[0].url, specUrl); - assert.deepEqual(report[0].specs[1].url, specUrl2); - assert.deepEqual(report.length, 1); +`); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'redefined', + message: `"GrandBob" is defined as a non-partial dictionary mutiple times in ${specUrl}, ${specUrl2}`, + specs: [ { url: specUrl }, { url: specUrl2 }] + }); }); it('detects IDL names that are redefined with different types across specs', async () => { - const crawlResult = toCrawlResult( -`dictionary GrandBob { + const report = await analyzeIdl(` +// IDL in first spec +dictionary GrandBob { required boolean complete; -};`).concat(toCrawlResult( -`enum GrandBob { +};`, ` +// IDL in second spec +enum GrandBob { "complete", "incomplete" -};`, specUrl2)); - const report = await studyWebIdl(crawlResult); - - assert.deepEqual(report[0]?.name, 'redefinedWithDifferentTypes'); - assert.deepEqual(report[0].message, `"GrandBob" is defined multiple times with different types (dictionary, enum) in ${specUrl}, ${specUrl2}`); - assert.deepEqual(report[0].specs?.length, 2); - assert.deepEqual(report[0].specs[0].url, specUrl); - assert.deepEqual(report[0].specs[1].url, specUrl2); - assert.deepEqual(report.length, 1); +}; +`); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'redefinedWithDifferentTypes', + message: `"GrandBob" is defined multiple times with different types (dictionary, enum) in ${specUrl}, ${specUrl2}`, + specs: [ { url: specUrl }, { url: specUrl2 }] + }); }); @@ -209,9 +265,11 @@ dictionary GrandBob { const report = await analyzeIdl(` partial interface MyPlace {}; `); - assert.deepEqual(report[0]?.name, 'noOriginalDefinition'); - assert.deepEqual(report[0].message, `"MyPlace" is only defined as a partial interface (in ${specUrl})`); - assert.deepEqual(report.length, 1); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'noOriginalDefinition', + message: `"MyPlace" is only defined as a partial interface (in ${specUrl})` + }); }); @@ -221,9 +279,11 @@ enum SingleValue { "single" }; `); - assert.deepEqual(report[0]?.name, 'singleEnumValue'); - assert.deepEqual(report[0].message, `The enum "SingleValue" has fewer than 2 possible values`); - assert.deepEqual(report.length, 1); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'singleEnumValue', + message: `The enum "SingleValue" has fewer than 2 possible values` + }); }); @@ -236,11 +296,15 @@ enum WrongCase { "not_good" }; `); - assert.deepEqual(report[0]?.name, 'wrongCaseEnumValue'); - assert.deepEqual(report[0].message, `The value "NotGood" of the enum "WrongCase" does not match the expected conventions (lower case, hyphen separated words)`); - assert.deepEqual(report[1]?.name, 'wrongCaseEnumValue'); - assert.deepEqual(report[1].message, `The value "not_good" of the enum "WrongCase" does not match the expected conventions (lower case, hyphen separated words)`); - assert.deepEqual(report.length, 2); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'wrongCaseEnumValue', + message: `The value "NotGood" of the enum "WrongCase" does not match the expected conventions (lower case, hyphen separated words)` + }); + assertAnomaly(report, 1, { + name: 'wrongCaseEnumValue', + message: `The value "not_good" of the enum "WrongCase" does not match the expected conventions (lower case, hyphen separated words)` + }); }); @@ -254,9 +318,11 @@ MyHome includes MyRoom; MyHome includes MyLivingRoom; MyHome includes MyRoom; `); - assert.deepEqual(report[0]?.name, 'redefinedIncludes'); - assert.deepEqual(report[0].message, `The includes statement "MyHome includes MyRoom" is defined more than once in ${specUrl}`); - assert.deepEqual(report.length, 1); + assertNbAnomalies(report, 1); + assertAnomaly(report, 0, { + name: 'redefinedIncludes', + message: `The includes statement "MyHome includes MyRoom" is defined more than once in ${specUrl}` + }); }); @@ -264,11 +330,15 @@ MyHome includes MyRoom; const report = await analyzeIdl(` MyHome includes MyRoom; `); - assert.deepEqual(report[0]?.name, 'unknownType'); - assert.deepEqual(report[0].message, `Target "MyHome" in includes statement "MyHome includes MyRoom" is not defined anywhere`); - assert.deepEqual(report[1]?.name, 'unknownType'); - assert.deepEqual(report[1].message, `Mixin "MyRoom" in includes statement "MyHome includes MyRoom" is not defined anywhere`); - assert.deepEqual(report.length, 2); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'unknownType', + message: `Target "MyHome" in includes statement "MyHome includes MyRoom" is not defined anywhere` + }); + assertAnomaly(report, 1, { + name: 'unknownType', + message: `Mixin "MyRoom" in includes statement "MyHome includes MyRoom" is not defined anywhere` + }); }); it('checks kinds of target and mixin in includes statements', async () => { @@ -278,11 +348,15 @@ dictionary MyHome { required boolean door; }; MyHome includes MyRoom; `); - assert.deepEqual(report[0]?.name, 'wrongKind'); - assert.deepEqual(report[0].message, `Target "MyHome" in includes statement "MyHome includes MyRoom" must be of kind "interface"`); - assert.deepEqual(report[1]?.name, 'wrongKind'); - assert.deepEqual(report[1].message, `Mixin "MyRoom" in includes statement "MyHome includes MyRoom" must be of kind "interface mixin"`); - assert.deepEqual(report.length, 2); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'wrongKind', + message: `Target "MyHome" in includes statement "MyHome includes MyRoom" must be of kind "interface"` + }); + assertAnomaly(report, 1, { + name: 'wrongKind', + message: `Mixin "MyRoom" in includes statement "MyHome includes MyRoom" must be of kind "interface mixin"` + }); }); @@ -293,11 +367,15 @@ MyHome includes MyRoom; [Exposed=*] interface MyLivingRoom : MyRoom {}; dictionary MyShelf : MyHome { required boolean full; }; `); - assert.deepEqual(report[0]?.name, 'unknownType'); - assert.deepEqual(report[0].message, `"MyLivingRoom" inherits from "MyRoom" which is not defined anywhere`); - assert.deepEqual(report[1]?.name, 'wrongKind'); - assert.deepEqual(report[1].message, `"MyShelf" is of kind "dictionary" but inherits from "MyHome" which is of kind "interface"`); - assert.deepEqual(report.length, 2); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'unknownType', + message: `"MyLivingRoom" inherits from "MyRoom" which is not defined anywhere` + }); + assertAnomaly(report, 1, { + name: 'wrongKind', + message: `"MyShelf" is of kind "dictionary" but inherits from "MyHome" which is of kind "interface"` + }); }); @@ -311,15 +389,23 @@ dictionary MyShelf : MyHome { required boolean full; }; attribute (DOMString or sequence) table; }; `); - assert.deepEqual(report[0]?.name, 'unknownType'); - assert.deepEqual(report[0].message, `Unknown type "bool" used in definition of "MyRoom"`); - assert.deepEqual(report[1]?.name, 'unknownType'); - assert.deepEqual(report[1].message, `Unknown type "MyBed" used in definition of "MyRoom"`); - assert.deepEqual(report[2]?.name, 'unknownType'); - assert.deepEqual(report[2].message, `Unknown type "MyUnknownType" used in definition of "MyRoom"`); - assert.deepEqual(report[3]?.name, 'unknownType'); - assert.deepEqual(report[3].message, `Unknown type "UnknownInnerType" used in definition of "MyRoom"`); - assert.deepEqual(report.length, 4); + assertNbAnomalies(report, 4); + assertAnomaly(report, 0, { + name: 'unknownType', + message: `Unknown type "bool" used in definition of "MyRoom"` + }); + assertAnomaly(report, 1, { + name: 'unknownType', + message: `Unknown type "MyBed" used in definition of "MyRoom"` + }); + assertAnomaly(report, 2, { + name: 'unknownType', + message: `Unknown type "MyUnknownType" used in definition of "MyRoom"` + }); + assertAnomaly(report, 3, { + name: 'unknownType', + message: `Unknown type "UnknownInnerType" used in definition of "MyRoom"` + }); }); @@ -336,15 +422,23 @@ interface mixin MyLivingRoom {}; namespace MyNamespaceMixin {}; interface mixin MyNamespaceMixin {}; `); - assert.deepEqual(report.length, 4); - assert.deepEqual(report[0].name, 'redefinedWithDifferentTypes'); - assert.deepEqual(report[0].message, `"MyNamespaceMixin" is defined multiple times with different types (namespace, interface mixin) in ${specUrl}`); - assert.deepEqual(report[1].name, 'wrongType'); - assert.deepEqual(report[1].message, `Namespace "MyNamespace" cannot be used as a type in definition of "MyHome"`); - assert.deepEqual(report[2].name, 'wrongType'); - assert.deepEqual(report[2].message, `Interface mixin "MyLivingRoom" cannot be used as a type in definition of "MyHome"`); - assert.deepEqual(report[3].name, 'wrongType'); - assert.deepEqual(report[3].message, `Name "MyNamespaceMixin" exists but is not a type and cannot be used in definition of "MyHome"`); + assertNbAnomalies(report, 4); + assertAnomaly(report, 0, { + name: 'redefinedWithDifferentTypes', + message: `"MyNamespaceMixin" is defined multiple times with different types (namespace, interface mixin) in ${specUrl}` + }); + assertAnomaly(report, 1, { + name: 'wrongType', + message: `Namespace "MyNamespace" cannot be used as a type in definition of "MyHome"` + }); + assertAnomaly(report, 2, { + name: 'wrongType', + message: `Interface mixin "MyLivingRoom" cannot be used as a type in definition of "MyHome"` + }); + assertAnomaly(report, 3, { + name: 'wrongType', + message: `Name "MyNamespaceMixin" exists but is not a type and cannot be used in definition of "MyHome"` + }); }); @@ -361,12 +455,331 @@ interface mixin MyNamespaceMixin {}; attribute boolean hasTVToo; }; `); - assert.deepEqual(report[0]?.name, 'unknownExtAttr'); - assert.deepEqual(report[0].message, `Unknown extended attribute "UnknownExtAttr" used in definition of "MyRoom"`); - assert.deepEqual(report[1]?.name, 'unknownExtAttr'); - assert.deepEqual(report[1].message, `Unknown extended attribute "SuperUnknownExtAttr" used in definition of "MyLivingRoom"`); - assert.deepEqual(report[2]?.name, 'unknownExtAttr'); - assert.deepEqual(report[2].message, `Unknown extended attribute "SuperUnknownExtAttr" used in definition of "MyBedRoom"`); - assert.deepEqual(report.length, 3); + assertNbAnomalies(report, 3); + assertAnomaly(report, 0, { + name: 'unknownExtAttr', + message: `Unknown extended attribute "UnknownExtAttr" used in definition of "MyRoom"` + }); + assertAnomaly(report, 1, { + name: 'unknownExtAttr', + message: `Unknown extended attribute "SuperUnknownExtAttr" used in definition of "MyLivingRoom"` + }); + assertAnomaly(report, 2, { + name: 'unknownExtAttr', + message: `Unknown extended attribute "SuperUnknownExtAttr" used in definition of "MyBedRoom"` + }); + }); + + + it('reports overloads across definitions (partial, same spec)', async () => { + const report = await analyzeIdl(` +[Global=Home,Exposed=*] +interface MyHome { + undefined overload(); + undefined overload(DOMString thing); +}; + +partial interface MyHome { + undefined overload(DOMString thing, boolean asap); +}; + +[Exposed=*] +interface MyPartialHome {}; + +partial interface MyPartialHome { + Promise overload(); +}; + +partial interface MyPartialHome { + Promise overload(DOMString thing); +}; +`); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'overloaded', + message: '"operation overload" in partial interface "MyHome" overloads an operation defined in interface "MyHome"' + }); + assertAnomaly(report, 1, { + name: 'overloaded', + message: '"operation overload" in partial interface "MyPartialHome" overloads an operation defined in another partial interface "MyPartialHome"' + }); + }); + + + it('reports overloads across definitions (partial, different specs)', async () => { + const report = await analyzeIdl(` +// IDL in first spec +[Global=Home,Exposed=*] +interface MyHome { + undefined overload(); + undefined overload(DOMString thing); +}; + +[Exposed=*] +interface MyPartialHome {}; + +partial interface MyPartialHome { + Promise overload(); +}; +`, ` +// IDL in second spec +partial interface MyHome { + undefined overload(DOMString thing, boolean asap); +}; + +partial interface MyPartialHome { + Promise overload(DOMString thing); +}; +`); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'overloaded', + message: `"operation overload" in partial interface "MyHome" overloads an operation defined in interface "MyHome" (in ${specUrl})`, + specs: [ { url: specUrl2 }] + }); + + // No way to know which spec to blame for MyPartialHome overloads, both + // should be reported + assertAnomaly(report, 1, { + name: 'overloaded', + message: `"operation overload" in partial interface "MyPartialHome" (in ${specUrl2}) overloads an operation defined in another partial interface "MyPartialHome" (in ${specUrl})`, + specs: [ { url: specUrl2 }, { url: specUrl }] + }); + }); + + + it('reports overloads across definitions (mixin, same spec)', async () => { + const report = await analyzeIdl(` +[Global=Home,Exposed=*] +interface MyHome { + undefined overload(); +}; +MyHome includes MyRoom; + +interface mixin MyRoom { + undefined overload(DOMString thing); +}; + +interface mixin MyPartialRoom { + Promise overload(); +}; + +partial interface mixin MyPartialRoom { + Promise overload(DOMString thing); +}; +`); + assertNbAnomalies(report, 2); + assertAnomaly(report, 0, { + name: 'overloaded', + message: '"operation overload" in interface "MyHome" overloads an operation defined in interface mixin "MyRoom"' + }); + assertAnomaly(report, 1, { + name: 'overloaded', + message: '"operation overload" in partial interface mixin "MyPartialRoom" overloads an operation defined in interface mixin "MyPartialRoom"' + }); + }); + + + it('reports overloads across definitions (mixin, different specs)', async () => { + const report = await analyzeIdl(` +// IDL in first spec +[Global=Home,Exposed=*] +interface MyHome { + undefined overload(); +}; +MyHome includes MyRoom; + +interface mixin MyPartialRoom { + Promise overload(); +}; + +partial interface mixin MyPartialRoom { + Promise overload(DOMString thing); +}; +`, ` +// IDL in second spec +interface mixin MyRoom { + undefined overload(DOMString thing); +}; + +partial interface mixin MyPartialRoom { + Promise overload(DOMString thing, boolean asap); +}; +`); + assertNbAnomalies(report, 4); + assertAnomaly(report, 0, { + name: 'overloaded', + message: `"operation overload" in interface "MyHome" overloads an operation defined in interface mixin "MyRoom" (in ${specUrl2})`, + specs: [ { url: specUrl }] + }); + + assertAnomaly(report, 1, { + name: 'overloaded', + message: '"operation overload" in partial interface mixin "MyPartialRoom" overloads an operation defined in interface mixin "MyPartialRoom"', + specs: [ { url: specUrl }] + }); + + assertAnomaly(report, 2, { + name: 'overloaded', + message: `"operation overload" in partial interface mixin "MyPartialRoom" overloads an operation defined in interface mixin "MyPartialRoom" (in ${specUrl})`, + specs: [ { url: specUrl2 }] + }); + + assertAnomaly(report, 3, { + name: 'overloaded', + message: `"operation overload" in partial interface mixin "MyPartialRoom" (in ${specUrl2}) overloads an operation defined in another partial interface mixin "MyPartialRoom" (in ${specUrl})`, + specs: [ { url: specUrl2 }, { url: specUrl }] + }); + }); + + + it('reports overloads across definitions (partial and mixin, same spec)', async () => { + const report = await analyzeIdl(` +[Global=Home,Exposed=*] +interface MyHome {}; +MyHome includes MyRoom; + +partial interface MyHome { + undefined overload(DOMString thing); +}; + +interface mixin MyRoom { + undefined overload(DOMString thing); +}; + +partial interface mixin MyRoom { + undefined overload(DOMString thing, boolean asap); +}; +`); + assertNbAnomalies(report, 3); + assertAnomaly(report, 0, { + name: 'overloaded', + message: '"operation overload" in partial interface "MyHome" overloads an operation defined in interface mixin "MyRoom"' + }); + assertAnomaly(report, 1, { + name: 'overloaded', + message: '"operation overload" in partial interface "MyHome" overloads an operation defined in partial interface mixin "MyRoom"' + }); + assertAnomaly(report, 2, { + name: 'overloaded', + message: '"operation overload" in partial interface mixin "MyRoom" overloads an operation defined in interface mixin "MyRoom"' + }); + }); + + + it('reports overloads across definitions (partial and mixin, different specs)', async () => { + const report = await analyzeIdl(` +// IDL in first spec +[Global=Home,Exposed=*] +interface MyHome {}; +MyHome includes MyRoom; + +partial interface MyHome { + undefined overload(DOMString thing); +}; +`, ` +// IDL in second spec +interface mixin MyRoom { + undefined overload(DOMString thing); +}; + +partial interface mixin MyRoom { + undefined overload(DOMString thing, boolean asap); +}; +`); + assertNbAnomalies(report, 3); + assertAnomaly(report, 0, { + name: 'overloaded', + message: `"operation overload" in partial interface "MyHome" overloads an operation defined in interface mixin "MyRoom" (in ${specUrl2})`, + specs: [ { url: specUrl }] + }); + + assertAnomaly(report, 1, { + name: 'overloaded', + message: `"operation overload" in partial interface "MyHome" overloads an operation defined in partial interface mixin "MyRoom" (in ${specUrl2})`, + specs: [ { url: specUrl }] + }); + + assertAnomaly(report, 2, { + name: 'overloaded', + message: '"operation overload" in partial interface mixin "MyRoom" overloads an operation defined in interface mixin "MyRoom"', + specs: [ { url: specUrl2 }] + }); + }); + + + it('reports redefined members (same spec)', async () => { + const report = await analyzeIdl(` +[Global=Home,Exposed=*] +interface MyHome { + undefined dejaVu(); + attribute boolean dejaVu; +}; +MyHome includes MyRoom; + +partial interface MyHome { + attribute DOMString dejaVu; +}; + +interface mixin MyRoom { + attribute unsigned long dejaVu; +}; +`); + assertNbAnomalies(report, 4); + assertAnomaly(report, 0, { + name: 'redefinedMember', + message: '"dejaVu" in interface "MyHome" is defined more than once' + }); + assertAnomaly(report, 1, { + name: 'redefinedMember', + message: '"dejaVu" in partial interface "MyHome" duplicates a member defined in interface "MyHome"' + }); + assertAnomaly(report, 2, { + name: 'redefinedMember', + message: '"dejaVu" in interface "MyHome" duplicates a member defined in interface mixin "MyRoom"' + }); + assertAnomaly(report, 3, { + name: 'redefinedMember', + message: '"dejaVu" in partial interface "MyHome" duplicates a member defined in interface mixin "MyRoom"' + }); + }); + + + it('reports redefined members (different specs)', async () => { + const report = await analyzeIdl(` +// IDL in first spec +[Global=Home,Exposed=*] +interface MyHome { + undefined dejaVu(); +}; +MyHome includes MyRoom; +`, ` +// IDL in second spec +partial interface MyHome { + attribute DOMString dejaVu; +}; + +interface mixin MyRoom { + attribute unsigned long dejaVu; +}; +`); + assertNbAnomalies(report, 3); + assertAnomaly(report, 0, { + name: 'redefinedMember', + message: `"dejaVu" in partial interface "MyHome" duplicates a member defined in interface "MyHome" (in ${specUrl})`, + specs: [ { url: specUrl2 }] + }); + + assertAnomaly(report, 1, { + name: 'redefinedMember', + message: `"dejaVu" in interface "MyHome" duplicates a member defined in interface mixin "MyRoom" (in ${specUrl2})`, + specs: [ { url: specUrl }] + }); + + assertAnomaly(report, 2, { + name: 'redefinedMember', + message: '"dejaVu" in partial interface "MyHome" duplicates a member defined in interface mixin "MyRoom"', + specs: [ { url: specUrl2 }] + }); }); });