378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
/**
|
|
* @author Yosuke Ota
|
|
* See LICENSE file in root directory for full license.
|
|
*/
|
|
'use strict'
|
|
|
|
const semver = require('semver')
|
|
const utils = require('../utils')
|
|
|
|
/**
|
|
* @typedef {object} SyntaxRule
|
|
* @property {string} supported
|
|
* @property { (context: RuleContext) => TemplateListener } [createTemplateBodyVisitor]
|
|
* @property { (context: RuleContext) => RuleListener } [createScriptVisitor]
|
|
*/
|
|
|
|
const FEATURES = {
|
|
// Vue.js 2.5.0+
|
|
'slot-scope-attribute': require('./syntaxes/slot-scope-attribute'),
|
|
// Vue.js 2.6.0+
|
|
'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
|
|
'v-slot': require('./syntaxes/v-slot'),
|
|
// Vue.js 3.0.0+
|
|
'v-model-argument': require('./syntaxes/v-model-argument'),
|
|
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'),
|
|
'v-is': require('./syntaxes/v-is'),
|
|
'script-setup': require('./syntaxes/script-setup'),
|
|
'style-css-vars-injection': require('./syntaxes/style-css-vars-injection'),
|
|
// Vue.js 3.1.0+
|
|
'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix'),
|
|
// Vue.js 3.2.0+
|
|
'v-memo': require('./syntaxes/v-memo'),
|
|
'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
|
|
'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier')
|
|
}
|
|
|
|
const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
|
|
|
|
const cache = new Map()
|
|
/**
|
|
* Get the `semver.Range` object of a given range text.
|
|
* @param {string} x The text expression for a semver range.
|
|
* @returns {semver.Range} The range object of a given range text.
|
|
* It's null if the `x` is not a valid range text.
|
|
*/
|
|
function getSemverRange(x) {
|
|
const s = String(x)
|
|
let ret = cache.get(s) || null
|
|
|
|
if (!ret) {
|
|
try {
|
|
ret = new semver.Range(s)
|
|
} catch (_error) {
|
|
// Ignore parsing error.
|
|
}
|
|
cache.set(s, ret)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description:
|
|
'disallow unsupported Vue.js syntax on the specified version',
|
|
categories: undefined,
|
|
url: 'https://eslint.vuejs.org/rules/no-unsupported-features.html'
|
|
},
|
|
fixable: 'code',
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
version: {
|
|
type: 'string'
|
|
},
|
|
ignores: {
|
|
type: 'array',
|
|
items: {
|
|
enum: SYNTAX_NAMES
|
|
},
|
|
uniqueItems: true
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
],
|
|
messages: {
|
|
// Vue.js 2.5.0+
|
|
forbiddenSlotScopeAttribute:
|
|
'`slot-scope` are not supported except Vue.js ">=2.5.0 <3.0.0".',
|
|
// Vue.js 2.6.0+
|
|
forbiddenDynamicDirectiveArguments:
|
|
'Dynamic arguments are not supported until Vue.js "2.6.0".',
|
|
forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
|
|
// Vue.js 3.0.0+
|
|
forbiddenVModelArgument:
|
|
'Argument on `v-model` is not supported until Vue.js "3.0.0".',
|
|
forbiddenVModelCustomModifiers:
|
|
'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".',
|
|
forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".',
|
|
forbiddenScriptSetup:
|
|
'`<script setup>` are not supported until Vue.js "3.0.0".',
|
|
forbiddenStyleCssVarsInjection:
|
|
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
|
|
// Vue.js 3.1.0+
|
|
forbiddenIsAttributeWithVuePrefix:
|
|
'`is="vue:"` are not supported until Vue.js "3.1.0".',
|
|
// Vue.js 3.2.0+
|
|
forbiddenVMemo: '`v-memo` are not supported until Vue.js "3.2.0".',
|
|
forbiddenVBindPropModifierShorthand:
|
|
'`.prop` shorthand are not supported until Vue.js "3.2.0".',
|
|
forbiddenVBindAttrModifier:
|
|
'`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".'
|
|
}
|
|
},
|
|
/** @param {RuleContext} context */
|
|
create(context) {
|
|
const { version, ignores } = Object.assign(
|
|
{
|
|
version: null,
|
|
ignores: []
|
|
},
|
|
context.options[0] || {}
|
|
)
|
|
if (!version) {
|
|
// version is not set.
|
|
return {}
|
|
}
|
|
const versionRange = getSemverRange(version)
|
|
|
|
/**
|
|
* Check whether a given case object is full-supported on the configured node version.
|
|
* @param {SyntaxRule} aCase The case object to check.
|
|
* @returns {boolean} `true` if it's supporting.
|
|
*/
|
|
function isNotSupportingVersion(aCase) {
|
|
return !semverSubset(versionRange, getSemverRange(aCase.supported))
|
|
}
|
|
|
|
/** @type {TemplateListener} */
|
|
let templateBodyVisitor = {}
|
|
/** @type {RuleListener} */
|
|
let scriptVisitor = {}
|
|
|
|
for (const syntaxName of SYNTAX_NAMES) {
|
|
/** @type {SyntaxRule} */
|
|
const syntax = FEATURES[syntaxName]
|
|
if (ignores.includes(syntaxName) || !isNotSupportingVersion(syntax)) {
|
|
continue
|
|
}
|
|
if (syntax.createTemplateBodyVisitor) {
|
|
const visitor = syntax.createTemplateBodyVisitor(context)
|
|
templateBodyVisitor = utils.compositingVisitors(
|
|
templateBodyVisitor,
|
|
visitor
|
|
)
|
|
}
|
|
if (syntax.createScriptVisitor) {
|
|
const visitor = syntax.createScriptVisitor(context)
|
|
scriptVisitor = utils.compositingVisitors(scriptVisitor, visitor)
|
|
}
|
|
}
|
|
|
|
return utils.defineTemplateBodyVisitor(
|
|
context,
|
|
templateBodyVisitor,
|
|
scriptVisitor
|
|
)
|
|
}
|
|
}
|
|
|
|
// TODO replace semver.subset() in the major version.
|
|
/**
|
|
* semver.subset()
|
|
*
|
|
* We need to use a copy of the semver source code until a major version upgrade.
|
|
*
|
|
* @see https://github.com/npm/node-semver/blob/e79ac3a450e8bb504e78b8159e3efc70895699b8/ranges/subset.js#L43
|
|
* @license ISC at Isaac Z. Schlueter and Contributors
|
|
* https://github.com/npm/node-semver/blob/master/LICENSE
|
|
*
|
|
* @param {semver.Range} sub
|
|
* @param {semver.Range} dom
|
|
*/
|
|
function semverSubset(sub, dom) {
|
|
if (sub === dom) return true
|
|
|
|
sub = new semver.Range(sub)
|
|
dom = new semver.Range(dom)
|
|
let sawNonNull = false
|
|
|
|
// eslint-disable-next-line no-labels
|
|
OUTER: for (const simpleSub of sub.set) {
|
|
for (const simpleDom of dom.set) {
|
|
const isSub = simpleSubset(simpleSub, simpleDom)
|
|
sawNonNull = sawNonNull || isSub !== null
|
|
// eslint-disable-next-line no-labels
|
|
if (isSub) continue OUTER
|
|
}
|
|
if (sawNonNull) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* @license ISC at Isaac Z. Schlueter and Contributors
|
|
* https://github.com/npm/node-semver/blob/master/LICENSE
|
|
* @param {readonly semver.Comparator[]} sub
|
|
* @param {readonly semver.Comparator[]} dom
|
|
*/
|
|
function simpleSubset(sub, dom) {
|
|
if (sub === dom) return true
|
|
|
|
/**
|
|
* @param {semver.Comparator} c
|
|
*/
|
|
function isAny(c) {
|
|
return Object.keys(c.semver).length === 0
|
|
}
|
|
|
|
if (sub.length === 1 && isAny(sub[0])) {
|
|
if (dom.length === 1 && isAny(dom[0])) return true
|
|
else sub = [new semver.Comparator('>=0.0.0')]
|
|
}
|
|
|
|
if (dom.length === 1 && isAny(dom[0])) {
|
|
dom = [new semver.Comparator('>=0.0.0')]
|
|
}
|
|
|
|
const eqSet = new Set()
|
|
let gt, lt
|
|
for (const c of sub) {
|
|
if (c.operator === '>' || c.operator === '>=') gt = higherGT(gt, c)
|
|
else if (c.operator === '<' || c.operator === '<=') lt = lowerLT(lt, c)
|
|
else eqSet.add(c.semver)
|
|
}
|
|
|
|
if (eqSet.size > 1) return null
|
|
|
|
let gtltComp
|
|
if (gt && lt) {
|
|
gtltComp = semver.compare(gt.semver, lt.semver)
|
|
if (gtltComp > 0) return null
|
|
else if (gtltComp === 0 && (gt.operator !== '>=' || lt.operator !== '<='))
|
|
return null
|
|
}
|
|
|
|
// will iterate one or zero times
|
|
for (const eq of eqSet) {
|
|
if (gt && !semver.satisfies(eq, String(gt))) return null
|
|
|
|
if (lt && !semver.satisfies(eq, String(lt))) return null
|
|
|
|
for (const c of dom) {
|
|
if (!semver.satisfies(eq, String(c))) return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
let higher, lower
|
|
let hasDomLT, hasDomGT
|
|
// if the subset has a prerelease, we need a comparator in the superset
|
|
// with the same tuple and a prerelease, or it's not a subset
|
|
let needDomLTPre = lt && lt.semver.prerelease.length ? lt.semver : false
|
|
let needDomGTPre = gt && gt.semver.prerelease.length ? gt.semver : false
|
|
// exception: <1.2.3-0 is the same as <1.2.3
|
|
if (
|
|
needDomLTPre &&
|
|
needDomLTPre.prerelease.length === 1 &&
|
|
lt &&
|
|
lt.operator === '<' &&
|
|
needDomLTPre.prerelease[0] === 0
|
|
) {
|
|
needDomLTPre = false
|
|
}
|
|
|
|
for (const c of dom) {
|
|
hasDomGT = hasDomGT || c.operator === '>' || c.operator === '>='
|
|
hasDomLT = hasDomLT || c.operator === '<' || c.operator === '<='
|
|
if (gt) {
|
|
if (needDomGTPre) {
|
|
if (
|
|
c.semver.prerelease &&
|
|
c.semver.prerelease.length &&
|
|
c.semver.major === needDomGTPre.major &&
|
|
c.semver.minor === needDomGTPre.minor &&
|
|
c.semver.patch === needDomGTPre.patch
|
|
) {
|
|
needDomGTPre = false
|
|
}
|
|
}
|
|
if (c.operator === '>' || c.operator === '>=') {
|
|
higher = higherGT(gt, c)
|
|
if (higher === c && higher !== gt) return false
|
|
} else if (
|
|
gt.operator === '>=' &&
|
|
!semver.satisfies(gt.semver, String(c))
|
|
)
|
|
return false
|
|
}
|
|
if (lt) {
|
|
if (needDomLTPre) {
|
|
if (
|
|
c.semver.prerelease &&
|
|
c.semver.prerelease.length &&
|
|
c.semver.major === needDomLTPre.major &&
|
|
c.semver.minor === needDomLTPre.minor &&
|
|
c.semver.patch === needDomLTPre.patch
|
|
) {
|
|
needDomLTPre = false
|
|
}
|
|
}
|
|
if (c.operator === '<' || c.operator === '<=') {
|
|
lower = lowerLT(lt, c)
|
|
if (lower === c && lower !== lt) return false
|
|
} else if (
|
|
lt.operator === '<=' &&
|
|
!semver.satisfies(lt.semver, String(c))
|
|
)
|
|
return false
|
|
}
|
|
if (!c.operator && (lt || gt) && gtltComp !== 0) return false
|
|
}
|
|
|
|
// if there was a < or >, and nothing in the dom, then must be false
|
|
// UNLESS it was limited by another range in the other direction.
|
|
// Eg, >1.0.0 <1.0.1 is still a subset of <2.0.0
|
|
if (gt && hasDomLT && !lt && gtltComp !== 0) return false
|
|
|
|
if (lt && hasDomGT && !gt && gtltComp !== 0) return false
|
|
|
|
// we needed a prerelease range in a specific tuple, but didn't get one
|
|
// then this isn't a subset. eg >=1.2.3-pre is not a subset of >=1.0.0,
|
|
// because it includes prereleases in the 1.2.3 tuple
|
|
if (needDomGTPre || needDomLTPre) return false
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* @license ISC at Isaac Z. Schlueter and Contributors
|
|
* https://github.com/npm/node-semver/blob/master/LICENSE
|
|
* @param {semver.Comparator | void} a
|
|
* @param {semver.Comparator} b
|
|
*/
|
|
const higherGT = (a, b) => {
|
|
if (!a) return b
|
|
const comp = semver.compare(a.semver, b.semver)
|
|
return comp > 0
|
|
? a
|
|
: comp < 0
|
|
? b
|
|
: b.operator === '>' && a.operator === '>='
|
|
? b
|
|
: a
|
|
}
|
|
|
|
/**
|
|
* @license ISC at Isaac Z. Schlueter and Contributors
|
|
* https://github.com/npm/node-semver/blob/master/LICENSE
|
|
* @param {semver.Comparator | void} a
|
|
* @param {semver.Comparator} b
|
|
*/
|
|
const lowerLT = (a, b) => {
|
|
if (!a) return b
|
|
const comp = semver.compare(a.semver, b.semver)
|
|
return comp < 0
|
|
? a
|
|
: comp > 0
|
|
? b
|
|
: b.operator === '<' && a.operator === '<='
|
|
? b
|
|
: a
|
|
}
|