365 lines
15 KiB
JavaScript
365 lines
15 KiB
JavaScript
const require_runtime = require('../_virtual/_rolldown/runtime.js');
|
|
const require_index = require('../utils/index.js');
|
|
const require_index$1 = require('../utils/style-variables/index.js');
|
|
const require_property_references = require('../utils/property-references.js');
|
|
const require_comments = require('../utils/comments.js');
|
|
let _eslint_community_eslint_utils = require("@eslint-community/eslint-utils");
|
|
_eslint_community_eslint_utils = require_runtime.__toESM(_eslint_community_eslint_utils);
|
|
|
|
//#region lib/rules/no-unused-properties.ts
|
|
var import_utils = /* @__PURE__ */ require_runtime.__toESM(require_index.default);
|
|
const GROUP_PROPERTY = "props";
|
|
const GROUP_DATA = "data";
|
|
const GROUP_ASYNC_DATA = "asyncData";
|
|
const GROUP_COMPUTED_PROPERTY = "computed";
|
|
const GROUP_METHODS = "methods";
|
|
const GROUP_SETUP = "setup";
|
|
const GROUP_WATCHER = "watch";
|
|
const GROUP_EXPOSE = "expose";
|
|
const GROUP_INJECT = "inject";
|
|
const UNREFERENCED_UNKNOWN_MEMBER = "unknownMemberAsUnreferenced";
|
|
const UNREFERENCED_RETURN = "returnAsUnreferenced";
|
|
const PROPERTY_LABEL = {
|
|
props: "property",
|
|
data: "data",
|
|
asyncData: "async data",
|
|
computed: "computed property",
|
|
methods: "method",
|
|
setup: "property returned from `setup()`",
|
|
inject: "inject",
|
|
watch: "watch",
|
|
provide: "provide",
|
|
expose: "expose"
|
|
};
|
|
function findExpression(context, id) {
|
|
const variable = import_utils.default.findVariableByIdentifier(context, id);
|
|
if (!variable) return id;
|
|
if (variable.defs.length === 1) {
|
|
const def = variable.defs[0];
|
|
if (def.type === "Variable" && def.parent.kind === "const" && def.node.init) {
|
|
if (def.node.init.type === "Identifier") return findExpression(context, def.node.init);
|
|
return def.node.init;
|
|
}
|
|
}
|
|
return id;
|
|
}
|
|
/**
|
|
* Check if the given component property is marked as `@public` in JSDoc comments.
|
|
*/
|
|
function isPublicMember(property, sourceCode) {
|
|
if (property.type === "object" && property.groupName !== "props") return isPublicProperty(property.property, sourceCode);
|
|
return false;
|
|
}
|
|
/**
|
|
* Check if the given property node is marked as `@public` in JSDoc comments.
|
|
*/
|
|
function isPublicProperty(node, sourceCode) {
|
|
const jsdoc = getJSDocFromProperty(node, sourceCode);
|
|
if (jsdoc) return /(?:^|\s|\*)@public\b/u.test(jsdoc.value);
|
|
return false;
|
|
}
|
|
/**
|
|
* Get the JSDoc comment for a given property node.
|
|
*/
|
|
function getJSDocFromProperty(node, sourceCode) {
|
|
const jsdoc = findJSDocComment(node, sourceCode);
|
|
if (jsdoc) return jsdoc;
|
|
if (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression") return findJSDocComment(node.value, sourceCode);
|
|
return null;
|
|
}
|
|
/**
|
|
* Finds a JSDoc comment for the given node.
|
|
*/
|
|
function findJSDocComment(node, sourceCode) {
|
|
let currentNode = node;
|
|
let tokenBefore = null;
|
|
while (currentNode) {
|
|
tokenBefore = sourceCode.getTokenBefore(currentNode, { includeComments: true });
|
|
if (!tokenBefore || !_eslint_community_eslint_utils.default.isCommentToken(tokenBefore)) return null;
|
|
if (tokenBefore.type === "Line") {
|
|
currentNode = tokenBefore;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (tokenBefore && require_comments.isJSDocComment(tokenBefore)) return tokenBefore;
|
|
return null;
|
|
}
|
|
var no_unused_properties_default = {
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description: "disallow unused properties",
|
|
categories: void 0,
|
|
url: "https://eslint.vuejs.org/rules/no-unused-properties.html"
|
|
},
|
|
fixable: null,
|
|
schema: [{
|
|
type: "object",
|
|
properties: {
|
|
groups: {
|
|
type: "array",
|
|
items: { enum: [
|
|
GROUP_PROPERTY,
|
|
GROUP_DATA,
|
|
GROUP_ASYNC_DATA,
|
|
GROUP_COMPUTED_PROPERTY,
|
|
GROUP_METHODS,
|
|
GROUP_SETUP,
|
|
GROUP_INJECT
|
|
] },
|
|
additionalItems: false,
|
|
uniqueItems: true
|
|
},
|
|
deepData: { type: "boolean" },
|
|
ignorePublicMembers: { type: "boolean" },
|
|
unreferencedOptions: {
|
|
type: "array",
|
|
items: { enum: [UNREFERENCED_UNKNOWN_MEMBER, UNREFERENCED_RETURN] },
|
|
additionalItems: false,
|
|
uniqueItems: true
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}],
|
|
messages: { unused: "'{{name}}' of {{group}} found, but never used." }
|
|
},
|
|
create(context) {
|
|
const options = context.options[0] || {};
|
|
const groups = new Set(options.groups || [GROUP_PROPERTY]);
|
|
const deepData = Boolean(options.deepData);
|
|
const ignorePublicMembers = Boolean(options.ignorePublicMembers);
|
|
const unreferencedOptions = new Set(options.unreferencedOptions || []);
|
|
let propsReferencePattern = null;
|
|
const propertyReferenceExtractor = require_property_references.definePropertyReferenceExtractor(context, {
|
|
unknownMemberAsUnreferenced: unreferencedOptions.has(UNREFERENCED_UNKNOWN_MEMBER),
|
|
returnAsUnreferenced: unreferencedOptions.has(UNREFERENCED_RETURN)
|
|
});
|
|
const templatePropertiesContainer = {
|
|
propertyReferences: [],
|
|
refNames: /* @__PURE__ */ new Set()
|
|
};
|
|
const vueComponentPropertiesContainerMap = /* @__PURE__ */ new Map();
|
|
function getVueComponentPropertiesContainer(node) {
|
|
let container = vueComponentPropertiesContainerMap.get(node);
|
|
if (!container) {
|
|
container = {
|
|
properties: [],
|
|
propertyReferences: [],
|
|
propertyReferencesForProps: []
|
|
};
|
|
vueComponentPropertiesContainerMap.set(node, container);
|
|
}
|
|
return container;
|
|
}
|
|
function verifyDataOptionDeepProperties(segments, propertyValue, propertyReferences) {
|
|
let targetExpr = propertyValue;
|
|
if (targetExpr.type === "Identifier") targetExpr = findExpression(context, targetExpr);
|
|
if (targetExpr.type === "ObjectExpression") for (const prop of targetExpr.properties) {
|
|
if (prop.type !== "Property") continue;
|
|
const name = import_utils.default.getStaticPropertyName(prop);
|
|
if (name == null) continue;
|
|
if (!propertyReferences.hasProperty(name, { unknownCallAsAny: true })) {
|
|
context.report({
|
|
node: prop.key,
|
|
messageId: "unused",
|
|
data: {
|
|
group: PROPERTY_LABEL.data,
|
|
name: [...segments, name].join(".")
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
verifyDataOptionDeepProperties([...segments, name], prop.value, propertyReferences.getNest(name));
|
|
}
|
|
}
|
|
/**
|
|
* Report all unused properties.
|
|
*/
|
|
function reportUnusedProperties() {
|
|
for (const container of vueComponentPropertiesContainerMap.values()) {
|
|
const propertyReferences = require_property_references.mergePropertyReferences([...container.propertyReferences, ...templatePropertiesContainer.propertyReferences]);
|
|
const propertyReferencesForProps = require_property_references.mergePropertyReferences(container.propertyReferencesForProps);
|
|
for (const property of container.properties) {
|
|
if (property.groupName === "props" && propertyReferencesForProps.hasProperty(property.name) || propertyReferences.hasProperty("$props")) continue;
|
|
if (property.groupName === "setup" && templatePropertiesContainer.refNames.has(property.name)) continue;
|
|
if (ignorePublicMembers && isPublicMember(property, context.sourceCode)) continue;
|
|
if (propertyReferences.hasProperty(property.name)) {
|
|
if (deepData && (property.groupName === "data" || property.groupName === "asyncData") && property.type === "object") verifyDataOptionDeepProperties([property.name], property.property.value, propertyReferences.getNest(property.name));
|
|
continue;
|
|
}
|
|
context.report({
|
|
node: property.node,
|
|
messageId: "unused",
|
|
data: {
|
|
group: PROPERTY_LABEL[property.groupName],
|
|
name: property.name
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function getParentProperty(node) {
|
|
if (!node.parent || node.parent.type !== "Property" || node.parent.value !== node) return null;
|
|
const property = node.parent;
|
|
if (!import_utils.default.isProperty(property)) return null;
|
|
return property;
|
|
}
|
|
const scriptVisitor = import_utils.default.compositingVisitors(import_utils.default.defineScriptSetupVisitor(context, {
|
|
onDefinePropsEnter(node, props) {
|
|
if (!groups.has("props")) return;
|
|
const container = getVueComponentPropertiesContainer(context.sourceCode.ast);
|
|
for (const prop of props) {
|
|
if (!prop.propName) continue;
|
|
if (prop.type === "object") container.properties.push({
|
|
type: prop.type,
|
|
name: prop.propName,
|
|
groupName: "props",
|
|
node: prop.key,
|
|
property: prop.node
|
|
});
|
|
else container.properties.push({
|
|
type: prop.type,
|
|
name: prop.propName,
|
|
groupName: "props",
|
|
node: prop.type === "infer-type" ? prop.node : prop.key
|
|
});
|
|
}
|
|
let target = node;
|
|
if (target.parent && target.parent.type === "CallExpression" && target.parent.arguments[0] === target && target.parent.callee.type === "Identifier" && target.parent.callee.name === "withDefaults") target = target.parent;
|
|
if (!target.parent || target.parent.type !== "VariableDeclarator" || target.parent.init !== target) return;
|
|
propsReferencePattern = target.parent.id;
|
|
const propertyReferences = propertyReferenceExtractor.extractFromPattern(propsReferencePattern);
|
|
container.propertyReferencesForProps.push(propertyReferences);
|
|
},
|
|
onDefineModelEnter(node, model) {
|
|
if (!groups.has("props")) return;
|
|
const container = getVueComponentPropertiesContainer(context.sourceCode.ast);
|
|
if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.init === node) {
|
|
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
|
|
});
|
|
}
|
|
}), import_utils.default.defineVueVisitor(context, {
|
|
CallExpression(node, vueData) {
|
|
if (node.callee.type !== "Identifier") return;
|
|
let groupName = null;
|
|
if (/^mapMutations|mapActions$/u.test(node.callee.name)) groupName = "methods";
|
|
else if (/^mapState|mapGetters|mapWritableState$/u.test(node.callee.name)) groupName = "computed";
|
|
if (!groupName || node.arguments.length === 0) return;
|
|
const arg = node.arguments.length === 2 ? node.arguments[1] : node.arguments[0];
|
|
if (arg.type === "ObjectExpression") {
|
|
const container = getVueComponentPropertiesContainer(vueData.node);
|
|
for (const prop of arg.properties) {
|
|
const name = prop.type === "SpreadElement" ? null : import_utils.default.getStaticPropertyName(prop);
|
|
if (name) container.properties.push({
|
|
type: "array",
|
|
name,
|
|
groupName,
|
|
node: prop
|
|
});
|
|
}
|
|
} else if (arg.type === "ArrayExpression") {
|
|
const container = getVueComponentPropertiesContainer(vueData.node);
|
|
for (const element of arg.elements) {
|
|
if (!element || element.type !== "Literal" && element.type !== "TemplateLiteral") continue;
|
|
const name = import_utils.default.getStringLiteralValue(element);
|
|
if (name) container.properties.push({
|
|
type: "array",
|
|
name,
|
|
groupName,
|
|
node: element
|
|
});
|
|
}
|
|
}
|
|
},
|
|
onVueObjectEnter(node, vueNode) {
|
|
const container = getVueComponentPropertiesContainer(vueNode.node);
|
|
for (const watcherOrExpose of import_utils.default.iterateProperties(node, new Set([GROUP_WATCHER, GROUP_EXPOSE]))) if (watcherOrExpose.groupName === GROUP_WATCHER) {
|
|
const watcher = watcherOrExpose;
|
|
container.propertyReferences.push(propertyReferenceExtractor.extractFromPath(watcher.name, watcher.node));
|
|
if (watcher.type === "object") {
|
|
const property = watcher.property;
|
|
if (property.kind === "init") for (const handlerValueNode of import_utils.default.iterateWatchHandlerValues(property)) container.propertyReferences.push(propertyReferenceExtractor.extractFromNameLiteral(handlerValueNode));
|
|
}
|
|
} else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
|
|
const expose = watcherOrExpose;
|
|
container.propertyReferences.push(propertyReferenceExtractor.extractFromName(expose.name, expose.node));
|
|
}
|
|
container.properties.push(...import_utils.default.iterateProperties(node, groups));
|
|
},
|
|
"ObjectExpression > Property > :function[params.length>0]"(node, vueData) {
|
|
const property = getParentProperty(node);
|
|
if (!property) return;
|
|
if (property.parent === vueData.node) {
|
|
if (import_utils.default.getStaticPropertyName(property) !== "data") return;
|
|
} else {
|
|
const parentProperty = getParentProperty(property.parent);
|
|
if (!parentProperty) return;
|
|
if (parentProperty.parent === vueData.node) {
|
|
if (import_utils.default.getStaticPropertyName(parentProperty) !== "computed") return;
|
|
} else {
|
|
const parentParentProperty = getParentProperty(parentProperty.parent);
|
|
if (!parentParentProperty) return;
|
|
if (parentParentProperty.parent === vueData.node) {
|
|
if (import_utils.default.getStaticPropertyName(parentParentProperty) !== "computed" || import_utils.default.getStaticPropertyName(property) !== "get") return;
|
|
} else return;
|
|
}
|
|
}
|
|
const propertyReferences = propertyReferenceExtractor.extractFromFunctionParam(node, 0);
|
|
getVueComponentPropertiesContainer(vueData.node).propertyReferences.push(propertyReferences);
|
|
},
|
|
onSetupFunctionEnter(node, vueData) {
|
|
const container = getVueComponentPropertiesContainer(vueData.node);
|
|
const propertyReferences = propertyReferenceExtractor.extractFromFunctionParam(node, 0);
|
|
container.propertyReferencesForProps.push(propertyReferences);
|
|
},
|
|
onRenderFunctionEnter(node, vueData) {
|
|
const container = getVueComponentPropertiesContainer(vueData.node);
|
|
const propertyReferences = propertyReferenceExtractor.extractFromFunctionParam(node, 0);
|
|
container.propertyReferencesForProps.push(propertyReferences);
|
|
if (vueData.functional) {
|
|
const propertyReferencesForV2 = propertyReferenceExtractor.extractFromFunctionParam(node, 1);
|
|
container.propertyReferencesForProps.push(propertyReferencesForV2.getNest("props"));
|
|
}
|
|
},
|
|
"ThisExpression, Identifier"(node, vueData) {
|
|
if (!import_utils.default.isThis(node, context)) return;
|
|
const container = getVueComponentPropertiesContainer(vueData.node);
|
|
const propertyReferences = propertyReferenceExtractor.extractFromExpression(node, false);
|
|
container.propertyReferences.push(propertyReferences);
|
|
}
|
|
}), {
|
|
Program() {
|
|
const styleVars = require_index$1.getStyleVariablesContext(context);
|
|
if (styleVars) templatePropertiesContainer.propertyReferences.push(propertyReferenceExtractor.extractFromStyleVariablesContext(styleVars));
|
|
},
|
|
"Program:exit"(node) {
|
|
if (!node.templateBody) reportUnusedProperties();
|
|
}
|
|
});
|
|
return import_utils.default.defineTemplateBodyVisitor(context, {
|
|
VExpressionContainer(node) {
|
|
const property = propertyReferenceExtractor.extractFromVExpressionContainer(node);
|
|
templatePropertiesContainer.propertyReferences.push(property);
|
|
if (!propsReferencePattern) return;
|
|
for (const key of property.allProperties().keys()) if (propsReferencePattern.type === "Identifier" && propsReferencePattern.name === key) templatePropertiesContainer.propertyReferences.push(property.getNest(key));
|
|
},
|
|
"VAttribute[directive=false]"(node) {
|
|
if (node.key.name === "ref" && node.value != null) templatePropertiesContainer.refNames.add(node.value.value);
|
|
},
|
|
"VElement[parent.type!='VElement']:exit"() {
|
|
reportUnusedProperties();
|
|
}
|
|
}, scriptVisitor);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
exports.default = no_unused_properties_default; |