mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-31 02:06:21 +00:00
* refactor(cli): enforce utils leaf-layer dependency direction (#9146) Move domain-coupled modules out of packages/cli/src/utils into the directories that own them: config/ (dialogScopeUtils, settingsUtils), i18n/ (languageUtils), ui/ (handleAutoUpdate, standalone-update, systemInfo, systemInfoFields, update-relaunch, commands, doctorChecks), nonInteractive/ (nonInteractiveHelpers, chat-recording-failure, tool-result-boundary-diagnostics, permission-suggestions), serve/ (sandbox), services/housekeeping/ (scheduler, non-interactive-scheduler), and commands/review/ (findings). Extract the generic normalizePartList helper into utils/normalize-part-list.ts so utils consumers keep importing downward, and move the MergeStrategy enum into utils/deepMerge.ts (its owner). Add an eslint architecture rule (no-utils-upward-import) that forbids value imports from utils/ back up into a domain directory. Type-only imports stay exempt: they are erased at compile time and cannot create a runtime cycle (Settings in modelConfigUtils, CommandContext in sessionPaths). No behavior change: typecheck, build, and the affected unit tests pass. * fix: use Qwen Team 2026 license header on new files (#9146) * chore: refresh stale utils/ path references after leaf-layer move (#9146) * docs: reconcile no-utils-upward-import header with the allowed type-only set (#9146) * fix(cli): allowlist sandbox process.env accesses after leaf-layer move (#9146) * chore(ci): re-record qwen-autofix.yml size baseline after #9677 (#9146) #9677 recorded qwen-autofix.yml at 392111 bytes while the file it committed was already 397656, so every PR that merged main after it tripped the growth ratchet. Re-record the actual size; the file itself is unchanged by this PR. * fix(review): drop the stale utils/findings.ts digest root after the leaf-layer move (#9146) The #9146 move returned findings.ts to commands/review/, but the digest root lists merged from main still pinned it under utils/, where the file no longer exists — the absent root darkened every review's staleness check and failed review-source-digest.test.ts. Drop the stale file-shaped root from both digest copies and their pins; the commands/review/ directory root covers the validator at its new home, and the two utils helpers keep their file-shaped roots. * fix(review): colocate seatbelt profiles with the sandbox module (#9146) * fix(review): exempt inline type-only specifiers from the utils upward-import rule (#9146) * fix(review): report upward inline type-specifier imports under verbatimModuleSyntax (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): pin mixed-specifier and zero-specifier upward imports in the utils rule (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): anchor the nested-checkout utils rule fixture on the last marker (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): pin that the utils/findings.ts digest root stays removed (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): reword stale-bundle SCOPE header to the post-move helper shape (#9146) * test(review): drop the pre-move utils/findings.ts from the skill-parity fixture (#9146) * test(serve): derive the seatbelt colocation tripwire from BUILTIN_SEATBELT_PROFILES (#9146) * fix(architecture): fail closed on computed dynamic imports in the utils leaf rule (#9146) * fix(cli): point settings.test.ts at the post-move settingsUtils path (#9146) main updated settings.test.ts after this branch moved settingsUtils.ts from utils/ into config/, and the merge kept main's old import specifier, which vite fails to resolve. Repoint it at ./settingsUtils.js; every other consumer already uses the new path. * fix(cli): close utils boundary review gaps * test(cli): cover utils boundary allow paths --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
178 lines
6.1 KiB
JavaScript
178 lines
6.1 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { Linter } from 'eslint';
|
|
import rule from '../../eslint-rules/no-utils-upward-import.js';
|
|
|
|
function runRule(code, filename) {
|
|
const linter = new Linter({ configType: 'eslintrc' });
|
|
linter.defineRule('architecture/no-utils-upward-import', rule);
|
|
return linter.verify(
|
|
code,
|
|
{
|
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
rules: { 'architecture/no-utils-upward-import': 'error' },
|
|
},
|
|
{ filename },
|
|
);
|
|
}
|
|
|
|
describe('no-utils-upward-import', () => {
|
|
it.each([
|
|
['packages/cli/src/utils/deepMerge.ts', '../config/settings.js'],
|
|
[
|
|
'packages/cli/src/utils/housekeeping/cleanup.ts',
|
|
'../../config/settings.js',
|
|
],
|
|
['packages/cli/src/utils/foo.ts', '../ui/commands/types.js'],
|
|
['packages/cli/src/utils/foo.ts', '../i18n/index.js'],
|
|
['packages/cli/src/utils/foo.ts', 'src/config/settings.js'],
|
|
[
|
|
'packages/cli/src/utils/foo.ts',
|
|
'../nonInteractive/nonInteractiveHelpers.js',
|
|
],
|
|
// nested checkout: the marker appears twice, so the utils root must be
|
|
// anchored on the LAST one — an indexOf anchor would resolve the import
|
|
// inside the outer utils root and report nothing
|
|
[
|
|
'/tmp/packages/cli/src/utils/nested/packages/cli/src/utils/foo.ts',
|
|
'../config/settings.js',
|
|
],
|
|
])('rejects upward imports from %s', (filename, importedPath) => {
|
|
expect(
|
|
runRule(`import value from '${importedPath}';`, filename),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it('rejects export and dynamic upward sources', () => {
|
|
const file = 'packages/cli/src/utils/foo.ts';
|
|
expect(
|
|
runRule("export { value } from '../config/settings.js';", file),
|
|
).toHaveLength(1);
|
|
// a zero-specifier re-export is a runtime edge (`export {} from` survives
|
|
// emission), not a vacuous `[].every(...)` type-only exemption
|
|
expect(
|
|
runRule("export {} from '../config/settings.js';", file),
|
|
).toHaveLength(1);
|
|
expect(
|
|
runRule("export * from '../config/settings.js';", file),
|
|
).toHaveLength(1);
|
|
expect(runRule("import('../config/settings.js');", file)).toHaveLength(1);
|
|
expect(runRule('import(`../config/settings.js`);', file)).toHaveLength(1);
|
|
expect(runRule("import('src/config/settings.js');", file)).toHaveLength(1);
|
|
});
|
|
|
|
it('fails closed on computed dynamic sources with a local known prefix', () => {
|
|
const file = 'packages/cli/src/utils/foo.ts';
|
|
// A multi-segment template: interpolation can contribute a `../` step,
|
|
// and a leading `../` cannot be undone, so the import cannot be proven
|
|
// to stay inside utils/ — reported even though part is interpolated.
|
|
expect(runRule('import(`../i18n/${locale}.js`);', file)).toHaveLength(1);
|
|
// A `+` concatenation with a relative leftmost literal, same reasoning.
|
|
expect(runRule("import('../config/' + name + '.js');", file)).toHaveLength(
|
|
1,
|
|
);
|
|
// A relative `./` prefix is also unprovable (interpolation could still
|
|
// climb), so it is reported too.
|
|
expect(runRule('import(`./sub/${name}.js`);', file)).toHaveLength(1);
|
|
expect(runRule('import(`src/config/${name}.js`);', file)).toHaveLength(1);
|
|
});
|
|
|
|
it('drops computed dynamic sources with no statically known local prefix', () => {
|
|
const file = 'packages/cli/src/utils/foo.ts';
|
|
// A bare identifier or a package-like prefix is the same boundary the
|
|
// static check applies to non-relative specifiers — not reported.
|
|
expect(runRule('import(moduleName);', file)).toHaveLength(0);
|
|
expect(runRule('import(`${pkg}/entry.js`);', file)).toHaveLength(0);
|
|
});
|
|
|
|
it('allows imports that stay within utils', () => {
|
|
expect(
|
|
runRule('import(`./sibling.js`);', 'packages/cli/src/utils/foo.ts'),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from './sibling.js';",
|
|
'packages/cli/src/utils/foo.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from '../cleanup.js';",
|
|
'packages/cli/src/utils/housekeeping/cleanup.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from '../../utils/sibling.js';",
|
|
'packages/cli/src/utils/housekeeping/cleanup.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from 'src/utils/sibling.js';",
|
|
'packages/cli/src/utils/foo.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
// same doubly-nested shape as the reject case: a sibling import stays
|
|
// inside the LAST marker's utils root
|
|
expect(
|
|
runRule(
|
|
"import value from './sibling.js';",
|
|
'/tmp/packages/cli/src/utils/nested/packages/cli/src/utils/foo.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from 'src/utils/sibling.js';",
|
|
'/tmp/packages/cli/src/utils/nested/packages/cli/src/utils/foo.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it('allows package and builtin imports', () => {
|
|
const file = 'packages/cli/src/utils/foo.ts';
|
|
expect(runRule("import fs from 'node:fs';", file)).toHaveLength(0);
|
|
expect(
|
|
runRule("import value from '@qwen-code/qwen-code-core';", file),
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it('ignores tests, fixtures, __tests__, and non-utils consumers', () => {
|
|
expect(
|
|
runRule(
|
|
"import value from '../config/settings.js';",
|
|
'packages/cli/src/utils/foo.test.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from '../config/settings.js';",
|
|
'packages/cli/src/utils/foo.spec.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from '../config/settings.js';",
|
|
'packages/cli/src/utils/__tests__/helper.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
expect(
|
|
runRule(
|
|
"import value from '../config/settings.js';",
|
|
'packages/cli/src/utils/fixtures/helper.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
// non-utils consumers may import utils freely
|
|
expect(
|
|
runRule(
|
|
"import value from '../utils/sibling.js';",
|
|
'packages/cli/src/config/foo.ts',
|
|
),
|
|
).toHaveLength(0);
|
|
});
|
|
});
|