Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions eslint-rules/no-core-root-barrel-import.js
Original file line number Diff line number Diff line change
@@ -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 === '<input>' || filename === '<text>') 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);
},
};
},
};
17 changes: 17 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}',
Expand Down
28 changes: 12 additions & 16 deletions packages/core/src/core/coreToolScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions packages/core/src/core/nonInteractiveToolExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/core/permissionFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/extension/extensionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));

Expand Down
17 changes: 7 additions & 10 deletions packages/core/src/extension/extensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/shell-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/tool-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions scripts/tests/no-core-root-barrel-config.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
42 changes: 42 additions & 0 deletions scripts/tests/no-core-root-barrel-import.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});