sicua
Version:
A tool for analyzing project structure and dependencies
221 lines (220 loc) • 9.15 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compareComponents = compareComponents;
exports.generateDeduplicationData = generateDeduplicationData;
exports.filterSignificantSimilarities = filterSignificantSimilarities;
const analysisUtils_1 = require("../../../utils/common/analysisUtils");
const deduplication_types_1 = require("../types/deduplication.types");
const propComparison_1 = require("./propComparison");
const structureComparison_1 = require("./structureComparison");
const short_unique_id_1 = __importDefault(require("short-unique-id"));
const { randomUUID } = new short_unique_id_1.default();
/**
* Compares two components for similarity with balanced filtering
*/
function compareComponents(comp1, comp2, thresholds = deduplication_types_1.DEFAULT_SIMILARITY_THRESHOLDS) {
// Find common props and structures
const commonProps = findCommonProps(comp1.props, comp2.props);
const commonStructure = (0, structureComparison_1.findCommonStructure)(comp1.jsxStructure, comp2.jsxStructure);
// Calculate structure complexity
const complexity1 = (0, structureComparison_1.calculateStructureComplexity)(comp1.jsxStructure);
const complexity2 = (0, structureComparison_1.calculateStructureComplexity)(comp2.jsxStructure);
const complexityRatio = Math.min(complexity1.complexity, complexity2.complexity) /
Math.max(complexity1.complexity, complexity2.complexity);
// Check minimum complexity requirements
if (complexityRatio < thresholds.minComplexityRatio ||
complexity1.complexity < thresholds.minStructureComplexity ||
complexity2.complexity < thresholds.minStructureComplexity) {
return createLowSimilarityResult(comp1, comp2, commonProps, commonStructure);
}
// Calculate base similarity scores
const propsScore = (0, propComparison_1.calculatePropsSimilarity)(commonProps, [comp1, comp2]);
const childComponentScore = (0, structureComparison_1.calculateChildComponentSimilarity)(comp1.jsxStructure, comp2.jsxStructure);
const styleScore = (0, structureComparison_1.calculateStyleSimilarity)(comp1.jsxStructure, comp2.jsxStructure);
const baseStructureScore = (0, structureComparison_1.calculateStructureSimilarity)(commonStructure, [
comp1.jsxStructure,
comp2.jsxStructure,
]);
// Apply common pattern penalty
const commonPatternPenalty = calculateCommonPatternPenalty(comp1.jsxStructure, comp2.jsxStructure);
// Calculate weighted similarity score
const structureScore = (baseStructureScore + childComponentScore + styleScore) / 3;
const rawSimilarityScore = propsScore * 0.4 + structureScore * 0.6;
// Apply penalty for too many common UI patterns
const similarityScore = Math.max(0, Math.round((rawSimilarityScore - commonPatternPenalty) * 100) / 100);
const deduplicationData = generateDeduplicationData([comp1, comp2], commonProps, commonStructure);
return {
groupId: randomUUID(),
components: [(0, analysisUtils_1.generateComponentId)(comp1), (0, analysisUtils_1.generateComponentId)(comp2)],
commonProps,
commonJSXStructure: commonStructure,
similarityScore,
deduplicationData,
};
}
/**
* Calculates penalty for components that share mostly common UI patterns
*/
function calculateCommonPatternPenalty(struct1, struct2) {
if (!struct1 || !struct2)
return 0;
const elements1 = getAllElementNames(struct1);
const elements2 = getAllElementNames(struct2);
// Count common UI patterns
const commonPatterns1 = elements1.filter((el) => deduplication_types_1.COMMON_UI_PATTERNS.includes(el)).length;
const commonPatterns2 = elements2.filter((el) => deduplication_types_1.COMMON_UI_PATTERNS.includes(el)).length;
// Calculate ratio of common patterns
const ratio1 = elements1.length > 0 ? commonPatterns1 / elements1.length : 0;
const ratio2 = elements2.length > 0 ? commonPatterns2 / elements2.length : 0;
const avgRatio = (ratio1 + ratio2) / 2;
// Apply penalty if more than 70% of elements are common UI patterns
if (avgRatio > 0.7) {
return (avgRatio - 0.7) * 0.5; // Up to 15% penalty
}
return 0;
}
/**
* Gets all element names from JSX structure
*/
function getAllElementNames(structure) {
const names = [structure.tagName];
structure.children.forEach((child) => {
names.push(...getAllElementNames(child));
});
return names;
}
/**
* Creates a low similarity result
*/
function createLowSimilarityResult(comp1, comp2, commonProps, commonStructure) {
return {
groupId: randomUUID(),
components: [(0, analysisUtils_1.generateComponentId)(comp1), (0, analysisUtils_1.generateComponentId)(comp2)],
commonProps,
commonJSXStructure: commonStructure,
similarityScore: 0.1,
deduplicationData: generateDeduplicationData([comp1, comp2], commonProps, commonStructure),
};
}
/**
* Finds common props between two sets of props
*/
function findCommonProps(props1, props2) {
if (!props1 || !props2)
return [];
return props1.filter((prop1) => props2.some((prop2) => prop1.name === prop2.name && prop1.type === prop2.type));
}
/**
* Generates detailed deduplication data for components
*/
function generateDeduplicationData(components, commonProps, commonStructure) {
const componentData = components.map((comp) => ({
name: comp.name,
path: comp.fullPath,
content: comp.content || "",
componentId: (0, analysisUtils_1.generateComponentId)(comp),
}));
const propSimilarities = commonProps.map((prop) => ({
name: prop.name,
type: prop.type,
isRequired: prop.required,
usedInComponents: components.map((c) => (0, analysisUtils_1.generateComponentId)(c)),
}));
const propDifferences = components.map((comp) => ({
componentName: comp.name,
componentId: (0, analysisUtils_1.generateComponentId)(comp),
uniqueProps: (comp.props || [])
.filter((prop) => !commonProps.some((cp) => cp.name === prop.name))
.map((prop) => ({
name: prop.name,
type: prop.type,
isRequired: prop.required,
})),
}));
const jsxSimilarity = {
sharedRootElement: commonStructure[0]?.tagName || "",
sharedStructure: extractSharedStructure(commonStructure),
sharedClassNames: extractSharedClassNames(commonStructure),
};
const jsxDifferences = components.map((comp) => ({
componentName: comp.name,
componentId: (0, analysisUtils_1.generateComponentId)(comp),
uniqueElements: findUniqueElements(comp.jsxStructure, commonStructure),
}));
return {
components: componentData,
commonalities: {
props: propSimilarities,
structure: jsxSimilarity,
},
differences: {
props: propDifferences,
structure: jsxDifferences,
},
};
}
/**
* Extracts the structure of shared elements
*/
function extractSharedStructure(structure) {
const result = [];
const process = (node) => {
result.push(node.tagName);
node.children.forEach(process);
};
structure.forEach(process);
return result;
}
/**
* Extracts shared class names from JSX structure
*/
function extractSharedClassNames(structure) {
const classNames = [];
const process = (node) => {
const className = node.props.find((p) => p.name === "className");
if (className) {
const classes = className.type
.replace(/['"]/g, "")
.split(" ")
.filter(Boolean);
classNames.push(...classes);
}
node.children.forEach(process);
};
structure.forEach(process);
return [...new Set(classNames)];
}
/**
* Finds elements unique to a component's structure
*/
function findUniqueElements(componentStructure, commonStructure) {
const unique = [];
const process = (node, commonNode, path = "") => {
if (!node)
return;
if (!commonNode || node.tagName !== commonNode.tagName) {
unique.push({
element: node.tagName,
location: path,
props: node.props.reduce((acc, prop) => ({
...acc,
[prop.name]: prop.type,
}), {}),
});
}
node.children.forEach((child, index) => {
process(child, commonNode?.children[index], `${path}${path ? "." : ""}children[${index}]`);
});
};
process(componentStructure, commonStructure?.[0]);
return unique;
}
/**
* Filters similarities based on a minimum threshold
*/
function filterSignificantSimilarities(similarities, threshold = deduplication_types_1.DEFAULT_SIMILARITY_THRESHOLDS.minSimilarityScore) {
return similarities.filter((s) => s.similarityScore >= threshold);
}