Skip to content

Commit

Permalink
Add support for defineModel
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jan 10, 2024
1 parent 26fc85e commit 783fcd6
Show file tree
Hide file tree
Showing 17 changed files with 465 additions and 30 deletions.
13 changes: 8 additions & 5 deletions docs/rules/define-macros-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
}
```

- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`.
- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"`, `"defineSlots"`, and `"defineModel"`.
- `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end.

### `{ "order": ["defineProps", "defineEmits"] }` (default)
Expand Down Expand Up @@ -69,14 +69,15 @@ defineEmits(/* ... */)

</eslint-code-block>

### `{ "order": ["defineOptions", "defineProps", "defineEmits", "defineSlots"] }`
### `{ "order": ["defineOptions", "defineModel", "defineProps", "defineEmits", "defineSlots"] }`

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots']}]}">
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots']}]}">

```vue
<!-- ✓ GOOD -->
<script setup>
defineOptions({/* ... */})
const model = defineModel()
defineProps(/* ... */)
defineEmits(/* ... */)
const slots = defineSlots()
Expand All @@ -85,7 +86,7 @@ const slots = defineSlots()

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots']}]}">
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots']}]}">

```vue
<!-- ✗ BAD -->
Expand All @@ -94,18 +95,20 @@ defineEmits(/* ... */)
const slots = defineSlots()
defineProps(/* ... */)
defineOptions({/* ... */})
const model = defineModel()
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots']}]}">
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots']}]}">

```vue
<!-- ✗ BAD -->
<script setup>
const bar = ref()
defineOptions({/* ... */})
const model = defineModel()
defineProps(/* ... */)
defineEmits(/* ... */)
const slots = defineSlots()
Expand Down
1 change: 1 addition & 0 deletions docs/rules/no-unsupported-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This rule reports unsupported Vue.js syntax on the specified version.
- `ignores` ... You can use this `ignores` option to ignore the given features.
The `"ignores"` option accepts an array of the following strings.
- Vue.js 3.4.0+
- `"define-model"` ... `defineModel()` macro.
- `"v-bind-same-name-shorthand"` ... `v-bind` same-name shorthand.
- Vue.js 3.3.0+
- `"define-slots"` ... `defineSlots()` macro.
Expand Down
12 changes: 11 additions & 1 deletion lib/rules/define-macros-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ const MACROS_EMITS = 'defineEmits'
const MACROS_PROPS = 'defineProps'
const MACROS_OPTIONS = 'defineOptions'
const MACROS_SLOTS = 'defineSlots'
const ORDER_SCHEMA = [MACROS_EMITS, MACROS_PROPS, MACROS_OPTIONS, MACROS_SLOTS]
const MACROS_MODEL = 'defineModel'
const ORDER_SCHEMA = [
MACROS_EMITS,
MACROS_PROPS,
MACROS_OPTIONS,
MACROS_SLOTS,
MACROS_MODEL
]
const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]

