123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- /**
- * @fileoverview Require default value for props
- * @author Michał Sajnóg <msajnog93@gmail.com> (https://github.com/michalsnik)
- */
- 'use strict'
- /**
- * @typedef {import('../utils').ComponentProp} ComponentProp
- * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
- * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
- * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject
- */
- const utils = require('../utils')
- const { isDef } = require('../utils')
- const NATIVE_TYPES = new Set([
- 'String',
- 'Number',
- 'Boolean',
- 'Function',
- 'Object',
- 'Array',
- 'Symbol'
- ])
- /**
- * Detects whether given value node is a Boolean type
- * @param {Expression} value
- * @return {boolean}
- */
- function isValueNodeOfBooleanType(value) {
- if (value.type === 'Identifier' && value.name === 'Boolean') {
- return true
- }
- if (value.type === 'ArrayExpression') {
- const elements = value.elements.filter(isDef)
- return (
- elements.length === 1 &&
- elements[0].type === 'Identifier' &&
- elements[0].name === 'Boolean'
- )
- }
- return false
- }
- module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description: 'require default value for props',
- categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
- url: 'https://eslint.vuejs.org/rules/require-default-prop.html'
- },
- fixable: null,
- schema: [],
- messages: {
- missingDefault: `Prop '{{propName}}' requires default value to be set.`
- }
- },
- /** @param {RuleContext} context */
- create(context) {
- /**
- * Checks if the passed prop is required
- * @param {ObjectExpression} propValue - ObjectExpression AST node for a single prop
- * @return {boolean}
- */
- function propIsRequired(propValue) {
- const propRequiredNode = propValue.properties.find(
- (p) =>
- p.type === 'Property' &&
- utils.getStaticPropertyName(p) === 'required' &&
- p.value.type === 'Literal' &&
- p.value.value === true
- )
- return Boolean(propRequiredNode)
- }
- /**
- * Checks if the passed prop has a default value
- * @param {ObjectExpression} propValue - ObjectExpression AST node for a single prop
- * @return {boolean}
- */
- function propHasDefault(propValue) {
- const propDefaultNode = propValue.properties.find(
- (p) =>
- p.type === 'Property' && utils.getStaticPropertyName(p) === 'default'
- )
- return Boolean(propDefaultNode)
- }
- /**
- * Checks whether the given props that don't have a default value
- * @param {ComponentObjectProp} prop Vue component's "props" node
- * @return {boolean}
- */
- function isWithoutDefaultValue(prop) {
- if (prop.value.type !== 'ObjectExpression') {
- if (prop.value.type === 'Identifier') {
- return NATIVE_TYPES.has(prop.value.name)
- }
- if (
- prop.value.type === 'CallExpression' ||
- prop.value.type === 'MemberExpression'
- ) {
- // OK
- return false
- }
- // NG
- return true
- }
- return !propIsRequired(prop.value) && !propHasDefault(prop.value)
- }
- /**
- * Detects whether given prop node is a Boolean
- * @param {ComponentObjectProp} prop
- * @return {Boolean}
- */
- function isBooleanProp(prop) {
- const value = utils.skipTSAsExpression(prop.value)
- return (
- isValueNodeOfBooleanType(value) ||
- (value.type === 'ObjectExpression' &&
- value.properties.some(
- (p) =>
- p.type === 'Property' &&
- p.key.type === 'Identifier' &&
- p.key.name === 'type' &&
- isValueNodeOfBooleanType(p.value)
- ))
- )
- }
- /**
- * @param {ComponentProp[]} props
- * @param {(prop: ComponentObjectProp|ComponentTypeProp)=>boolean} [ignore]
- */
- function processProps(props, ignore) {
- for (const prop of props) {
- if (prop.type === 'object') {
- if (prop.node.shorthand) {
- continue
- }
- if (!isWithoutDefaultValue(prop)) {
- continue
- }
- if (isBooleanProp(prop)) {
- continue
- }
- if (ignore?.(prop)) {
- continue
- }
- const propName =
- prop.propName == null
- ? `[${context.getSourceCode().getText(prop.node.key)}]`
- : prop.propName
- context.report({
- node: prop.node,
- messageId: `missingDefault`,
- data: {
- propName
- }
- })
- } else if (prop.type === 'type') {
- if (prop.required) {
- continue
- }
- if (prop.types.length === 1 && prop.types[0] === 'Boolean') {
- continue
- }
- if (ignore?.(prop)) {
- continue
- }
- context.report({
- node: prop.node,
- messageId: `missingDefault`,
- data: {
- propName: prop.propName
- }
- })
- }
- }
- }
- return utils.compositingVisitors(
- utils.defineScriptSetupVisitor(context, {
- onDefinePropsEnter(node, props) {
- const hasWithDefaults = utils.hasWithDefaults(node)
- const defaultsByWithDefaults =
- utils.getWithDefaultsPropExpressions(node)
- const isUsingPropsDestructure = utils.isUsingPropsDestructure(node)
- const defaultsByAssignmentPatterns =
- utils.getDefaultPropExpressionsForPropsDestructure(node)
- processProps(props, (prop) => {
- if (prop.type === 'type') {
- if (!hasWithDefaults) {
- // If don't use withDefaults(), exclude it from the report.
- return true
- }
- if (defaultsByWithDefaults[prop.propName]) {
- return true
- }
- }
- if (!isUsingPropsDestructure) {
- return false
- }
- if (prop.propName == null) {
- // If using Props Destructure but the property name cannot be determined,
- // it will be ignored.
- return true
- }
- return Boolean(defaultsByAssignmentPatterns[prop.propName])
- })
- }
- }),
- utils.executeOnVue(context, (obj) => {
- processProps(utils.getComponentPropsFromOptions(obj))
- })
- )
- }
- }
|