Merge pull request #2744 from LaZzyMan/fix/tree-sitter-symlink-wasm

fix(core): resolve tree-sitter wasm path for symlinked CLI
This commit is contained in:
顾盼 2026-03-30 15:52:02 +08:00 committed by GitHub
commit fcaa1729d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 5 deletions

View file

@ -4,12 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import {
initParser,
isShellCommandReadOnlyAST,
extractCommandRules,
_resetParser,
_resolveWasmPathForTesting,
} from './shellAstParser.js';
beforeAll(async () => {
@ -20,6 +22,44 @@ afterAll(() => {
_resetParser();
});
describe('WASM path resolution', () => {
it('resolves bundled WASM relative to the real CLI path when launched via symlink', () => {
const symlinkedCliPath = path.join('/usr', 'bin', 'qwen');
const realCliPath = path.join(
'/opt',
'homebrew',
'lib',
'node_modules',
'@qwen-code',
'qwen-code',
'dist',
'cli.js',
);
const result = _resolveWasmPathForTesting(
'tree-sitter.wasm',
symlinkedCliPath,
() => realCliPath,
);
expect(result).toBe(
path.join(
'/opt',
'homebrew',
'lib',
'node_modules',
'@qwen-code',
'qwen-code',
'dist',
'vendor',
'tree-sitter',
'tree-sitter.wasm',
),
);
expect(result).not.toContain(path.join('/usr', 'bin', 'vendor'));
});
});
// =========================================================================
// isShellCommandReadOnlyAST — mirror all tests from shellReadOnlyChecker.test.ts
// =========================================================================

View file

@ -15,6 +15,7 @@
*/
import Parser from 'web-tree-sitter';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -22,8 +23,18 @@ import { fileURLToPath } from 'node:url';
// Constants
// ---------------------------------------------------------------------------
const __filename_ = fileURLToPath(import.meta.url);
const __dirname_ = path.dirname(__filename_);
const __filename_ = resolveModuleFilePath(fileURLToPath(import.meta.url));
function resolveModuleFilePath(moduleFilePath: string): string {
try {
const resolved = fs.realpathSync(moduleFilePath);
// Guard against test environments where `fs` is mocked and realpathSync
// returns undefined rather than throwing.
return typeof resolved === 'string' ? resolved : moduleFilePath;
} catch {
return moduleFilePath;
}
}
/**
* Root commands considered read-only by default (no sub-command analysis needed
@ -569,10 +580,24 @@ let initPromise: Promise<void> | null = null;
* - Bundle (dist/cli.js): vendor at same level (0 levels)
*/
function resolveWasmPath(filename: string): string {
const inSrcUtils = __filename_.includes(path.join('src', 'utils'));
const levelsUp = !inSrcUtils ? 0 : __filename_.endsWith('.ts') ? 2 : 3;
return resolveWasmPathForModule(filename, __filename_);
}
function resolveWasmPathForModule(
filename: string,
moduleFilePath: string,
resolvePath: (moduleFilePath: string) => string = resolveModuleFilePath,
): string {
const resolvedModuleFilePath = resolvePath(moduleFilePath);
const moduleDir = path.dirname(resolvedModuleFilePath);
const inSrcUtils = resolvedModuleFilePath.includes(path.join('src', 'utils'));
const levelsUp = !inSrcUtils
? 0
: resolvedModuleFilePath.endsWith('.ts')
? 2
: 3;
return path.join(
__dirname_,
moduleDir,
...Array<string>(levelsUp).fill('..'),
'vendor',
'tree-sitter',
@ -1084,3 +1109,15 @@ export function _resetParser(): void {
bashLanguage = null;
initPromise = null;
}
/**
* Internal helper exposed for tests.
* @internal
*/
export function _resolveWasmPathForTesting(
filename: string,
moduleFilePath: string,
resolvePath?: (moduleFilePath: string) => string,
): string {
return resolveWasmPathForModule(filename, moduleFilePath, resolvePath);
}