mirror of
https://github.com/langgenius/dify.git
synced 2026-03-16 12:27:42 +08:00
317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import tsParser from '@typescript-eslint/parser'
|
|
|
|
const TS_TSX_FILE_PATTERN = /\.(?:ts|tsx)$/
|
|
const TYPE_COVERAGE_EXCLUDE_BASENAMES = new Set([
|
|
'type',
|
|
'types',
|
|
'declarations',
|
|
])
|
|
const GENERATED_FILE_COMMENT_PATTERNS = [
|
|
/@generated/i,
|
|
/\bauto-?generated\b/i,
|
|
/\bgenerated by\b/i,
|
|
/\bgenerate by\b/i,
|
|
/\bdo not edit\b/i,
|
|
/\bdon not edit\b/i,
|
|
]
|
|
const PARSER_OPTIONS = {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
ecmaFeatures: { jsx: true },
|
|
}
|
|
|
|
const collectedExcludedFilesCache = new Map()
|
|
|
|
export const COMPONENT_COVERAGE_EXCLUDE_LABEL = 'type-only files, pure barrel files, generated files, pure static files'
|
|
|
|
export function isTypeCoverageExcludedComponentFile(filePath) {
|
|
return TYPE_COVERAGE_EXCLUDE_BASENAMES.has(getPathBaseNameWithoutExtension(filePath))
|
|
}
|
|
|
|
export function getComponentCoverageExclusionReasons(filePath, sourceCode) {
|
|
if (!isEligibleComponentSourceFilePath(filePath))
|
|
return []
|
|
|
|
const reasons = []
|
|
if (isTypeCoverageExcludedComponentFile(filePath))
|
|
reasons.push('type-only')
|
|
|
|
if (typeof sourceCode !== 'string' || sourceCode.length === 0)
|
|
return reasons
|
|
|
|
if (isGeneratedComponentFile(sourceCode))
|
|
reasons.push('generated')
|
|
|
|
const ast = parseComponentFile(sourceCode)
|
|
if (!ast)
|
|
return reasons
|
|
|
|
if (isPureBarrelComponentFile(ast))
|
|
reasons.push('pure-barrel')
|
|
else if (isPureStaticComponentFile(ast))
|
|
reasons.push('pure-static')
|
|
|
|
return reasons
|
|
}
|
|
|
|
export function collectComponentCoverageExcludedFiles(rootDir, options = {}) {
|
|
const normalizedRootDir = path.resolve(rootDir)
|
|
const pathPrefix = normalizePathPrefix(options.pathPrefix ?? '')
|
|
const cacheKey = `${normalizedRootDir}::${pathPrefix}`
|
|
const cached = collectedExcludedFilesCache.get(cacheKey)
|
|
if (cached)
|
|
return cached
|
|
|
|
const files = []
|
|
walkComponentFiles(normalizedRootDir, (absolutePath) => {
|
|
const relativePath = path.relative(normalizedRootDir, absolutePath).split(path.sep).join('/')
|
|
const prefixedPath = pathPrefix ? `${pathPrefix}/${relativePath}` : relativePath
|
|
const sourceCode = fs.readFileSync(absolutePath, 'utf8')
|
|
if (getComponentCoverageExclusionReasons(prefixedPath, sourceCode).length > 0)
|
|
files.push(prefixedPath)
|
|
})
|
|
|
|
files.sort((a, b) => a.localeCompare(b))
|
|
collectedExcludedFilesCache.set(cacheKey, files)
|
|
return files
|
|
}
|
|
|
|
function normalizePathPrefix(pathPrefix) {
|
|
return pathPrefix.replace(/\\/g, '/').replace(/\/$/, '')
|
|
}
|
|
|
|
function walkComponentFiles(currentDir, onFile) {
|
|
if (!fs.existsSync(currentDir))
|
|
return
|
|
|
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(currentDir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === '__tests__' || entry.name === '__mocks__')
|
|
continue
|
|
walkComponentFiles(entryPath, onFile)
|
|
continue
|
|
}
|
|
|
|
if (!isEligibleComponentSourceFilePath(entry.name))
|
|
continue
|
|
|
|
onFile(entryPath)
|
|
}
|
|
}
|
|
|
|
function isEligibleComponentSourceFilePath(filePath) {
|
|
return TS_TSX_FILE_PATTERN.test(filePath)
|
|
&& !isTestLikePath(filePath)
|
|
}
|
|
|
|
function isTestLikePath(filePath) {
|
|
return /(?:^|\/)__tests__\//.test(filePath)
|
|
|| /(?:^|\/)__mocks__\//.test(filePath)
|
|
|| /\.(?:spec|test)\.(?:ts|tsx)$/.test(filePath)
|
|
|| /\.stories\.(?:ts|tsx)$/.test(filePath)
|
|
|| /\.d\.ts$/.test(filePath)
|
|
}
|
|
|
|
function getPathBaseNameWithoutExtension(filePath) {
|
|
if (!filePath)
|
|
return ''
|
|
|
|
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
const fileName = normalizedPath.split('/').pop() ?? ''
|
|
return fileName.replace(TS_TSX_FILE_PATTERN, '')
|
|
}
|
|
|
|
function isGeneratedComponentFile(sourceCode) {
|
|
const leadingText = sourceCode.split('\n').slice(0, 5).join('\n')
|
|
return GENERATED_FILE_COMMENT_PATTERNS.some(pattern => pattern.test(leadingText))
|
|
}
|
|
|
|
function parseComponentFile(sourceCode) {
|
|
try {
|
|
return tsParser.parse(sourceCode, PARSER_OPTIONS)
|
|
}
|
|
catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function isPureBarrelComponentFile(ast) {
|
|
let hasRuntimeReExports = false
|
|
|
|
for (const statement of ast.body) {
|
|
if (statement.type === 'ExportAllDeclaration') {
|
|
hasRuntimeReExports = true
|
|
continue
|
|
}
|
|
|
|
if (statement.type === 'ExportNamedDeclaration' && statement.source) {
|
|
hasRuntimeReExports = hasRuntimeReExports || statement.exportKind !== 'type'
|
|
continue
|
|
}
|
|
|
|
if (statement.type === 'TSInterfaceDeclaration' || statement.type === 'TSTypeAliasDeclaration')
|
|
continue
|
|
|
|
return false
|
|
}
|
|
|
|
return hasRuntimeReExports
|
|
}
|
|
|
|
function isPureStaticComponentFile(ast) {
|
|
const importedStaticBindings = collectImportedStaticBindings(ast.body)
|
|
const staticBindings = new Set()
|
|
let hasRuntimeValue = false
|
|
|
|
for (const statement of ast.body) {
|
|
if (statement.type === 'ImportDeclaration')
|
|
continue
|
|
|
|
if (statement.type === 'TSInterfaceDeclaration' || statement.type === 'TSTypeAliasDeclaration')
|
|
continue
|
|
|
|
if (statement.type === 'ExportAllDeclaration')
|
|
return false
|
|
|
|
if (statement.type === 'ExportNamedDeclaration' && statement.source)
|
|
return false
|
|
|
|
if (statement.type === 'ExportDefaultDeclaration') {
|
|
if (!isStaticExpression(statement.declaration, staticBindings, importedStaticBindings))
|
|
return false
|
|
hasRuntimeValue = true
|
|
continue
|
|
}
|
|
|
|
if (statement.type === 'ExportNamedDeclaration' && statement.declaration) {
|
|
if (!handleStaticDeclaration(statement.declaration, staticBindings, importedStaticBindings))
|
|
return false
|
|
hasRuntimeValue = true
|
|
continue
|
|
}
|
|
|
|
if (statement.type === 'ExportNamedDeclaration' && statement.specifiers.length > 0) {
|
|
const allStaticSpecifiers = statement.specifiers.every((specifier) => {
|
|
if (specifier.type !== 'ExportSpecifier' || specifier.exportKind === 'type')
|
|
return false
|
|
return specifier.local.type === 'Identifier' && staticBindings.has(specifier.local.name)
|
|
})
|
|
if (!allStaticSpecifiers)
|
|
return false
|
|
hasRuntimeValue = true
|
|
continue
|
|
}
|
|
|
|
if (!handleStaticDeclaration(statement, staticBindings, importedStaticBindings))
|
|
return false
|
|
hasRuntimeValue = true
|
|
}
|
|
|
|
return hasRuntimeValue
|
|
}
|
|
|
|
function handleStaticDeclaration(statement, staticBindings, importedStaticBindings) {
|
|
if (statement.type !== 'VariableDeclaration' || statement.kind !== 'const')
|
|
return false
|
|
|
|
for (const declarator of statement.declarations) {
|
|
if (declarator.id.type !== 'Identifier' || !declarator.init)
|
|
return false
|
|
|
|
if (!isStaticExpression(declarator.init, staticBindings, importedStaticBindings))
|
|
return false
|
|
|
|
staticBindings.add(declarator.id.name)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function collectImportedStaticBindings(statements) {
|
|
const importedBindings = new Set()
|
|
|
|
for (const statement of statements) {
|
|
if (statement.type !== 'ImportDeclaration')
|
|
continue
|
|
|
|
const importSource = String(statement.source.value ?? '')
|
|
const isTypeLikeSource = isTypeCoverageExcludedComponentFile(importSource)
|
|
const importIsStatic = statement.importKind === 'type' || isTypeLikeSource
|
|
if (!importIsStatic)
|
|
continue
|
|
|
|
for (const specifier of statement.specifiers) {
|
|
if (specifier.local?.type === 'Identifier')
|
|
importedBindings.add(specifier.local.name)
|
|
}
|
|
}
|
|
|
|
return importedBindings
|
|
}
|
|
|
|
function isStaticExpression(node, staticBindings, importedStaticBindings) {
|
|
switch (node.type) {
|
|
case 'Literal':
|
|
return true
|
|
case 'Identifier':
|
|
return staticBindings.has(node.name) || importedStaticBindings.has(node.name)
|
|
case 'TemplateLiteral':
|
|
return node.expressions.every(expression => isStaticExpression(expression, staticBindings, importedStaticBindings))
|
|
case 'ArrayExpression':
|
|
return node.elements.every(element => !element || isStaticExpression(element, staticBindings, importedStaticBindings))
|
|
case 'ObjectExpression':
|
|
return node.properties.every((property) => {
|
|
if (property.type === 'SpreadElement')
|
|
return isStaticExpression(property.argument, staticBindings, importedStaticBindings)
|
|
|
|
if (property.type !== 'Property' || property.method)
|
|
return false
|
|
|
|
if (property.computed && !isStaticExpression(property.key, staticBindings, importedStaticBindings))
|
|
return false
|
|
|
|
if (property.shorthand)
|
|
return property.value.type === 'Identifier' && staticBindings.has(property.value.name)
|
|
|
|
return isStaticExpression(property.value, staticBindings, importedStaticBindings)
|
|
})
|
|
case 'UnaryExpression':
|
|
return isStaticExpression(node.argument, staticBindings, importedStaticBindings)
|
|
case 'BinaryExpression':
|
|
case 'LogicalExpression':
|
|
return isStaticExpression(node.left, staticBindings, importedStaticBindings)
|
|
&& isStaticExpression(node.right, staticBindings, importedStaticBindings)
|
|
case 'ConditionalExpression':
|
|
return isStaticExpression(node.test, staticBindings, importedStaticBindings)
|
|
&& isStaticExpression(node.consequent, staticBindings, importedStaticBindings)
|
|
&& isStaticExpression(node.alternate, staticBindings, importedStaticBindings)
|
|
case 'MemberExpression':
|
|
return isStaticMemberExpression(node, staticBindings, importedStaticBindings)
|
|
case 'ChainExpression':
|
|
return isStaticExpression(node.expression, staticBindings, importedStaticBindings)
|
|
case 'TSAsExpression':
|
|
case 'TSSatisfiesExpression':
|
|
case 'TSTypeAssertion':
|
|
case 'TSNonNullExpression':
|
|
return isStaticExpression(node.expression, staticBindings, importedStaticBindings)
|
|
case 'ParenthesizedExpression':
|
|
return isStaticExpression(node.expression, staticBindings, importedStaticBindings)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
function isStaticMemberExpression(node, staticBindings, importedStaticBindings) {
|
|
if (!isStaticExpression(node.object, staticBindings, importedStaticBindings))
|
|
return false
|
|
|
|
if (!node.computed)
|
|
return node.property.type === 'Identifier'
|
|
|
|
return isStaticExpression(node.property, staticBindings, importedStaticBindings)
|
|
}
|