fix: 修复通配符匹配问题
This commit is contained in:
65
index.js
65
index.js
@@ -3,6 +3,31 @@ const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* 工具函数:安全执行异步操作,忽略错误
|
||||
*
|
||||
* @template T
|
||||
* @param {() => Promise<T>} fn - 要执行的异步函数
|
||||
* @param {T} defaultValue - 出错时的默认值
|
||||
* @returns {Promise<T>} 函数执行结果或默认值
|
||||
*/
|
||||
async function safeExecute(fn, defaultValue = undefined) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:安全执行同步操作,忽略错误
|
||||
*
|
||||
* @template T
|
||||
* @param {() => T} fn - 要执行的同步函数
|
||||
* @param {T} defaultValue - 出错时的默认值
|
||||
* @returns {T} 函数执行结果或默认值
|
||||
*/
|
||||
|
||||
/**
|
||||
* 错误类型常量.
|
||||
*
|
||||
@@ -191,8 +216,10 @@ class FileDiscovery {
|
||||
const files = new Set();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
await safeExecute(async () => {
|
||||
const matchedFiles = await this.expandGlob(pattern.trim());
|
||||
matchedFiles.forEach(file => files.add(file));
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(files).sort();
|
||||
@@ -240,17 +267,16 @@ class FileDiscovery {
|
||||
async expandGlob(pattern) {
|
||||
// 如果是普通文件路径,直接返回
|
||||
if (!pattern.includes('*') && !pattern.includes('?')) {
|
||||
try {
|
||||
const stats = await fs.stat(pattern);
|
||||
const stats = await safeExecute(() => fs.stat(pattern));
|
||||
if (stats) {
|
||||
if (stats.isFile()) {
|
||||
return [pattern];
|
||||
} else if (stats.isDirectory()) {
|
||||
// 如果是目录,返回目录下所有文件
|
||||
return await this.getAllFilesInDirectory(pattern);
|
||||
}
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 处理 glob 模式
|
||||
@@ -267,8 +293,7 @@ class FileDiscovery {
|
||||
async getAllFilesInDirectory(dirPath, includeHidden = false) {
|
||||
const files = [];
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
const entries = await safeExecute(() => fs.readdir(dirPath, { withFileTypes: true }), []);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
@@ -285,9 +310,6 @@ class FileDiscovery {
|
||||
files.push(...subFiles);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略无法访问的目录
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
@@ -312,8 +334,7 @@ class FileDiscovery {
|
||||
}
|
||||
|
||||
// 处理简单的文件名通配符
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const entries = await safeExecute(() => fs.readdir(dir, { withFileTypes: true }), []);
|
||||
|
||||
for (const entry of entries) {
|
||||
// 默认忽略隐藏文件(以.开头的文件)
|
||||
@@ -325,9 +346,6 @@ class FileDiscovery {
|
||||
files.push(path.join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 目录不存在或无法访问
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
@@ -348,18 +366,18 @@ class FileDiscovery {
|
||||
const basePath = parts[0].replace(/\*+$/, '').replace(/\/$/, '') || '.';
|
||||
const remainingPattern = parts[1].replace(/^\//, '');
|
||||
|
||||
try {
|
||||
const allFiles = await this.getAllFilesInDirectory(basePath);
|
||||
const allFiles = await safeExecute(() => this.getAllFilesInDirectory(basePath), []);
|
||||
|
||||
for (const file of allFiles) {
|
||||
const relativePath = path.relative(basePath, file);
|
||||
if (this.matchPattern(relativePath, remainingPattern)) {
|
||||
// 对于 **/pattern 模式,匹配相对路径的末尾部分
|
||||
if (remainingPattern && (relativePath === remainingPattern || relativePath.endsWith('/' + remainingPattern))) {
|
||||
files.push(file);
|
||||
} else if (this.matchPattern(relativePath, remainingPattern)) {
|
||||
// 对于包含通配符的模式,使用 matchPattern
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
} else {
|
||||
// 处理单层通配符
|
||||
const parts = pattern.split('/');
|
||||
@@ -385,8 +403,7 @@ class FileDiscovery {
|
||||
const [currentPart, ...remainingParts] = parts;
|
||||
const files = [];
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
||||
const entries = await safeExecute(() => fs.readdir(currentPath, { withFileTypes: true }), []);
|
||||
|
||||
for (const entry of entries) {
|
||||
// 默认忽略隐藏文件和目录(以.开头的)
|
||||
@@ -411,9 +428,6 @@ class FileDiscovery {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
@@ -686,4 +700,5 @@ module.exports = {
|
||||
ActionOutputs,
|
||||
ActionError,
|
||||
ErrorType,
|
||||
safeExecute,
|
||||
};
|
||||
|
||||
@@ -76,20 +76,144 @@ describe('测试 FileDiscovery 类', () => {
|
||||
expect(files[0]).toBe('test-dir/file1.js');
|
||||
});
|
||||
|
||||
it('应该使用通配符模式查找文件', async () => {
|
||||
it('应该找到通配符(*.js)形式的文件', async () => {
|
||||
const discovery = new FileDiscovery();
|
||||
|
||||
const files = await discovery.findFiles(['test-dir/*.js']);
|
||||
expect(files).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('应该递归查找文件', async () => {
|
||||
it('应该找到递归通配符(**/*.js)形式的文件', async () => {
|
||||
const discovery = new FileDiscovery();
|
||||
|
||||
const files = await discovery.findFiles(['test-dir/**/*.js']);
|
||||
expect(files).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('应该找到递归通配符开头与文件名(**/config.json)形式的文件', async () => {
|
||||
const tempDir = await createTestDir();
|
||||
const discovery = new FileDiscovery();
|
||||
|
||||
// 创建多个目录层级的同名文件
|
||||
const configFiles = [
|
||||
path.join(tempDir, 'config.json'),
|
||||
path.join(tempDir, 'src', 'config.json'),
|
||||
path.join(tempDir, 'src', 'utils', 'config.json'),
|
||||
path.join(tempDir, 'lib', 'config.json'),
|
||||
path.join(tempDir, 'test', 'config.json'),
|
||||
];
|
||||
|
||||
// 创建一些其他文件用于验证不会误匹配
|
||||
const otherFiles = [
|
||||
path.join(tempDir, 'src', 'app.js'),
|
||||
path.join(tempDir, 'src', 'utils', 'helper.js'),
|
||||
path.join(tempDir, 'lib', 'main.js'),
|
||||
path.join(tempDir, 'test', 'test.js'),
|
||||
];
|
||||
|
||||
// 创建所有文件
|
||||
for (const file of [...configFiles, ...otherFiles]) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
await createTestFile(file, '{"test": "content"}');
|
||||
}
|
||||
|
||||
// 测试 tempDir 目录下的 **/config.json 模式(限制在测试目录内)
|
||||
const files = await discovery.findFiles([path.join(tempDir, '**/config.json')]);
|
||||
|
||||
// 应该找到所有的 config.json 文件
|
||||
expect(files).toHaveLength(configFiles.length);
|
||||
for (const configFile of configFiles) {
|
||||
expect(files).toContain(configFile);
|
||||
}
|
||||
|
||||
// 清理测试文件
|
||||
cleanupTestFiles([...configFiles, ...otherFiles]);
|
||||
cleanupTestDirs(tempDir);
|
||||
});
|
||||
|
||||
it('应该找到递归通配符开头与扩展名(**/*.md)形式的文件', async () => {
|
||||
const tempDir = await createTestDir();
|
||||
const discovery = new FileDiscovery();
|
||||
|
||||
// 创建多层目录结构中的 .md 文件
|
||||
const mdFiles = [
|
||||
path.join(tempDir, 'README.md'),
|
||||
path.join(tempDir, 'docs', 'guide.md'),
|
||||
path.join(tempDir, 'src', 'docs', 'api.md'),
|
||||
path.join(tempDir, 'lib', 'components', 'button.md'),
|
||||
path.join(tempDir, 'test', 'integration', 'tests.md'),
|
||||
];
|
||||
|
||||
// 创建一些其他扩展名的文件用于验证不会误匹配
|
||||
const otherFiles = [
|
||||
path.join(tempDir, 'src', 'main.js'),
|
||||
path.join(tempDir, 'src', 'styles.css'),
|
||||
path.join(tempDir, 'docs', 'config.json'),
|
||||
path.join(tempDir, 'test', 'test.py'),
|
||||
];
|
||||
|
||||
// 创建所有文件
|
||||
for (const file of [...mdFiles, ...otherFiles]) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
await createTestFile(file, '# Test Content');
|
||||
}
|
||||
|
||||
// 测试 tempDir 目录下的 **/*.md 模式(限制在测试目录内)
|
||||
const files = await discovery.findFiles([path.join(tempDir, '**/*.md')]);
|
||||
|
||||
// 应该找到所有的 .md 文件
|
||||
expect(files).toHaveLength(mdFiles.length);
|
||||
for (const mdFile of mdFiles) {
|
||||
expect(files).toContain(mdFile);
|
||||
}
|
||||
|
||||
// 清理测试文件
|
||||
cleanupTestFiles([...mdFiles, ...otherFiles]);
|
||||
cleanupTestDirs(tempDir);
|
||||
});
|
||||
|
||||
it('应该正确处理递归通配符前缀的复杂模式', async () => {
|
||||
const tempDir = await createTestDir();
|
||||
const discovery = new FileDiscovery();
|
||||
|
||||
// 创建测试文件结构
|
||||
const testFiles = [
|
||||
path.join(tempDir, 'src', 'components', 'Button.js'),
|
||||
path.join(tempDir, 'src', 'components', 'Input.js'),
|
||||
path.join(tempDir, 'src', 'utils', 'helpers.js'),
|
||||
path.join(tempDir, 'lib', 'vendor', 'jquery.js'),
|
||||
path.join(tempDir, 'test', 'unit', 'Button.test.js'),
|
||||
path.join(tempDir, 'test', 'integration', 'App.test.js'),
|
||||
];
|
||||
|
||||
// 创建一些非 JS 文件
|
||||
const nonJSFiles = [
|
||||
path.join(tempDir, 'src', 'styles.css'),
|
||||
path.join(tempDir, 'docs', 'README.md'),
|
||||
path.join(tempDir, 'config.json'),
|
||||
];
|
||||
|
||||
// 创建所有文件
|
||||
for (const file of [...testFiles, ...nonJSFiles]) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
await createTestFile(file, 'console.log("test");');
|
||||
}
|
||||
|
||||
// 测试 tempDir 目录下的 **/*.js 模式(限制在测试目录内)
|
||||
const files = await discovery.findFiles([path.join(tempDir, '**/*.js')]);
|
||||
|
||||
// 应该找到所有的 .js 文件
|
||||
expect(files).toHaveLength(testFiles.length);
|
||||
for (const jsFile of testFiles) {
|
||||
expect(files).toContain(jsFile);
|
||||
}
|
||||
|
||||
// 清理测试文件
|
||||
cleanupTestFiles([...testFiles, ...nonJSFiles]);
|
||||
cleanupTestDirs(tempDir);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理空目录', async () => {
|
||||
const tempDir = await createTestDir();
|
||||
const discovery = new FileDiscovery();
|
||||
@@ -193,7 +317,6 @@ describe('测试 FileDiscovery 类', () => {
|
||||
cleanupTestFiles(deepFile);
|
||||
cleanupTestDirs(tempDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('测试 FileDiscovery.validateFiles 方法', () => {
|
||||
const existingFile = 'existing-file.txt';
|
||||
|
||||
Reference in New Issue
Block a user