/**
Expand Down Expand Up @@ -116,6 +123,9 @@ function create(context) {
onDefineSlotsExit(node) {
macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
},
onDefineModelExit(node) {
macrosNodes.set(MACROS_MODEL, getDefineMacrosStatement(node))
},
onDefineExposeExit(node) {
defineExposeNode = getDefineMacrosStatement(node)
}
Expand Down
20 changes: 20 additions & 0 deletions lib/rules/no-undef-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ module.exports = {
const propertyReferences =
propertyReferenceExtractor.extractFromPattern(pattern)
ctx.verifyReferences(propertyReferences)
},
onDefineModelEnter(node, model) {
const ctx = getVueComponentContext(programNode)

ctx.defineProperties.set(model.name.modelName, {
isProps: true
})

if (
!node.parent ||
node.parent.type !== 'VariableDeclarator' ||
node.parent.init !== node
) {
return
}

const pattern = node.parent.id
const propertyReferences =
propertyReferenceExtractor.extractFromPattern(pattern)
ctx.verifyReferences(propertyReferences)
}
}),
utils.defineVueVisitor(context, {
Expand Down
3 changes: 3 additions & 0 deletions lib/rules/no-unsupported-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const FEATURES = {
'define-options': require('./syntaxes/define-options'),
'define-slots': require('./syntaxes/define-slots'),
// Vue.js 3.4.0+
'define-model': require('./syntaxes/define-model'),
'v-bind-same-name-shorthand': require('./syntaxes/v-bind-same-name-shorthand')
}

Expand Down Expand Up @@ -128,6 +129,8 @@ module.exports = {
forbiddenDefineSlots:
'`defineSlots()` macros are not supported until Vue.js "3.3.0".',
// Vue.js 3.4.0+
forbiddenDefineModel:
'`defineModel()` macros are not supported until Vue.js "3.4.0".',
forbiddenVBindSameNameShorthand:
'`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".'
}
Expand Down
11 changes: 11 additions & 0 deletions lib/rules/no-unused-emit-declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,17 @@ module.exports = {
emitReferenceIds
})
},
onDefineModelEnter(node, model) {
if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.init === node
) {
// If store the return of defineModel() in a variable, we can mark the 'update:modelName' event as used if use that variable.
// If that variable is unused it will already be reported by `no-unused-var` rule.
emitCalls.set(`update:${model.name.modelName}`, node)
}
},
...callVisitor
}),
{
Expand Down
35 changes: 33 additions & 2 deletions lib/rules/no-unused-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const {
* @typedef {object} ComponentNonObjectPropertyData
* @property {string} name
* @property {GroupName} groupName
* @property {'array' | 'type' | 'infer-type'} type
* @property {'array' | 'type' | 'infer-type' | 'model' } type
* @property {ASTNode} node
*
* @typedef { ComponentNonObjectPropertyData | ComponentObjectPropertyData } ComponentPropertyData
Expand Down Expand Up @@ -406,7 +406,9 @@ module.exports = {
if (!groups.has('props')) {
return
}
const container = getVueComponentPropertiesContainer(node)
const container = getVueComponentPropertiesContainer(
context.getSourceCode().ast
)

for (const prop of props) {
if (!prop.propName) {
Expand Down Expand Up @@ -452,6 +454,35 @@ module.exports = {
const propertyReferences =
propertyReferenceExtractor.extractFromPattern(pattern)
container.propertyReferencesForProps.push(propertyReferences)
},
onDefineModelEnter(node, model) {
if (!groups.has('props')) {
return
}
const container = getVueComponentPropertiesContainer(
context.getSourceCode().ast
)
if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.init === node
) {
// If store the return of defineModel() in a variable, we can mark the model prop as used if use that variable.
// If that variable is unused it will already be reported by `no-unused-var` rule.
container.propertyReferences.push(
propertyReferenceExtractor.extractFromName(
model.name.modelName,
model.name.node || node
)
)
return
}
container.properties.push({
type: 'model',
name: model.name.modelName,
groupName: 'props',
node: model.name.node || node
})
}
}),
utils.defineVueVisitor(context, {
Expand Down
57 changes: 38 additions & 19 deletions lib/rules/require-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
/**
* @param {Expression} node
* @returns {boolean|null}
*/
function optionHasType(node) {
switch (node.type) {
case 'ObjectExpression': {
// foo: {
return objectHasType(node)
}
case 'ArrayExpression': {
// foo: [
return node.elements.length > 0
}
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
return false
}
}

// Unknown
return null
}
/**
* @param {ObjectExpression} node
* @returns {boolean}
Expand All @@ -44,37 +67,20 @@ module.exports = {
)
return Boolean(typeProperty || validatorProperty)
}

/**
* @param {ComponentProp} prop
*/
function checkProperty(prop) {
if (prop.type !== 'object' && prop.type !== 'array') {
return
}
let hasType = true
let hasType

if (prop.type === 'array') {
hasType = false
} else {
const { value } = prop
switch (value.type) {
case 'ObjectExpression': {
// foo: {
hasType = objectHasType(value)
break
}
case 'ArrayExpression': {
// foo: [
hasType = value.elements.length > 0
break
}
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
hasType = false
break
}
}
hasType = optionHasType(value) ?? true
}

if (!hasType) {
Expand All @@ -99,6 +105,19 @@ module.exports = {
for (const prop of props) {
checkProperty(prop)
}
},
onDefineModelEnter(node, model) {
if (model.typeNode) return
if (model.options && (optionHasType(model.options) ?? true)) {
return
}
context.report({
node: model.options || node,
messageId: 'requireType',
data: {
name: model.name.modelName
}
})
}
}),
utils.executeOnVue(context, (obj) => {
Expand Down
22 changes: 22 additions & 0 deletions lib/rules/syntaxes/define-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../../utils/index')

module.exports = {
supported: '>=3.4.0',
/** @param {RuleContext} context @returns {RuleListener} */
createScriptVisitor(context) {
return utils.defineScriptSetupVisitor(context, {
onDefineModelEnter(node) {
context.report({
node,
messageId: 'forbiddenDefineModel'
})
}
})
}
}
Loading

0 comments on commit 783fcd6

Please sign in to comment.