mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
Harden WebExtension compatibility linting
This commit is contained in:
parent
0a5aa84e87
commit
408fe82021
|
|
@ -5,21 +5,66 @@ function propertyNameOrValue(n) {
|
|||
return n.property.type == "Literal" ? n.property.value : n.property.name
|
||||
}
|
||||
|
||||
function isVersionNewer(version, minimumVersion) {
|
||||
const versionParts = String(version).split(".").map(Number)
|
||||
const minimumParts = String(minimumVersion).split(".").map(Number)
|
||||
if (versionParts.some(Number.isNaN) || minimumParts.some(Number.isNaN))
|
||||
return false
|
||||
function compareVersions(left, right) {
|
||||
const versionPattern = /^(≤)?\d+(\.\d+)*$/
|
||||
if (!versionPattern.test(String(left)) || !versionPattern.test(String(right)))
|
||||
return undefined
|
||||
const versionParts = String(left).replace(/^≤/, "").split(".").map(Number)
|
||||
const minimumParts = String(right).replace(/^≤/, "").split(".").map(Number)
|
||||
const length = Math.max(versionParts.length, minimumParts.length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
const difference = (versionParts[i] || 0) - (minimumParts[i] || 0)
|
||||
if (difference !== 0) return difference > 0
|
||||
if (difference !== 0) return difference
|
||||
}
|
||||
return false
|
||||
return 0
|
||||
}
|
||||
|
||||
function detectBrowserUsage(context, node, browser, minimumVersion) {
|
||||
let localApi = api
|
||||
function supportState(statement, minimumVersion) {
|
||||
if (Array.isArray(statement)) {
|
||||
const states = statement.map(item => supportState(item, minimumVersion))
|
||||
if (states.includes("supported")) return "supported"
|
||||
if (states.includes("tooRecent")) return "tooRecent"
|
||||
return "unsupported"
|
||||
}
|
||||
if (
|
||||
!statement ||
|
||||
typeof statement !== "object" ||
|
||||
statement.flags ||
|
||||
statement.prefix ||
|
||||
statement.alternative_name ||
|
||||
statement.partial_implementation
|
||||
) {
|
||||
return "unsupported"
|
||||
}
|
||||
const added = statement.version_added
|
||||
if (added === false || added === null || added === undefined)
|
||||
return "unsupported"
|
||||
if (statement.version_removed) {
|
||||
if (minimumVersion === undefined) return "unsupported"
|
||||
const removed = compareVersions(statement.version_removed, minimumVersion)
|
||||
if (removed === undefined || removed <= 0) return "unsupported"
|
||||
}
|
||||
if (minimumVersion === undefined) {
|
||||
return added === true || compareVersions(added, added) !== undefined
|
||||
? "supported"
|
||||
: "unsupported"
|
||||
}
|
||||
if (added === true) return "supported"
|
||||
const comparison = compareVersions(added, minimumVersion)
|
||||
if (comparison === undefined) return "unsupported"
|
||||
if (String(added).startsWith("≤") && comparison > 0)
|
||||
return "unsupported"
|
||||
return comparison > 0 ? "tooRecent" : "supported"
|
||||
}
|
||||
|
||||
function detectBrowserUsage(
|
||||
context,
|
||||
node,
|
||||
browser,
|
||||
minimumVersion,
|
||||
compatibilityApi,
|
||||
) {
|
||||
let localApi = compatibilityApi
|
||||
const fullName = []
|
||||
while (
|
||||
node.type == "MemberExpression" &&
|
||||
|
|
@ -33,8 +78,9 @@ function detectBrowserUsage(context, node, browser, minimumVersion) {
|
|||
if (!localApi.__compat) {
|
||||
continue
|
||||
}
|
||||
const support = localApi.__compat.support
|
||||
if (support[browser].version_added === false) {
|
||||
const support = localApi.__compat.support || {}
|
||||
const state = supportState(support[browser], minimumVersion)
|
||||
if (state === "unsupported") {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "unsupportedApis",
|
||||
|
|
@ -43,48 +89,53 @@ function detectBrowserUsage(context, node, browser, minimumVersion) {
|
|||
api: fullName.join("."),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const version = support[browser].version_added
|
||||
if (
|
||||
minimumVersion !== undefined &&
|
||||
isVersionNewer(version, minimumVersion)
|
||||
) {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "apiTooRecent",
|
||||
data: {
|
||||
api: fullName.join("."),
|
||||
name: browser,
|
||||
version: minimumVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (state === "tooRecent") {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "apiTooRecent",
|
||||
data: {
|
||||
api: fullName.join("."),
|
||||
name: browser,
|
||||
version: minimumVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = browser => ({
|
||||
meta: {
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { minimumVersion: { type: "string" } },
|
||||
additionalProperties: false,
|
||||
function createUnsupportedApis(browser, compatibilityApi = api) {
|
||||
return {
|
||||
meta: {
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { minimumVersion: { type: "string" } },
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
unsupportedApis: "{{ api }} unsupported on '{{ name }}'",
|
||||
apiTooRecent:
|
||||
"{{ api }} is not supported on {{ name }} {{ version }}",
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
unsupportedApis: "{{ api }} unsupported on '{{ name }}'",
|
||||
apiTooRecent:
|
||||
"{{ api }} is not supported on {{ name }} {{ version }}",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { minimumVersion } = context.options[0] || {}
|
||||
const detect = node =>
|
||||
detectBrowserUsage(context, node, browser, minimumVersion)
|
||||
return {
|
||||
'MemberExpression[object.name="browser"]': detect,
|
||||
'MemberExpression[object.name="browserBg"]': detect,
|
||||
}
|
||||
},
|
||||
})
|
||||
create(context) {
|
||||
const { minimumVersion } = context.options[0] || {}
|
||||
const detect = node =>
|
||||
detectBrowserUsage(
|
||||
context,
|
||||
node,
|
||||
browser,
|
||||
minimumVersion,
|
||||
compatibilityApi,
|
||||
)
|
||||
return {
|
||||
'MemberExpression[object.name="browser"]': detect,
|
||||
'MemberExpression[object.name="browserBg"]': detect,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createUnsupportedApis
|
||||
module.exports.supportState = supportState
|
||||
|
|
|
|||
97
custom-eslint-rules/tests/unsupported-apis.test.js
Normal file
97
custom-eslint-rules/tests/unsupported-apis.test.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
const { Linter } = require("eslint")
|
||||
const unsupportedApis = require("../lib/unsupported-apis")
|
||||
|
||||
function compat(support) {
|
||||
return { __compat: { support } }
|
||||
}
|
||||
|
||||
function verify(code, api, browser = "firefox", minimumVersion = "68") {
|
||||
const linter = new Linter()
|
||||
linter.defineRule("unsupported", unsupportedApis(browser, api))
|
||||
return linter.verify(code, {
|
||||
parserOptions: { ecmaVersion: 2020 },
|
||||
rules: { unsupported: ["error", { minimumVersion }] },
|
||||
})
|
||||
}
|
||||
|
||||
test("detects browser, browserBg, and computed API access", () => {
|
||||
const api = { tabs: { hide: compat({ firefox: { version_added: false } }) } }
|
||||
const messages = verify(
|
||||
'browser.tabs.hide(); browserBg.tabs.hide(); browser["tabs"]["hide"]()',
|
||||
api,
|
||||
)
|
||||
expect(messages.map(message => message.messageId)).toEqual([
|
||||
"unsupportedApis",
|
||||
"unsupportedApis",
|
||||
"unsupportedApis",
|
||||
])
|
||||
})
|
||||
|
||||
test("checks target minimum versions", () => {
|
||||
const api = {
|
||||
tabs: {
|
||||
old: compat({ firefox: { version_added: "67" } }),
|
||||
exact: compat({ firefox: { version_added: "68" } }),
|
||||
recent: compat({ firefox: { version_added: "69" } }),
|
||||
},
|
||||
}
|
||||
const messages = verify(
|
||||
"browser.tabs.old; browser.tabs.exact; browser.tabs.recent",
|
||||
api,
|
||||
)
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages[0].messageId).toBe("apiTooRecent")
|
||||
})
|
||||
|
||||
test.each([
|
||||
["missing target", undefined],
|
||||
["null", null],
|
||||
["removed", { version_added: "1", version_removed: "60" }],
|
||||
["qualified", { version_added: "1", flags: [{ type: "preference" }] }],
|
||||
["non-numeric", { version_added: "preview" }],
|
||||
])("treats %s BCD conservatively", (_name, statement) => {
|
||||
const api = { tabs: { test: compat({ firefox: statement }) } }
|
||||
expect(verify("browser.tabs.test", api)[0].messageId).toBe(
|
||||
"unsupportedApis",
|
||||
)
|
||||
})
|
||||
|
||||
test("accepts a supported unqualified statement from an array", () => {
|
||||
const api = {
|
||||
tabs: {
|
||||
test: compat({
|
||||
firefox: [
|
||||
{ version_added: false },
|
||||
{ version_added: "67" },
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
expect(verify("browser.tabs.test", api)).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("keeps platform suppressions independent", () => {
|
||||
const api = {
|
||||
tabs: {
|
||||
test: compat({
|
||||
firefox: { version_added: false },
|
||||
chrome: { version_added: false },
|
||||
}),
|
||||
},
|
||||
}
|
||||
const linter = new Linter()
|
||||
linter.defineRule("unsupported-firefox", unsupportedApis("firefox", api))
|
||||
linter.defineRule("unsupported-chrome", unsupportedApis("chrome", api))
|
||||
const messages = linter.verify(
|
||||
"// eslint-disable-next-line unsupported-firefox\nbrowser.tabs.test",
|
||||
{
|
||||
parserOptions: { ecmaVersion: 2020 },
|
||||
rules: {
|
||||
"unsupported-firefox": "error",
|
||||
"unsupported-chrome": "error",
|
||||
},
|
||||
},
|
||||
)
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages[0].ruleId).toBe("unsupported-chrome")
|
||||
})
|
||||
Loading…
Reference in a new issue