diff --git a/eslint-rules/no-core-root-barrel-import.js b/eslint-rules/no-core-root-barrel-import.js new file mode 100644 index 00000000000..1b926d58cb8 --- /dev/null +++ b/eslint-rules/no-core-root-barrel-import.js @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Prevents core production modules from importing the core root barrel. + */ + +import path from 'node:path'; + +const TEST_OR_FIXTURE_SEGMENTS = new Set(['__tests__', 'fixtures']); + +function isCoreProductionFile(filename) { + if (!filename || filename === '' || filename === '') return false; + const normalized = path.normalize(filename).replaceAll('\\', '/'); + const marker = 'packages/core/src/'; + const start = normalized.indexOf(marker); + if (start < 0) return false; + const relativePath = normalized.slice(start + marker.length); + const segments = relativePath.split('/'); + return !segments.some((segment) => TEST_OR_FIXTURE_SEGMENTS.has(segment)) && + !/\.test\.[cm]?[jt]sx?$/.test(relativePath); +} + +function resolvesToCoreRootBarrel(filename, importedPath) { + if (!importedPath.startsWith('.')) return false; + const normalized = path.normalize(filename).replaceAll('\\', '/'); + const marker = 'packages/core/src/'; + const sourceRoot = path.resolve( + normalized.slice(0, normalized.indexOf(marker) + marker.length), + ); + const resolvedImport = path.resolve(path.dirname(filename), importedPath); + const relativeToSource = path.relative(sourceRoot, resolvedImport); + return relativeToSource === 'index.js' || relativeToSource === 'index.ts'; +} + +export default { + meta: { + type: 'problem', + docs: { + description: + 'Disallow core production modules from importing the core root barrel.', + }, + schema: [], + messages: { + noCoreRootBarrelImport: + 'Core production modules must import symbols from their direct owner modules, not ../index.js.', + }, + }, + + create(context) { + const filename = context.filename; + if (!isCoreProductionFile(filename)) { + return {}; + } + + function checkSource(node) { + if ( + node.source && + typeof node.source.value === 'string' && + resolvesToCoreRootBarrel(filename, node.source.value) + ) { + context.report({ + node: node.source, + messageId: 'noCoreRootBarrelImport', + }); + } + } + + return { + ImportDeclaration: checkSource, + ExportNamedDeclaration: checkSource, + ExportAllDeclaration: checkSource, + ImportExpression(node) { + if (node.source.type === 'Literal') checkSource(node); + }, + }; + }, +}; diff --git a/eslint.config.js b/eslint.config.js index 08f87d9ee30..012e3327718 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,7 @@ import globals from 'globals'; // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format import storybook from 'eslint-plugin-storybook'; import checkFile from 'eslint-plugin-check-file'; +import noCoreRootBarrelImport from './eslint-rules/no-core-root-barrel-import.js'; import { legacyFilenames } from './eslint.legacy-filenames.mjs'; export default tseslint.config( @@ -174,6 +175,22 @@ export default tseslint.config( 'default-case': 'error', }, }, + { + files: ['packages/core/src/**/*.{ts,tsx}'], + ignores: [ + 'packages/core/src/**/*.test.{ts,tsx}', + 'packages/core/src/**/__tests__/**', + 'packages/core/src/**/fixtures/**', + ], + plugins: { + architecture: { + rules: { 'no-core-root-barrel-import': noCoreRootBarrelImport }, + }, + }, + rules: { + 'architecture/no-core-root-barrel-import': 'error', + }, + }, { files: [ 'packages/web-shell/client/**/*.{ts,tsx}', diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index 54f53613c18..4fa9a8ba2c6 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -4,21 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { ToolCallRequestInfo, ToolCallResponseInfo } from './turn.js'; import type { - ToolCallRequestInfo, - ToolCallResponseInfo, ToolCallConfirmationDetails, ToolResult, ToolResultDisplay, - ToolRegistry, - EditorType, - Config, ToolConfirmationPayload, AnyDeclarativeTool, AnyToolInvocation, - ChatRecordingService, ToolArtifact, -} from '../index.js'; +} from '../tools/tools.js'; +import type { EditorType } from '../utils/editor.js'; +import type { Config } from '../config/config.js'; +import type { ToolRegistry } from '../tools/tool-registry.js'; +import type { ChatRecordingService } from '../services/chatRecordingService.js'; import { createDebugLogger } from '../utils/debugLogger.js'; import { sanitizeToolNameForProvider } from '../utils/tool-name-utils.js'; import { compactToolResultDisplayForHistory } from '../utils/toolResultDisplayCompaction.js'; @@ -44,15 +43,12 @@ import { finalizeToolResponses, toolResponseTextLength, } from '../utils/tool-response-finalizer.js'; -import { - ToolConfirmationOutcome, - ApprovalMode, - logToolCall, - ToolErrorType, - ToolCallEvent, - InputFormat, - Kind, -} from '../index.js'; +import { ToolConfirmationOutcome, Kind } from '../tools/tools.js'; +import { ApprovalMode } from '../config/approval-mode.js'; +import { logToolCall } from '../telemetry/loggers.js'; +import { ToolCallEvent } from '../telemetry/types.js'; +import { InputFormat } from '../output/types.js'; +import { ToolErrorType } from '../tools/tool-error.js'; import type { FunctionResponse, FunctionResponsePart, diff --git a/packages/core/src/core/nonInteractiveToolExecutor.ts b/packages/core/src/core/nonInteractiveToolExecutor.ts index 06393c29f56..f0dbf769b79 100644 --- a/packages/core/src/core/nonInteractiveToolExecutor.ts +++ b/packages/core/src/core/nonInteractiveToolExecutor.ts @@ -4,11 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { - ToolCallRequestInfo, - ToolCallResponseInfo, - Config, -} from '../index.js'; +import type { ToolCallRequestInfo, ToolCallResponseInfo } from './turn.js'; +import type { Config } from '../config/config.js'; import { CoreToolScheduler, type AllToolCallsCompleteHandler, diff --git a/packages/core/src/core/permissionFlow.ts b/packages/core/src/core/permissionFlow.ts index a1a9cafdafa..f2cb3dbb656 100644 --- a/packages/core/src/core/permissionFlow.ts +++ b/packages/core/src/core/permissionFlow.ts @@ -19,8 +19,10 @@ * `invocation.getConfirmationDetails()`. */ -import type { AnyToolInvocation, Config } from '../index.js'; -import { ApprovalMode, ToolNames } from '../index.js'; +import type { AnyToolInvocation } from '../tools/tools.js'; +import type { Config } from '../config/config.js'; +import { ApprovalMode } from '../config/approval-mode.js'; +import { ToolNames } from '../tools/tool-names.js'; import { buildPermissionCheckContext, evaluatePermissionRules, diff --git a/packages/core/src/extension/extensionManager.test.ts b/packages/core/src/extension/extensionManager.test.ts index 29fe55f582f..73c3c219c2b 100644 --- a/packages/core/src/extension/extensionManager.test.ts +++ b/packages/core/src/extension/extensionManager.test.ts @@ -81,6 +81,9 @@ const mockLogExtensionDisable = vi.hoisted(() => vi.fn()); const mockLogExtensionUpdateEvent = vi.hoisted(() => vi.fn()); vi.mock('../telemetry/loggers.js', () => ({ logExtensionEnable: mockLogExtensionEnable, + logExtensionInstallEvent: mockLogExtensionInstallEvent, + logExtensionUninstall: mockLogExtensionUninstall, + logExtensionDisable: mockLogExtensionDisable, logExtensionUpdateEvent: mockLogExtensionUpdateEvent, })); diff --git a/packages/core/src/extension/extensionManager.ts b/packages/core/src/extension/extensionManager.ts index 05ecb4bfbd6..f82bc0c60cb 100644 --- a/packages/core/src/extension/extensionManager.ts +++ b/packages/core/src/extension/extensionManager.ts @@ -4,22 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { - MCPServerConfig, - ExtensionInstallMetadata, - SkillConfig, - SubagentConfig, - ClaudeMarketplaceConfig, -} from '../index.js'; +import type { MCPServerConfig, ExtensionInstallMetadata } from '../config/config.js'; +import { Config } from '../config/config.js'; +import type { SkillConfig } from '../skills/types.js'; +import type { SubagentConfig } from '../subagents/types.js'; +import type { ClaudeMarketplaceConfig } from './claude-converter.js'; import type { HookEventName, HookDefinition } from '../hooks/types.js'; +import { Storage } from '../config/storage.js'; import { - Storage, - Config, logExtensionEnable, logExtensionInstallEvent, logExtensionUninstall, logExtensionDisable, -} from '../index.js'; +} from '../telemetry/loggers.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index 8487f178253..9b51e6bc95f 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { AnyToolInvocation } from '../index.js'; +import type { AnyToolInvocation } from '../tools/tools.js'; import type { Config } from '../config/config.js'; import os from 'node:os'; import path from 'node:path'; diff --git a/packages/core/src/utils/tool-utils.ts b/packages/core/src/utils/tool-utils.ts index 251b00f3e50..836c9898c1e 100644 --- a/packages/core/src/utils/tool-utils.ts +++ b/packages/core/src/utils/tool-utils.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { AnyDeclarativeTool, AnyToolInvocation } from '../index.js'; -import { isTool } from '../index.js'; +import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js'; +import { isTool } from '../tools/tools.js'; import { ToolNames, ToolDisplayNames, diff --git a/scripts/tests/no-core-root-barrel-config.test.js b/scripts/tests/no-core-root-barrel-config.test.js new file mode 100644 index 00000000000..e69d80cb305 --- /dev/null +++ b/scripts/tests/no-core-root-barrel-config.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { ESLint } from 'eslint'; + +describe('core root barrel flat-config integration', () => { + it('reports a production self-import and ignores tests', async () => { + const eslint = new ESLint({ cwd: process.cwd(), overrideConfigFile: 'eslint.config.js' }); + const [production, test] = await eslint.lintText( + "import value from '../index.js';", + { filePath: 'packages/core/src/core/fixture-boundary.ts' }, + ).then(async (results) => [ + results, + eslint.lintText("import value from '../index.js';", { + filePath: 'packages/core/src/core/fixture-boundary.test.ts', + }), + ]); + expect( + production[0].messages.some( + (message) => message.ruleId === 'architecture/no-core-root-barrel-import', + ), + ).toBe(true); + expect( + (await test).some((result) => + result.messages.some((message) => message.ruleId === 'architecture/no-core-root-barrel-import'), + ), + ).toBe(false); + }); +}); diff --git a/scripts/tests/no-core-root-barrel-import.test.js b/scripts/tests/no-core-root-barrel-import.test.js new file mode 100644 index 00000000000..a60b196fa75 --- /dev/null +++ b/scripts/tests/no-core-root-barrel-import.test.js @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { Linter } from 'eslint'; +import rule from '../../eslint-rules/no-core-root-barrel-import.js'; + +function runRule(code, filename) { + const linter = new Linter({ configType: 'eslintrc' }); + linter.defineRule('architecture/no-core-root-barrel-import', rule); + return linter.verify( + code, + { + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + rules: { 'architecture/no-core-root-barrel-import': 'error' }, + }, + { filename }, + ); +} + +describe('no-core-root-barrel-import', () => { + it.each([ + ['packages/core/src/core/client.ts', '../index.js'], + ['packages/core/src/core/deep/module.ts', '../../index.js'], + ['packages/core/src/a/b/c/module.ts', '../../../index.js'], + ])('rejects root barrel imports at depth %s', (filename, importedPath) => { + expect(runRule(`import value from '${importedPath}';`, filename)).toHaveLength(1); + }); + + it('rejects export and dynamic root barrel sources', () => { + expect(runRule("export { value } from '../index.js';", 'packages/core/src/core/client.ts')).toHaveLength(1); + expect(runRule("export * from '../index.js';", 'packages/core/src/core/client.ts')).toHaveLength(1); + expect(runRule("import('../index.js');", 'packages/core/src/core/client.ts')).toHaveLength(1); + }); + + it('allows tests, fixtures, and non-core consumers', () => { + expect(runRule("import value from '../index.js';", 'packages/core/src/core/client.test.ts')).toHaveLength(0); + expect(runRule("import value from '../../index.js';", 'packages/core/src/fixtures/client.ts')).toHaveLength(0); + expect(runRule("import value from '@qwen-code/qwen-code-core';", 'packages/cli/src/index.ts')).toHaveLength(0); + }); + + it('allows direct owner imports', () => { + expect(runRule("import value from '../tools/tools.js';", 'packages/core/src/core/client.ts')).toHaveLength(0); + }); +});