feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
354
frontend/infra/eslint-plugin/README.md
Normal file
354
frontend/infra/eslint-plugin/README.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# @coze-arch/eslint-plugin
|
||||
|
||||
A comprehensive ESLint plugin designed for Flow applications, providing essential linting rules for code quality, import management, and Zustand state management best practices.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Rules
|
||||
- **Import Management**: Prevent deep relative imports and batch import/export issues
|
||||
- **Code Quality**: Enforce function length limits, proper error handling, and catch block usage
|
||||
- **Package Management**: Validate package.json structure and dependencies
|
||||
- **React/TSX**: Prevent leaked renders and other React-specific issues
|
||||
|
||||
### Zustand Rules
|
||||
- **State Management**: Enforce proper state mutation patterns and store conventions
|
||||
- **Performance**: Optimize selector usage and prevent unnecessary re-renders
|
||||
- **Best Practices**: Enforce naming conventions and proper store typing
|
||||
|
||||
### Processors
|
||||
- **JSON Processor**: Custom processor for linting package.json files
|
||||
|
||||
## Get Started
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install the package
|
||||
rush update
|
||||
|
||||
# Or using pnpm in workspace
|
||||
pnpm add @coze-arch/eslint-plugin@workspace:*
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Add the plugin to your ESLint configuration:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import flowPlugin from '@coze-arch/eslint-plugin';
|
||||
|
||||
export default [
|
||||
{
|
||||
plugins: {
|
||||
'@coze-arch': flowPlugin,
|
||||
},
|
||||
rules: {
|
||||
'@coze-arch/no-deep-relative-import': ['error', { max: 4 }],
|
||||
'@coze-arch/max-line-per-function': ['error', { max: 150 }],
|
||||
'@coze-arch/tsx-no-leaked-render': 'warn',
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Using Recommended Configuration
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import flowPlugin from '@coze-arch/eslint-plugin';
|
||||
|
||||
export default [
|
||||
...flowPlugin.configs.recommended,
|
||||
];
|
||||
```
|
||||
|
||||
### Zustand Rules
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import zustandPlugin from '@coze-arch/eslint-plugin/zustand';
|
||||
|
||||
export default [
|
||||
{
|
||||
plugins: {
|
||||
'@coze-arch/zustand': zustandPlugin,
|
||||
},
|
||||
...zustandPlugin.configs.recommended,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Rules
|
||||
|
||||
#### `no-deep-relative-import`
|
||||
Prevents excessive relative import nesting.
|
||||
|
||||
```js
|
||||
// ❌ Bad (default max: 3)
|
||||
import something from '../../../deep/path';
|
||||
|
||||
// ✅ Good
|
||||
import something from '../../shallow/path';
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `max` (number): Maximum allowed relative path depth (default: 3)
|
||||
|
||||
#### `max-line-per-function`
|
||||
Enforces maximum lines per function.
|
||||
|
||||
```js
|
||||
// ❌ Bad (exceeds limit)
|
||||
function longFunction() {
|
||||
// ... 200 lines of code
|
||||
}
|
||||
|
||||
// ✅ Good
|
||||
function shortFunction() {
|
||||
// ... less than 150 lines
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `max` (number): Maximum lines per function (default: 150)
|
||||
|
||||
#### `tsx-no-leaked-render`
|
||||
Prevents leaked renders in TSX components.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
{count && <Component />} // count could be 0
|
||||
|
||||
// ✅ Good
|
||||
{count > 0 && <Component />}
|
||||
{Boolean(count) && <Component />}
|
||||
```
|
||||
|
||||
#### `no-pkg-dir-import`
|
||||
Prevents importing from package directories.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
import something from 'package/src/internal';
|
||||
|
||||
// ✅ Good
|
||||
import something from 'package';
|
||||
```
|
||||
|
||||
#### `use-error-in-catch`
|
||||
Enforces proper error handling in catch blocks.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
try {
|
||||
doSomething();
|
||||
} catch (e) {
|
||||
console.log('error occurred');
|
||||
}
|
||||
|
||||
// ✅ Good
|
||||
try {
|
||||
doSomething();
|
||||
} catch (error) {
|
||||
console.error('error occurred:', error);
|
||||
}
|
||||
```
|
||||
|
||||
#### `no-empty-catch`
|
||||
Prevents empty catch blocks.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
try {
|
||||
doSomething();
|
||||
} catch (error) {
|
||||
// empty
|
||||
}
|
||||
|
||||
// ✅ Good
|
||||
try {
|
||||
doSomething();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
```
|
||||
|
||||
#### `no-new-error`
|
||||
Discourages creating new Error instances.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
throw new Error('Something went wrong');
|
||||
|
||||
// ✅ Good (when configured)
|
||||
throw createError('Something went wrong');
|
||||
```
|
||||
|
||||
### Zustand Rules
|
||||
|
||||
#### `no-state-mutation`
|
||||
Prevents direct state mutation in Zustand stores.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
const state = useStore.getState();
|
||||
state.count = 5;
|
||||
|
||||
// ✅ Good
|
||||
useStore.setState({ count: 5 });
|
||||
```
|
||||
|
||||
#### `prefer-selector`
|
||||
Encourages using selectors for state access.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
const { count, name } = useStore();
|
||||
|
||||
// ✅ Good
|
||||
const count = useStore(state => state.count);
|
||||
const name = useStore(state => state.name);
|
||||
```
|
||||
|
||||
#### `store-name-convention`
|
||||
Enforces naming conventions for stores.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
const myStore = create(() => ({}));
|
||||
|
||||
// ✅ Good
|
||||
const useMyStore = create(() => ({}));
|
||||
```
|
||||
|
||||
#### `prefer-shallow`
|
||||
Encourages using shallow equality for object selections.
|
||||
|
||||
```js
|
||||
// ❌ Bad
|
||||
const { user, settings } = useStore(state => ({
|
||||
user: state.user,
|
||||
settings: state.settings
|
||||
}));
|
||||
|
||||
// ✅ Good
|
||||
const { user, settings } = useStore(
|
||||
state => ({ user: state.user, settings: state.settings }),
|
||||
shallow
|
||||
);
|
||||
```
|
||||
|
||||
### Package.json Rules
|
||||
|
||||
#### `package-require-author`
|
||||
Ensures package.json has an author field.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-package",
|
||||
"author": "developer@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
#### `package-disallow-deps`
|
||||
Prevents usage of disallowed dependencies (configurable).
|
||||
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
rush update
|
||||
|
||||
# Run tests
|
||||
rushx test
|
||||
|
||||
# Run with coverage
|
||||
rushx test:cov
|
||||
|
||||
# Lint code
|
||||
rushx lint
|
||||
|
||||
# Build (no-op for this package)
|
||||
rushx build
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Main plugin entry
|
||||
├── processors/
|
||||
│ └── json.ts # JSON processor for package.json
|
||||
├── rules/ # Core ESLint rules
|
||||
│ ├── no-deep-relative-import/
|
||||
│ ├── max-lines-per-function/
|
||||
│ ├── tsx-no-leaked-render/
|
||||
│ └── ...
|
||||
└── zustand/ # Zustand-specific rules
|
||||
├── index.ts # Zustand plugin entry
|
||||
└── rules/
|
||||
├── no-state-mutation/
|
||||
├── prefer-selector/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Adding New Rules
|
||||
|
||||
1. Create a new directory under `src/rules/` or `src/zustand/rules/`
|
||||
2. Implement the rule in `index.ts`
|
||||
3. Add comprehensive tests in `index.test.ts`
|
||||
4. Export the rule in the main plugin file
|
||||
5. Add the rule to recommended configuration if appropriate
|
||||
|
||||
### Testing
|
||||
|
||||
Tests are written using ESLint's `RuleTester`:
|
||||
|
||||
```ts
|
||||
import { RuleTester } from 'eslint';
|
||||
import { myRule } from './index';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('my-rule', myRule, {
|
||||
valid: [
|
||||
// Valid code examples
|
||||
],
|
||||
invalid: [
|
||||
// Invalid code examples with expected errors
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime Dependencies
|
||||
- `@typescript-eslint/utils` - TypeScript ESLint utilities
|
||||
- `eslint-module-utils` - ESLint module resolution utilities
|
||||
- `eslint-rule-composer` - Rule composition utilities
|
||||
- `eslint-traverse` - AST traversal utilities
|
||||
- `eslint-utils` - General ESLint utilities
|
||||
- `semver` - Semantic versioning utilities
|
||||
|
||||
### Development Dependencies
|
||||
- `@typescript-eslint/rule-tester` - Rule testing utilities
|
||||
- `vitest` - Test runner
|
||||
- `eslint` - ESLint core
|
||||
- TypeScript and various ESLint plugins for development
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0 License
|
||||
|
||||
## Author
|
||||
|
||||
fanwenjie.fe@bytedance.com
|
||||
|
||||
---
|
||||
|
||||
For more information about ESLint plugin development, see the [ESLint Plugin Developer Guide](https://eslint.org/docs/developer-guide/).
|
||||
12
frontend/infra/eslint-plugin/config/rush-project.json
Normal file
12
frontend/infra/eslint-plugin/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
46
frontend/infra/eslint-plugin/eslint.config.js
Normal file
46
frontend/infra/eslint-plugin/eslint.config.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const js = require('@eslint/js');
|
||||
const tsParser = require('@typescript-eslint/parser');
|
||||
const tsPlugin = require('@typescript-eslint/eslint-plugin');
|
||||
const importPlugin = require('eslint-plugin-import');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ['**/*.json', 'dist'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
vi: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
'import/order': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tsPlugin,
|
||||
},
|
||||
rules: {
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
...tsPlugin.configs.stylistic.rules,
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'arrow-body-style': 'off',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
49
frontend/infra/eslint-plugin/package.json
Normal file
49
frontend/infra/eslint-plugin/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@coze-arch/eslint-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "eslint plugin for flow app",
|
||||
"license": "Apache-2.0",
|
||||
"author": "fanwenjie.fe@bytedance.com",
|
||||
"exports": {
|
||||
".": "./src/index.js",
|
||||
"./zustand": "./src/zustand/index.js"
|
||||
},
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"dev": "npm run build -- -w",
|
||||
"lint": "eslint ./src --cache --quiet",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "~8.10.0",
|
||||
"eslint-module-utils": "~2.8.1",
|
||||
"eslint-rule-composer": "~0.3.0",
|
||||
"eslint-traverse": "^1.0.0",
|
||||
"eslint-utils": "~3.0.0",
|
||||
"semver": "^7.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "~7.25.8",
|
||||
"@babel/eslint-plugin": "~7.25.7",
|
||||
"@eslint/js": "~9.12.0",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@types/eslint": "~9.6.1",
|
||||
"@types/estree": "^1.0.1",
|
||||
"@types/node": "^18",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "~8.17.0",
|
||||
"@typescript-eslint/rule-tester": "~8.10.0",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"eslint": "~9.12.0",
|
||||
"eslint-config-prettier": "~9.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-prettier": "~5.2.1",
|
||||
"eslint-plugin-react": "~7.37.1",
|
||||
"eslint-plugin-unicorn": "48.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"vitest": "~3.0.5"
|
||||
}
|
||||
}
|
||||
8
frontend/infra/eslint-plugin/src/index.js
Normal file
8
frontend/infra/eslint-plugin/src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
require('ts-node').register({
|
||||
transpileOnly: true,
|
||||
cwd: __dirname,
|
||||
options: {},
|
||||
})
|
||||
|
||||
const { flowPreset } = require('./index.ts')
|
||||
module.exports = flowPreset
|
||||
89
frontend/infra/eslint-plugin/src/index.ts
Normal file
89
frontend/infra/eslint-plugin/src/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { jsonParser } from './processors/json';
|
||||
import { disallowDepRule } from './rules/package-disallow-deps';
|
||||
import { noDeepRelativeImportRule } from './rules/no-deep-relative-import';
|
||||
import { noDuplicatedDepsRule } from './rules/no-duplicated-deps';
|
||||
import { requireAuthorRule } from './rules/package-require-author';
|
||||
import { maxLinePerFunctionRule } from './rules/max-lines-per-function';
|
||||
import { noNewErrorRule } from './rules/no-new-error';
|
||||
import { noBatchImportOrExportRule } from './rules/no-batch-import-or-export';
|
||||
import { useErrorInCatch } from './rules/use-error-in-catch';
|
||||
import { noEmptyCatch } from './rules/no-empty-catch';
|
||||
import { noPkgDirImport } from './rules/no-pkg-dir-import';
|
||||
import { tsxNoLeakedRender } from './rules/tsx-no-leaked-render';
|
||||
|
||||
export const flowPreset = {
|
||||
rules: {
|
||||
'package-require-author': requireAuthorRule,
|
||||
'package-disallow-deps': disallowDepRule,
|
||||
'no-deep-relative-import': noDeepRelativeImportRule,
|
||||
'no-duplicated-deps': noDuplicatedDepsRule,
|
||||
'max-line-per-function': maxLinePerFunctionRule,
|
||||
'no-new-error': noNewErrorRule,
|
||||
'no-batch-import-or-export': noBatchImportOrExportRule,
|
||||
'no-empty-catch': noEmptyCatch,
|
||||
'use-error-in-catch': useErrorInCatch,
|
||||
'no-pkg-dir-import': noPkgDirImport,
|
||||
'tsx-no-leaked-render': tsxNoLeakedRender,
|
||||
},
|
||||
configs: {
|
||||
recommended: [
|
||||
{
|
||||
rules: {
|
||||
'@coze-arch/tsx-no-leaked-render': 'warn',
|
||||
'@coze-arch/no-pkg-dir-import': 'error',
|
||||
'@coze-arch/no-duplicated-deps': 'error',
|
||||
// 不允许超过 4 层的相对应用
|
||||
'@coze-arch/no-deep-relative-import': [
|
||||
'error',
|
||||
{
|
||||
max: 4,
|
||||
},
|
||||
],
|
||||
'@coze-arch/package-require-author': 'error',
|
||||
// 函数代码行不要超过 150
|
||||
'@coze-arch/max-line-per-function': [
|
||||
'error',
|
||||
{
|
||||
max: 150,
|
||||
},
|
||||
],
|
||||
'@coze-arch/no-new-error': 'off',
|
||||
'@coze-arch/no-batch-import-or-export': 'error',
|
||||
'@coze-arch/no-empty-catch': 'error',
|
||||
'@coze-arch/use-error-in-catch': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['package.json'],
|
||||
processor: '@coze-arch/json-processor',
|
||||
rules: {
|
||||
// TODO: 需要重构为直接解析json,否则全局规则都会对processor处理后的文件`package.js`生效.
|
||||
//https://github.com/eslint/json
|
||||
'@coze-arch/package-require-author': 'error',
|
||||
'@coze-arch/package-disallow-deps': 'error',
|
||||
// 关闭prettier规则,因为该规则lint package.js存在bug
|
||||
'prettier/prettier': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
processors: {
|
||||
'json-processor': jsonParser,
|
||||
},
|
||||
};
|
||||
39
frontend/infra/eslint-plugin/src/processors/json.ts
Normal file
39
frontend/infra/eslint-plugin/src/processors/json.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const prefix = 'module.exports = ';
|
||||
|
||||
export const jsonParser = {
|
||||
_prefix: prefix,
|
||||
supportsAutofix: false,
|
||||
preprocess(text: string) {
|
||||
return [`${prefix}${text}`];
|
||||
},
|
||||
postprocess(messages /* , fileName */) {
|
||||
return messages.reduce((total, next) => {
|
||||
// disable js rules running on json files
|
||||
// this becomes too noisey, and splitting js and json
|
||||
// into separate overrides so neither inherit the other
|
||||
// is lame
|
||||
// revisit once https://github.com/eslint/rfcs/pull/9 lands
|
||||
// return total.concat(next);
|
||||
|
||||
return total.concat(
|
||||
next.filter(error => error.ruleId?.startsWith('@coze-arch/')),
|
||||
);
|
||||
}, []);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,546 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { maxLinePerFunctionRule } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
ruleTester.run('max-lines-per-function', maxLinePerFunctionRule, {
|
||||
valid: [
|
||||
// Test code in global scope doesn't count
|
||||
{
|
||||
code: 'var x = 5;\nvar x = 2;\n',
|
||||
options: [{ max: 1 }],
|
||||
},
|
||||
|
||||
// Test single line standalone function
|
||||
{
|
||||
code: 'function name() {}',
|
||||
options: [{ max: 1 }],
|
||||
},
|
||||
|
||||
// Test standalone function with lines of code
|
||||
{
|
||||
code: 'function name() {\nvar x = 5;\nvar x = 2;\n}',
|
||||
options: [{ max: 4 }],
|
||||
},
|
||||
|
||||
// Test inline arrow function
|
||||
{
|
||||
code: 'const bar = () => 2',
|
||||
options: [{ max: 1 }],
|
||||
},
|
||||
|
||||
// Test arrow function
|
||||
{
|
||||
code: 'const bar = () => {\nconst x = 2 + 1;\nreturn x;\n}',
|
||||
options: [{ max: 4 }],
|
||||
},
|
||||
|
||||
// skipBlankLines: false with simple standalone function
|
||||
{
|
||||
code: 'function name() {\nvar x = 5;\n\t\n \n\nvar x = 2;\n}',
|
||||
options: [{ max: 7 }],
|
||||
},
|
||||
// single line comment
|
||||
{
|
||||
code: "function name() {\nvar x = 5;\n// a comment on it's own line\nvar x = 2; // end of line comment\n}",
|
||||
options: [{ max: 5 }],
|
||||
},
|
||||
|
||||
// multiple different comment types
|
||||
{
|
||||
code: 'function name() {\nvar x = 5;\n/* a \n multi \n line \n comment \n*/\n\nvar x = 2; // end of line comment\n}',
|
||||
options: [{ max: 10 }],
|
||||
},
|
||||
|
||||
// with multiple different comment types, including trailing and leading whitespace
|
||||
{
|
||||
code: 'function name() {\nvar x = 5;\n\t/* a comment with leading whitespace */\n/* a comment with trailing whitespace */\t\t\n\t/* a comment with trailing and leading whitespace */\t\t\n/* a \n multi \n line \n comment \n*/\t\t\n\nvar x = 2; // end of line comment\n}',
|
||||
options: [{ max: 13 }],
|
||||
},
|
||||
|
||||
// Multiple params on separate lines test
|
||||
{
|
||||
code: `function foo(
|
||||
aaa = 1,
|
||||
bbb = 2,
|
||||
ccc = 3
|
||||
) {
|
||||
return aaa + bbb + ccc
|
||||
}`,
|
||||
options: [{ max: 7 }],
|
||||
},
|
||||
|
||||
// IIFE validity test
|
||||
{
|
||||
code: `(
|
||||
function
|
||||
()
|
||||
{
|
||||
}
|
||||
)
|
||||
()`,
|
||||
options: [{ max: 4 }],
|
||||
},
|
||||
|
||||
{
|
||||
code: `function parent() {
|
||||
var x = 0;
|
||||
function nested() {
|
||||
var y = 0;
|
||||
x = 2;
|
||||
}
|
||||
if ( x === y ) {
|
||||
x++;
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 10 }],
|
||||
},
|
||||
|
||||
// Class method validity test
|
||||
{
|
||||
code: `class foo {
|
||||
method() {
|
||||
let y = 10;
|
||||
let x = 20;
|
||||
return y + x;
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 5 }],
|
||||
},
|
||||
|
||||
// IIFEs
|
||||
{
|
||||
code: `(function(){
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let z = x + y;
|
||||
let foo = {};
|
||||
return bar;
|
||||
}());`,
|
||||
options: [{ max: 7 }],
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
// Test simple standalone function is recognized
|
||||
{
|
||||
code: 'function name() {\n}',
|
||||
options: [{ max: 1 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'name'", lineCount: 2, maxLines: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test anonymous function assigned to variable is recognized
|
||||
{
|
||||
code: 'var func = function() {\n}',
|
||||
options: [{ max: 1 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'func'", lineCount: 2, maxLines: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test arrow functions are recognized
|
||||
{
|
||||
code: 'const bar = () => {\nconst x = 2 + 1;\nreturn x;\n}',
|
||||
options: [{ max: 3 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "Arrow function 'bar'", lineCount: 4, maxLines: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test inline arrow functions are recognized
|
||||
{
|
||||
code: 'const bar = () =>\n 2',
|
||||
options: [{ max: 1 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "Arrow function 'bar'", lineCount: 2, maxLines: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test that option defaults work as expected
|
||||
{
|
||||
code: `() => {${'var foo\n'.repeat(150)}}`,
|
||||
options: [{}],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: 'Arrow function', lineCount: 151, maxLines: 150 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test skipBlankLines: false
|
||||
{
|
||||
code: 'function name() {\nvar x = 5;\n\t\n \n\nvar x = 2;\n}',
|
||||
options: [{ max: 6 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'name'", lineCount: 7, maxLines: 6 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test skipBlankLines: false with CRLF line endings
|
||||
{
|
||||
code: 'function name() {\r\nvar x = 5;\r\n\t\r\n \r\n\r\nvar x = 2;\r\n}',
|
||||
options: [{ max: 6 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'name'", lineCount: 7, maxLines: 6 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//
|
||||
{
|
||||
code: 'function name() {\nvar x = 5;\n\t\n \n\nvar x = 2;\n}',
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'name'", lineCount: 7, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// with CRLF line endings
|
||||
{
|
||||
code: 'function name() {\r\nvar x = 5;\r\n\t\r\n \r\n\r\nvar x = 2;\r\n}',
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'name'", lineCount: 7, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// for multiple types of comment
|
||||
{
|
||||
code: 'function name() { // end of line comment\nvar x = 5; /* mid line comment */\n\t// single line comment taking up whole line\n\t\n \n\nvar x = 2;\n}',
|
||||
options: [{ max: 6 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'name'", lineCount: 8, maxLines: 6 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test simple standalone function with params on separate lines
|
||||
{
|
||||
code: `function foo(
|
||||
aaa = 1,
|
||||
bbb = 2,
|
||||
ccc = 3
|
||||
) {
|
||||
return aaa + bbb + ccc
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'foo'", lineCount: 7, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test IIFE "function" keyword is included in the count
|
||||
{
|
||||
code: `(
|
||||
function
|
||||
()
|
||||
{
|
||||
}
|
||||
)
|
||||
()`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: 'function', lineCount: 4, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Test Generator
|
||||
{
|
||||
code: ` function* generator() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: {
|
||||
name: "generator function 'generator'",
|
||||
lineCount: 5,
|
||||
maxLines: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test nested functions are included in it's parent's function count.
|
||||
{
|
||||
code: `function parent() {
|
||||
var x = 0;
|
||||
function nested() {
|
||||
var y = 0;
|
||||
x = 2;
|
||||
}
|
||||
if ( x === y ) {
|
||||
x++;
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 9 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'parent'", lineCount: 10, maxLines: 9 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test nested functions are included in it's parent's function count.
|
||||
{
|
||||
code: `function parent() {
|
||||
var x = 0;
|
||||
function nested() {
|
||||
var y = 0;
|
||||
x = 2;
|
||||
}
|
||||
if ( x === y ) {
|
||||
x++;
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'parent'", lineCount: 10, maxLines: 2 },
|
||||
},
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "function 'nested'", lineCount: 4, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test regular methods are recognized
|
||||
{
|
||||
code: `class foo {
|
||||
method() {
|
||||
let y = 10;
|
||||
let x = 20;
|
||||
return y + x;
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "method 'method'", lineCount: 5, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test static methods are recognized
|
||||
{
|
||||
code: `class A {
|
||||
static foo (a) {
|
||||
return a
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "static method 'foo'", lineCount: 3, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test private methods are recognized
|
||||
{
|
||||
code: `class A {
|
||||
#privateMethod() {
|
||||
return "hello world";
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: {
|
||||
name: 'private method #privateMethod',
|
||||
lineCount: 3,
|
||||
maxLines: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test getters are recognized as properties
|
||||
{
|
||||
code: `var obj = {
|
||||
get
|
||||
foo
|
||||
() {
|
||||
return 1
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "getter 'foo'", lineCount: 5, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test setters are recognized as properties
|
||||
{
|
||||
code: `var obj = {
|
||||
set
|
||||
foo
|
||||
( val ) {
|
||||
this._foo = val;
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "setter 'foo'", lineCount: 5, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test computed property names
|
||||
{
|
||||
code: `class A {
|
||||
static
|
||||
[
|
||||
foo +
|
||||
bar
|
||||
]
|
||||
(a) {
|
||||
return a
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: 'static method', lineCount: 8, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Test computed property names with TemplateLiteral
|
||||
{
|
||||
code: `class A {
|
||||
static
|
||||
[
|
||||
\`s\`
|
||||
]
|
||||
() {
|
||||
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "static method 's'", lineCount: 7, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test computed property names with Literal
|
||||
{
|
||||
code: `class A {
|
||||
static
|
||||
"literal"
|
||||
() {
|
||||
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "static method 'literal'", lineCount: 5, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test computed property names with null
|
||||
{
|
||||
code: `class A {
|
||||
static
|
||||
null
|
||||
() {
|
||||
|
||||
}
|
||||
}`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: "static method 'null'", lineCount: 5, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Test the IIFEs option
|
||||
{
|
||||
code: `(function(){
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let z = x + y;
|
||||
let foo = {};
|
||||
return bar;
|
||||
}());`,
|
||||
options: [{ max: 2 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'exceed',
|
||||
data: { name: 'function', lineCount: 7, maxLines: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Rule } from 'eslint';
|
||||
|
||||
const getStaticStringValue = node => {
|
||||
switch (node.type) {
|
||||
case 'Literal':
|
||||
return String(node.value);
|
||||
case 'TemplateLiteral':
|
||||
if (node.expressions.length === 0 && node.quasis.length === 1) {
|
||||
return node.quasis[0].value.cooked;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
* 为什么需要这个判断,对于下面这种函数
|
||||
* ```
|
||||
* var obj1 = {
|
||||
* set
|
||||
* foo
|
||||
* ( val: any ) {
|
||||
* this.foo = val;
|
||||
* }
|
||||
* }
|
||||
*```
|
||||
* 如果不采用下面这个判断,函数判断将得到3,实际应该为5. 类似的还有
|
||||
* ```
|
||||
* //如果不采用下面这个判断,函数判断将得到3,实际应该为8
|
||||
* class A {
|
||||
static
|
||||
[
|
||||
foo +
|
||||
bar
|
||||
]
|
||||
(a) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
* ```
|
||||
*/
|
||||
const isEmbedded = node => {
|
||||
if (!node.parent) {
|
||||
return false;
|
||||
}
|
||||
if (node !== node.parent.value) {
|
||||
return false;
|
||||
}
|
||||
if (node.parent.type === 'MethodDefinition') {
|
||||
return true;
|
||||
}
|
||||
if (node.parent.type === 'Property') {
|
||||
return (
|
||||
node.parent.method === true ||
|
||||
node.parent.kind === 'get' ||
|
||||
node.parent.kind === 'set'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns function name
|
||||
* Q:为什么不直接用 node.id.value获取函数名称 ?
|
||||
* A:这种方式对于 传统的函数写法没问题,但是对于
|
||||
* const tips = {
|
||||
* fun: () => {}
|
||||
* };
|
||||
* 或者
|
||||
* const fun2 = () => {}
|
||||
* 方式书写函数得到的名称为null,所以采取下面这种方式获取,
|
||||
*
|
||||
*/
|
||||
|
||||
const getFunctionNameWithKind = node => {
|
||||
const { parent } = node;
|
||||
const tokens: string[] = [];
|
||||
|
||||
if (
|
||||
parent.type === 'MethodDefinition' ||
|
||||
parent.type === 'PropertyDefinition'
|
||||
) {
|
||||
// https://github.com/tc39/proposal-static-class-features
|
||||
if (parent.static) {
|
||||
tokens.push('static');
|
||||
}
|
||||
if (!parent.computed && parent.key.type === 'PrivateIdentifier') {
|
||||
tokens.push('private');
|
||||
}
|
||||
}
|
||||
if (node.async) {
|
||||
tokens.push('async');
|
||||
}
|
||||
if (node.generator) {
|
||||
tokens.push('generator');
|
||||
}
|
||||
|
||||
checkParentType(node, parent, tokens);
|
||||
return tokens.join(' ');
|
||||
};
|
||||
|
||||
const checkParentType = (node, parent, tokens) => {
|
||||
if (parent.type === 'Property' || parent.type === 'MethodDefinition') {
|
||||
if (parent.kind === 'constructor') {
|
||||
tokens.push('constructor');
|
||||
return;
|
||||
}
|
||||
if (parent.kind === 'get') {
|
||||
tokens.push('getter');
|
||||
} else if (parent.kind === 'set') {
|
||||
tokens.push('setter');
|
||||
} else {
|
||||
tokens.push('method');
|
||||
}
|
||||
} else if (parent.type === 'PropertyDefinition') {
|
||||
tokens.push('method');
|
||||
} else {
|
||||
if (node.type === 'ArrowFunctionExpression') {
|
||||
tokens.push('Arrow');
|
||||
}
|
||||
// VariableDeclarator
|
||||
tokens.push('function');
|
||||
}
|
||||
|
||||
getParentNodeName(node, parent, tokens);
|
||||
};
|
||||
|
||||
const getParentNodeName = (node, parent, tokens) => {
|
||||
if (
|
||||
parent.type === 'Property' ||
|
||||
parent.type === 'MethodDefinition' ||
|
||||
parent.type === 'PropertyDefinition' ||
|
||||
parent.type === 'CallExpression' ||
|
||||
parent.type === 'VariableDeclarator'
|
||||
) {
|
||||
if (!parent.computed && parent?.key?.type === 'PrivateIdentifier') {
|
||||
tokens.push(`#${parent.key.name}`);
|
||||
} else {
|
||||
const name = getStaticPropertyName(parent);
|
||||
|
||||
if (name !== null) {
|
||||
tokens.push(`'${name}'`);
|
||||
} else if (node.id) {
|
||||
tokens.push(`'${node.id.name}'`);
|
||||
}
|
||||
}
|
||||
} else if (node.id) {
|
||||
tokens.push(`'${node.id.name}'`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStaticPropertyName = node => {
|
||||
let prop;
|
||||
|
||||
switch (node?.type) {
|
||||
case 'ChainExpression':
|
||||
return getStaticPropertyName(node.expression);
|
||||
|
||||
case 'Property':
|
||||
case 'PropertyDefinition':
|
||||
case 'MethodDefinition':
|
||||
prop = node.key;
|
||||
break;
|
||||
|
||||
case 'MemberExpression':
|
||||
prop = node.property;
|
||||
break;
|
||||
case 'VariableDeclarator':
|
||||
prop = node.id;
|
||||
break;
|
||||
//TODO: CallExpression 场景较为复杂,目前应该没有完全覆盖
|
||||
case 'CallExpression':
|
||||
prop = node.callee;
|
||||
break;
|
||||
|
||||
// no default
|
||||
}
|
||||
|
||||
if (prop) {
|
||||
if (prop.type === 'Identifier' && !node.computed) {
|
||||
return prop.name;
|
||||
}
|
||||
|
||||
return getStaticStringValue(prop);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const maxLinePerFunctionRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce a maximum number of lines of code in a function',
|
||||
recommended: false,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
max: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
exceed:
|
||||
'{{name}} has too many lines ({{lineCount}}). Maximum is {{maxLines}}.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const maxLines = options.max || 150;
|
||||
|
||||
function checkFunctionLength(funcNode) {
|
||||
const node = isEmbedded(funcNode) ? funcNode.parent : funcNode;
|
||||
|
||||
// 针对函数声明,函数表达式,箭头函数,函数定义四种类型
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression' ||
|
||||
node.type === 'MethodDefinition' ||
|
||||
node.type === 'Property'
|
||||
) {
|
||||
const lineCount = node.loc.end.line - node.loc.start.line + 1;
|
||||
|
||||
const name = getFunctionNameWithKind(node.value || node);
|
||||
if (lineCount > maxLines) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'exceed',
|
||||
data: {
|
||||
name,
|
||||
lineCount: lineCount.toString(),
|
||||
maxLines: maxLines.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkFunctionLength,
|
||||
FunctionExpression: checkFunctionLength,
|
||||
ArrowFunctionExpression: checkFunctionLength,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { noBatchImportOrExportRule } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
ruleTester.run('no-batch-import-or-export', noBatchImportOrExportRule, {
|
||||
valid: [
|
||||
{ code: 'import { foo } from "someModule"' },
|
||||
{ code: 'import foo from "someModule"' },
|
||||
{ code: 'export { foo } from "someModule"' },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import * as foo from "someModule"',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'avoidUseBatchImport',
|
||||
data: { code: 'import * as foo from "someModule"' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'export * from "someModule"',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'avoidUseBatchExport',
|
||||
data: { code: 'export * from "someModule"' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'export * as foo from "someModule"',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'avoidUseBatchExport',
|
||||
data: { code: 'export * as foo from "someModule"' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Rule } from 'eslint';
|
||||
|
||||
export const noBatchImportOrExportRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disable batch import or export.',
|
||||
},
|
||||
messages: {
|
||||
avoidUseBatchExport: 'Avoid use batch export: "{{ code }}".',
|
||||
avoidUseBatchImport: 'Avoid use batch import: "{{ code }}".',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
ExportAllDeclaration: node => {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'avoidUseBatchExport',
|
||||
data: {
|
||||
code: context.sourceCode.getText(node).toString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
ImportDeclaration: node => {
|
||||
node.specifiers.forEach(v => {
|
||||
if (v.type === 'ImportNamespaceSpecifier') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'avoidUseBatchImport',
|
||||
data: {
|
||||
code: context.sourceCode.getText(node),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { noDeepRelativeImportRule } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
ruleTester.run('no-deep-relative-import', noDeepRelativeImportRule, {
|
||||
valid: [
|
||||
'import "./abc"',
|
||||
'import "../abc"',
|
||||
'import "abc"',
|
||||
'require("./abc")',
|
||||
'require("../abc")',
|
||||
'require("abc")',
|
||||
'require(123)',
|
||||
'require(xabc)',
|
||||
'import("./abc")',
|
||||
'import("../abc")',
|
||||
'import("abc")',
|
||||
'import(123)',
|
||||
'import(xabc)',
|
||||
{
|
||||
code: 'import "../../../abc"',
|
||||
options: [{ max: 4 }],
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import "../../../abc"',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'max',
|
||||
data: { max: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'require("../../../abc")',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'max',
|
||||
data: { max: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'import("../../../abc")',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'max',
|
||||
data: { max: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'import "../../../../../abc"',
|
||||
options: [{ max: 4 }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'max',
|
||||
data: { max: 4 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Rule } from 'eslint';
|
||||
|
||||
const isTooDeep = (declare: string, maxLevel: number) => {
|
||||
const match = /^(\.\.\/)+/.exec(declare);
|
||||
if (match) {
|
||||
// 3 = '../'.length
|
||||
const deep = match[0].length / 3;
|
||||
if (deep >= maxLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const noDeepRelativeImportRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Detect how deep levels in import/require statments',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
max: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
max: "Don't import module exceed {{max}} times of '../'. You should use some alias to avoid such problem.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { max = 3 } = context.options[0] || {};
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (typeof node.source.value === 'string') {
|
||||
const declare = node.source.value.trim();
|
||||
if (isTooDeep(declare, max)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'max',
|
||||
data: { max },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (node.callee.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
if (node.callee.name !== 'require') {
|
||||
return;
|
||||
}
|
||||
if (node.arguments.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const arg = node.arguments[0];
|
||||
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
||||
const declare = arg.value.trim();
|
||||
if (isTooDeep(declare, max)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'max',
|
||||
data: { max },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
ImportExpression(node) {
|
||||
if (
|
||||
node.source.type === 'Literal' &&
|
||||
typeof node.source.value === 'string'
|
||||
) {
|
||||
const declare = node.source.value.trim();
|
||||
if (isTooDeep(declare, max)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'max',
|
||||
data: { max },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { jsonParser } from '../../processors/json';
|
||||
import { noDuplicatedDepsRule } from './index';
|
||||
|
||||
function preprocess(tests) {
|
||||
for (const type of Object.keys(tests)) {
|
||||
const item = tests[type];
|
||||
tests[type] = tests[type].map(item => {
|
||||
item.code = jsonParser.preprocess(item.code)[0];
|
||||
if (item.output) {
|
||||
item.output = jsonParser.preprocess(item.output)[0];
|
||||
}
|
||||
return item;
|
||||
});
|
||||
tests[type] = item;
|
||||
}
|
||||
return tests;
|
||||
}
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run(
|
||||
'no-duplicated-deps',
|
||||
noDuplicatedDepsRule,
|
||||
preprocess({
|
||||
valid: [
|
||||
{
|
||||
code: '{}',
|
||||
filename: 'xx/package.json',
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({ dependencies: {} }),
|
||||
filename: 'xx/package.json',
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({ dependencies: {}, devDependencies: {} }),
|
||||
filename: 'xx/package.json',
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: { a: '0.0.1', b: '1.0.0' },
|
||||
devDependencies: { c: '1.0.0' },
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: { a: '0.0.1' },
|
||||
devDependencies: { a: '1.0.0' },
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'no-duplicated',
|
||||
data: { depName: 'a' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: { a: '0.0.1', b: '0.1.1', c: '0.1.0' },
|
||||
devDependencies: { a: '1.0.0', b: '0.1.1' },
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'no-duplicated',
|
||||
data: { depName: 'a' },
|
||||
},
|
||||
{
|
||||
messageId: 'no-duplicated',
|
||||
data: { depName: 'b' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
export const noDuplicatedDepsRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Don't repeat deps in package.json",
|
||||
},
|
||||
messages: {
|
||||
'no-duplicated': '发现重复声明的依赖:{{depName}},请更正。',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
if (path.basename(filename) !== 'package.json') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
AssignmentExpression(node) {
|
||||
const json = node.right;
|
||||
const { properties } = json as any;
|
||||
if (!properties) {
|
||||
return;
|
||||
}
|
||||
// 对比 dependencies 与 devDependencies 之间是否存在重复依赖
|
||||
const dependencies = properties.find(
|
||||
p => p.key.value === 'dependencies',
|
||||
);
|
||||
const devDependencies = properties.find(
|
||||
p => p.key.value === 'devDependencies',
|
||||
);
|
||||
|
||||
if (!dependencies || !devDependencies) {
|
||||
return;
|
||||
}
|
||||
const depValue = dependencies.value.properties;
|
||||
const devDepValue = devDependencies.value.properties;
|
||||
depValue.forEach(dep => {
|
||||
const duplicated = devDepValue.find(
|
||||
d => d.key.value === dep.key.value,
|
||||
);
|
||||
if (duplicated) {
|
||||
context.report({
|
||||
node: dep,
|
||||
messageId: 'no-duplicated',
|
||||
data: { depName: duplicated.key.value },
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { noEmptyCatch } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
ruleTester.run('no-empty-catch', noEmptyCatch, {
|
||||
valid: ['try{ foo }catch(e){ console.log(e) }', 'try{ foo }catch(e){ bar }'],
|
||||
invalid: [
|
||||
{
|
||||
code: 'try{ foo }catch(e){ /* */ }',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'no-empty',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'try{ foo }catch(e){}',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'no-empty',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `try{ foo }catch(e){
|
||||
//
|
||||
}`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'no-empty',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
export const noEmptyCatch: Rule.RuleModule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Catch error block should not be empty.',
|
||||
},
|
||||
messages: {
|
||||
'no-empty':
|
||||
'Catch 代码块中不可为空,否则可能导致错误信息没有得到有效关注',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
CatchClause(node) {
|
||||
for (const statement of node.body.body) {
|
||||
if (
|
||||
!['EmptyStatement', 'CommentBlock', 'CommentLine'].includes(
|
||||
statement.type,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'no-empty',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { noNewErrorRule } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
ruleTester.run('no-new-error', noNewErrorRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `(function(){
|
||||
class CustomError extends Error {
|
||||
constructor(eventName, msg) {
|
||||
super(msg);
|
||||
this.eventName = eventName;
|
||||
this.msg = msg;
|
||||
this.name = 'CustomError';
|
||||
}
|
||||
};
|
||||
new CustomError('copy_error', 'empty copy');
|
||||
})();`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'throw new Error("error message")',
|
||||
output: 'throw new CustomError(\'normal_error\', "error message")',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'no-new-error',
|
||||
data: { name: 'new Error', lineCount: 1, maxLines: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
48
frontend/infra/eslint-plugin/src/rules/no-new-error/index.ts
Normal file
48
frontend/infra/eslint-plugin/src/rules/no-new-error/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
export const noNewErrorRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: "Don't use new Error()",
|
||||
},
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
'no-new-error': 'found use new Error()',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
NewExpression(node) {
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'Error') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'no-new-error',
|
||||
fix(fixer) {
|
||||
const args = node.arguments.map(arg => context.sourceCode.getText(arg)).join(',') || '\'custom error\'';
|
||||
return fixer.replaceText(node, `new CustomError('normal_error', ${args})`);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path, { relative } from 'path';
|
||||
import { Rule } from 'eslint';
|
||||
import readPkgUp from 'eslint-module-utils/readPkgUp';
|
||||
import resolve from 'eslint-module-utils/resolve';
|
||||
import { exportPathMatch } from './utils';
|
||||
|
||||
export const noPkgDirImport: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'limit import package directory directly',
|
||||
},
|
||||
messages: {
|
||||
invalidSubpath:
|
||||
'subPath `{{ subPath }}` is NOT exported in `{{ pkg }}`, you can config the `exports` fields in package.json',
|
||||
noExportsCfg:
|
||||
"NO `exports` fields config in `{{ pkg }}` package.json, you can't import by subPath ",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = `${node.source.value}`;
|
||||
const modulePath = resolve(importPath, context);
|
||||
|
||||
if (!modulePath) {
|
||||
// 解析不到的情况,暂不处理
|
||||
return;
|
||||
}
|
||||
|
||||
const { pkg, path: importPkgPath } = readPkgUp({
|
||||
cwd: modulePath,
|
||||
}) as any;
|
||||
|
||||
const { path: currentPkgPath } = readPkgUp({
|
||||
cwd: context.filename,
|
||||
}) as any;
|
||||
|
||||
if (!pkg.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地link会解析到node_modules目录,需要拿到pkg name再次解析。
|
||||
const moduleRealPath = resolve(pkg.name, context);
|
||||
|
||||
if (
|
||||
// 包名称就是引用路径
|
||||
pkg.name === importPath ||
|
||||
// 解析到其他包,如@type
|
||||
!importPath.startsWith(pkg.name) ||
|
||||
// 解析到自己包的文件
|
||||
currentPkgPath === importPkgPath ||
|
||||
!moduleRealPath ||
|
||||
moduleRealPath.includes('node_modules')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pkg.exports) {
|
||||
context.report({
|
||||
messageId: 'noExportsCfg',
|
||||
data: {
|
||||
pkg: pkg.name,
|
||||
},
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
loc: node.loc,
|
||||
});
|
||||
} else if (pkg.exports) {
|
||||
if (typeof pkg.exports === 'string') {
|
||||
context.report({
|
||||
messageId: 'noExportsCfg',
|
||||
data: {
|
||||
pkg: pkg.name,
|
||||
},
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
loc: node.loc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const validSubPath = Object.keys(pkg.exports);
|
||||
if (
|
||||
!validSubPath.some(p => {
|
||||
const pkgExportPath = path.join(pkg.name, p);
|
||||
return exportPathMatch(importPath, pkgExportPath);
|
||||
})
|
||||
) {
|
||||
const subPath = relative(pkg.name, importPath);
|
||||
context.report({
|
||||
messageId: 'invalidSubpath',
|
||||
data: {
|
||||
subPath,
|
||||
pkg: pkg.name,
|
||||
},
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
loc: node.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import resolve from 'eslint-module-utils/resolve';
|
||||
import readPkgUp from 'eslint-module-utils/readPkgUp';
|
||||
import { noPkgDirImport } from '../index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
vi.mock('eslint-module-utils/resolve', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('eslint-module-utils/readPkgUp', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
const validCases = [
|
||||
{
|
||||
code: 'import "xxx"',
|
||||
modulePath: undefined, // modulePath 为 空
|
||||
moduleRealPath: undefined,
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: '',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'some/pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'some/pkg', // 包名称与引用路径相同
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'some/pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: undefined, // 解析到不规范配置的package.json
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: '@types/pkg', // 解析到类型包
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/same/pkg', // 相同路径
|
||||
currentPkgPath: 'path/to/same/pkg',
|
||||
pkg: {
|
||||
name: '@types/pkg',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: undefined,
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/node_modules/pkg', // 解析到node_modules
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: { subPath: './subPath' },
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/sub/path';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: { 'sub/*': './subPath' },
|
||||
},
|
||||
},
|
||||
].map(c => {
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.modulePath);
|
||||
|
||||
if (!c.modulePath) {
|
||||
return {
|
||||
code: c.code,
|
||||
// TODO: 避免eslint duplication检测。可能需要改为其他方式
|
||||
settings: c,
|
||||
};
|
||||
}
|
||||
|
||||
if (c.pkg.name) {
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.moduleRealPath);
|
||||
}
|
||||
|
||||
vi.mocked(readPkgUp)
|
||||
.mockReturnValueOnce({
|
||||
pkg: c.pkg,
|
||||
path: c.importPkgPath,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
path: c.currentPkgPath,
|
||||
});
|
||||
|
||||
return {
|
||||
code: c.code,
|
||||
settings: c,
|
||||
};
|
||||
});
|
||||
|
||||
const invalidCases = [
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: undefined, // 为空
|
||||
},
|
||||
messageId: 'noExportsCfg',
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: 'main.js', // isString
|
||||
},
|
||||
messageId: 'noExportsCfg',
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: { otherPath: 'otherPath' },
|
||||
},
|
||||
messageId: 'invalidSubpath',
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/sub/path';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: {
|
||||
sub: './sub',
|
||||
},
|
||||
},
|
||||
messageId: 'invalidSubpath',
|
||||
},
|
||||
].map(c => {
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.modulePath);
|
||||
|
||||
if (!c.modulePath) {
|
||||
return {
|
||||
settings: c,
|
||||
code: c.code,
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.moduleRealPath);
|
||||
|
||||
vi.mocked(readPkgUp)
|
||||
.mockReturnValueOnce({
|
||||
pkg: c.pkg,
|
||||
path: c.importPkgPath,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
path: c.currentPkgPath,
|
||||
});
|
||||
return {
|
||||
settings: c,
|
||||
code: c.code,
|
||||
errors: [
|
||||
{
|
||||
messageId: c.messageId,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
ruleTester.run('no-pkg-dir-import', noPkgDirImport, {
|
||||
valid: [...validCases],
|
||||
invalid: [...invalidCases],
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { exportPathMatch } from '../utils';
|
||||
|
||||
describe('exportPathMatch', () => {
|
||||
it.each([
|
||||
['./foo', './foo'],
|
||||
['./foo.js', './*'],
|
||||
['./foo.js', './*.js'],
|
||||
['./foo/baz', './foo/*'],
|
||||
['./foo/baz/baz.js', './foo/*'],
|
||||
])(
|
||||
'import path is %s, export path is %s, should be matched',
|
||||
(importPath, exportPath) => {
|
||||
expect(exportPathMatch(importPath, exportPath)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['./foo', './bar'],
|
||||
['./foo.js', './*.ts'],
|
||||
['./foo.js', './foo.ts'],
|
||||
['./baz/bar', './foo/*'],
|
||||
['./foo/bar/baz.js', './foo/*.js'],
|
||||
])(
|
||||
'import path is %s, export path is %s, should NOT be matched',
|
||||
(importPath, exportPath) => {
|
||||
expect(exportPathMatch(importPath, exportPath)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
export function exportPathMatch(importPath: string, pkgExportPath: string) {
|
||||
if (importPath === pkgExportPath) {
|
||||
return true;
|
||||
}
|
||||
const pkgExportBasename = path.basename(pkgExportPath);
|
||||
|
||||
if (importPath.startsWith(path.dirname(pkgExportPath))) {
|
||||
if (pkgExportBasename === '*') {
|
||||
return true;
|
||||
}
|
||||
if (path.dirname(importPath) === path.dirname(pkgExportPath)) {
|
||||
return pkgExportBasename === `*${path.extname(importPath)}`;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { jsonParser } from '../../processors/json';
|
||||
import { disallowDepRule } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
function preprocess(tests) {
|
||||
for (const type of Object.keys(tests)) {
|
||||
const item = tests[type];
|
||||
tests[type] = tests[type].map(item => {
|
||||
item.code = jsonParser.preprocess(item.code)[0];
|
||||
if (item.output) {
|
||||
item.output = jsonParser.preprocess(item.output)[0];
|
||||
}
|
||||
return item;
|
||||
});
|
||||
tests[type] = item;
|
||||
}
|
||||
return tests;
|
||||
}
|
||||
|
||||
ruleTester.run(
|
||||
'package-disallow-deps',
|
||||
disallowDepRule,
|
||||
preprocess({
|
||||
valid: [
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: {
|
||||
react: '^16.0.0',
|
||||
},
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({}),
|
||||
filename: 'xx/package.json',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: {
|
||||
react: '^16.0.0',
|
||||
},
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
options: [['react']],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'disallowDep',
|
||||
data: { dependence: 'react', tips: '' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: {
|
||||
react: '^16.0.0',
|
||||
},
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
options: [[['react', '<17', 'abc']]],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'disallowVersion',
|
||||
data: {
|
||||
dependence: 'react',
|
||||
version: '^16.0.0',
|
||||
blockVersion: '<17',
|
||||
tips: 'abc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: {
|
||||
react: '^16.0.0',
|
||||
'react-dom': '^16',
|
||||
},
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
options: [[['react', '<17'], 'react-dom']],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'disallowVersion',
|
||||
data: {
|
||||
dependence: 'react',
|
||||
version: '^16.0.0',
|
||||
tips: '',
|
||||
blockVersion: '<17',
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: 'disallowDep',
|
||||
data: { dependence: 'react-dom', tips: '' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: JSON.stringify({
|
||||
dependencies: {
|
||||
react: '^16.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'react-dom': '^16',
|
||||
},
|
||||
}),
|
||||
filename: 'xx/package.json',
|
||||
options: [[['react', '<17'], 'react-dom']],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'disallowVersion',
|
||||
data: {
|
||||
dependence: 'react',
|
||||
version: '^16.0.0',
|
||||
blockVersion: '<17',
|
||||
tips: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: 'disallowDep',
|
||||
data: { dependence: 'react-dom', tips: '' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { Rule } from 'eslint';
|
||||
import semver from 'semver';
|
||||
|
||||
type RuleOptions = Array<string | [string, string]>;
|
||||
|
||||
export const disallowDepRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: '禁止使用某些 npm 包',
|
||||
},
|
||||
messages: {
|
||||
disallowDep:
|
||||
"monorepo 内禁止使用 '{{dependence}}',建议寻找同类 package 替换.\n {{ tips }}",
|
||||
disallowVersion:
|
||||
"monorepo 内禁止使用 '{{dependence}}@{{version}}' 版本,请替换为 {{blockVersion}} 之外的版本号",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'array',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
if (path.basename(filename) !== 'package.json') {
|
||||
return {};
|
||||
}
|
||||
const blocklist = context.options[0] as RuleOptions;
|
||||
if (!Array.isArray(blocklist)) {
|
||||
return {};
|
||||
}
|
||||
const normalizeBlocklist = blocklist.map(r =>
|
||||
typeof r === 'string' ? [r] : r,
|
||||
);
|
||||
const detect = (dep: string, version: string, node) => {
|
||||
const definition = normalizeBlocklist.find(r => r[0] === dep);
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
const [, blockVersion, tips] = definition;
|
||||
// 没有提供 version 参数,判定为不允许所有版本号
|
||||
if (typeof blockVersion !== 'string' || blockVersion.length <= 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'disallowDep',
|
||||
data: {
|
||||
dependence: dep,
|
||||
tips: tips || '',
|
||||
},
|
||||
});
|
||||
} else if (semver.intersects(version, blockVersion)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'disallowVersion',
|
||||
data: {
|
||||
dependence: dep,
|
||||
blockVersion,
|
||||
version,
|
||||
tips: tips || '',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
AssignmentExpression(node) {
|
||||
const json = node.right;
|
||||
const depProps = ['devDependencies', 'dependencies'];
|
||||
(json as any).properties
|
||||
.filter(r => depProps.includes(r.key.value))
|
||||
.forEach(r => {
|
||||
const props = r.value.properties;
|
||||
props.forEach(p => {
|
||||
const dep = p.key.value;
|
||||
const version = p.value.value;
|
||||
detect(dep, version, p);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
// cp-disable-next-line
|
||||
const isBytedancer = name => name.endsWith('@bytedance.com');
|
||||
|
||||
export const requireAuthorRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'validate author & maintainer property in package.json',
|
||||
},
|
||||
messages: {
|
||||
requireAuthor:
|
||||
'package.json 文件必须提供 author 字段,这有助于正确生成 CODEOWNER 文件,帮助正确指定代码 reviewer',
|
||||
authorShouldBeBytedancer:
|
||||
// cp-disable-next-line
|
||||
'package.json 文件的 author 字段值应该为 `@bytedance.com` 结尾的邮箱名',
|
||||
maintainerShouldBeBytedancers:
|
||||
// cp-disable-next-line
|
||||
'package.json 文件的 maintainers 字段值应该为 `@bytedance.com` 结尾的邮箱名数组',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
if (path.basename(filename) !== 'package.json') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
AssignmentExpression(node) {
|
||||
const json = node.right;
|
||||
const authorProp = (json as any).properties.find(
|
||||
p => p.key.value === 'author',
|
||||
);
|
||||
if (!authorProp) {
|
||||
context.report({
|
||||
node: json,
|
||||
messageId: 'requireAuthor',
|
||||
});
|
||||
} else {
|
||||
const authorValue = authorProp.value;
|
||||
if (!isBytedancer(authorValue.value)) {
|
||||
context.report({
|
||||
node: authorValue,
|
||||
messageId: 'authorShouldBeBytedancer',
|
||||
data: { author: authorValue.value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const maintainerProp = (json as any).properties.find(
|
||||
p => p.key.value === 'maintainers',
|
||||
);
|
||||
if (maintainerProp) {
|
||||
const maintainers = maintainerProp.value;
|
||||
if (maintainers.elements?.some(p => !isBytedancer(p.value))) {
|
||||
context.report({
|
||||
node: maintainers,
|
||||
messageId: 'maintainerShouldBeBytedancers',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"include": ["react.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { RuleTester } from 'eslint';
|
||||
|
||||
import parser from '@typescript-eslint/parser';
|
||||
import { tsxNoLeakedRender } from '.';
|
||||
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser,
|
||||
parserOptions: {
|
||||
tsconfigRootDir: path.resolve(__dirname, './fixture'),
|
||||
project: path.resolve(__dirname, './fixture/tsconfig.json'),
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('tsx-no-leaked-render', tsxNoLeakedRender, {
|
||||
valid: [
|
||||
{
|
||||
code: 'const Foo = (isBar: string) => (<div data-bar={ isBar && "bar" } />);',
|
||||
filename: 'react.tsx',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ruleComposer from 'eslint-rule-composer';
|
||||
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
|
||||
const originRule = reactPlugin.rules['jsx-no-leaked-render'];
|
||||
|
||||
// 扩展react/jsx-no-leaked-render。增加判断 「&&」 表达式左边为 boolean 、 null 、 undefined TS类型,则不报错。
|
||||
export const tsxNoLeakedRender = ruleComposer.filterReports(
|
||||
originRule,
|
||||
problem => {
|
||||
const { parent } = problem.node;
|
||||
// 如果表达式是用于jsx属性,则不需要修复。 如 <Comp prop={ { foo: 1 } && obj } />
|
||||
if (
|
||||
parent?.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
||||
parent?.parent?.type === AST_NODE_TYPES.JSXAttribute
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { useErrorInCatch } from './index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
ruleTester.run('use-error-in-catch', useErrorInCatch, {
|
||||
valid: ['try{ foo }catch(e){ console.log(e) }'],
|
||||
invalid: [
|
||||
{
|
||||
code: 'try{ foo }catch(error){ bar }',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'use-error',
|
||||
data: { paramName: 'error' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'try{ foo }catch(e){}',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'use-error',
|
||||
data: { paramName: 'e' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'try{ foo }catch(e){console.log(c)}',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'use-error',
|
||||
data: { paramName: 'e' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Rule } from 'eslint';
|
||||
import traverse from 'eslint-traverse';
|
||||
|
||||
export const useErrorInCatch: Rule.RuleModule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Use error in catch block',
|
||||
},
|
||||
messages: {
|
||||
'use-error':
|
||||
'Catch 中应该对捕获到的 "{{paramName}}" 做一些处理,不可直接忽略',
|
||||
},
|
||||
},
|
||||
|
||||
create(context: Rule.RuleContext) {
|
||||
return {
|
||||
CatchClause(node) {
|
||||
const errorParam = (node.param as { name: string })?.name;
|
||||
|
||||
let hasUsed = false;
|
||||
if (errorParam) {
|
||||
traverse(context, node.body, path => {
|
||||
const n = path.node;
|
||||
if (n.type === 'Identifier' && n.name === errorParam) {
|
||||
hasUsed = true;
|
||||
return traverse.STOP;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!hasUsed) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'use-error',
|
||||
data: { paramName: errorParam },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
8
frontend/infra/eslint-plugin/src/zustand/index.js
Normal file
8
frontend/infra/eslint-plugin/src/zustand/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
require('ts-node').register({
|
||||
transpileOnly: true,
|
||||
cwd: __dirname,
|
||||
options: {},
|
||||
})
|
||||
|
||||
const { flowPreset } = require('./index.ts')
|
||||
module.exports = flowPreset
|
||||
58
frontend/infra/eslint-plugin/src/zustand/index.ts
Normal file
58
frontend/infra/eslint-plugin/src/zustand/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Linter } from '@typescript-eslint/utils/ts-eslint';
|
||||
import { noGetStateInComp } from './rules/no-get-state-in-comp';
|
||||
import { noStateMutation } from './rules/no-state-mutation';
|
||||
import { preferCurryCreate } from './rules/prefer-curry-create';
|
||||
import { preferSelector } from './rules/prefer-selector';
|
||||
import { preferShallow } from './rules/prefer-shallow';
|
||||
import { properStoreTyping } from './rules/proper-store-typing';
|
||||
import { storeFilenameConvention } from './rules/store-filename-convention';
|
||||
import { storeNameConvention } from './rules/store-name-convention';
|
||||
import { devtoolsConfig } from './rules/zustand-devtools-config';
|
||||
import { preferMiddlewares } from './rules/zustand-prefer-middlewares';
|
||||
|
||||
export const flowPreset: Linter.Plugin = {
|
||||
rules: {
|
||||
'prefer-selector': preferSelector,
|
||||
'prefer-shallow': preferShallow,
|
||||
'store-name-convention': storeNameConvention,
|
||||
'no-state-mutation': noStateMutation,
|
||||
'store-filename-convention': storeFilenameConvention,
|
||||
'prefer-curry-create': preferCurryCreate,
|
||||
'prefer-middlewares': preferMiddlewares,
|
||||
'devtools-config': devtoolsConfig,
|
||||
'proper-store-typing': properStoreTyping,
|
||||
'no-get-state-in-comp': noGetStateInComp,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
rules: {
|
||||
'@coze-arch/zustand/no-get-state-in-comp': 'warn',
|
||||
'@coze-arch/zustand/proper-store-typing': 'warn',
|
||||
'@coze-arch/zustand/devtools-config': 'warn',
|
||||
'@coze-arch/zustand/prefer-middlewares': 'warn',
|
||||
'@coze-arch/zustand/prefer-curry-create': 'warn',
|
||||
'@coze-arch/zustand/no-state-mutation': 'error',
|
||||
'@coze-arch/zustand/store-filename-convention': 'warn',
|
||||
'@coze-arch/zustand/store-name-convention': 'error',
|
||||
'@coze-arch/zustand/prefer-selector': 'warn',
|
||||
'@coze-arch/zustand/prefer-shallow': 'warn',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
isObjLiteral,
|
||||
extractIdentifiersFromPattern,
|
||||
isSameIdentifier,
|
||||
} from '../utils';
|
||||
|
||||
describe('isObjLiteral', () => {
|
||||
it('should return true for ObjectExpression', () => {
|
||||
const node = { type: 'ObjectExpression' } as unknown as TSESTree.Expression;
|
||||
expect(isObjLiteral(node)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for ArrayExpression', () => {
|
||||
const node = { type: 'ArrayExpression' } as unknown as TSESTree.Expression;
|
||||
expect(isObjLiteral(node)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other types', () => {
|
||||
const node = { type: 'Literal' } as unknown as TSESTree.Expression;
|
||||
expect(isObjLiteral(node)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null or undefined', () => {
|
||||
expect(isObjLiteral(null as any)).toBe(false);
|
||||
expect(isObjLiteral(undefined as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractIdentifiersFromPattern', () => {
|
||||
it('should extract identifiers from a simple identifier pattern', () => {
|
||||
const node = {
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
name: 'a',
|
||||
};
|
||||
const result = extractIdentifiersFromPattern(node as TSESTree.Identifier);
|
||||
expect(result).toEqual([node]);
|
||||
});
|
||||
|
||||
it('should extract identifiers from an object pattern', () => {
|
||||
const node = {
|
||||
type: 'ObjectPattern',
|
||||
properties: [
|
||||
{
|
||||
type: 'Property',
|
||||
value: { type: AST_NODE_TYPES.Identifier, name: 'a' },
|
||||
},
|
||||
{
|
||||
type: 'RestElement',
|
||||
argument: { type: AST_NODE_TYPES.Identifier, name: 'b' },
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = extractIdentifiersFromPattern(
|
||||
node as TSESTree.ObjectPattern,
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'a' },
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract identifiers from an array pattern', () => {
|
||||
const node = {
|
||||
type: AST_NODE_TYPES.ArrayPattern,
|
||||
elements: [
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'a' },
|
||||
null,
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'b' },
|
||||
],
|
||||
};
|
||||
const result = extractIdentifiersFromPattern(node as TSESTree.ArrayPattern);
|
||||
expect(result).toEqual([
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'a' },
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract identifiers from nested patterns', () => {
|
||||
const node = {
|
||||
type: 'ObjectPattern',
|
||||
properties: [
|
||||
{
|
||||
type: 'Property',
|
||||
value: {
|
||||
type: 'ArrayPattern',
|
||||
elements: [
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'a' },
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'b' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = extractIdentifiersFromPattern(
|
||||
node as TSESTree.ObjectPattern,
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'a' },
|
||||
{ type: AST_NODE_TYPES.Identifier, name: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty patterns', () => {
|
||||
const node = {
|
||||
type: AST_NODE_TYPES.ObjectPattern,
|
||||
properties: [],
|
||||
};
|
||||
const result = extractIdentifiersFromPattern(
|
||||
node as unknown as TSESTree.ObjectPattern,
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameIdentifier', () => {
|
||||
it('should return true for identical identifiers', () => {
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id1: TSESTree.Identifier = {
|
||||
name: 'foo',
|
||||
range: [0, 5],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id2: TSESTree.Identifier = {
|
||||
name: 'foo',
|
||||
range: [0, 5],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
expect(isSameIdentifier(id1, id2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different names', () => {
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id1: TSESTree.Identifier = {
|
||||
name: 'foo',
|
||||
range: [0, 5],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id2: TSESTree.Identifier = {
|
||||
name: 'bar',
|
||||
range: [0, 5],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
expect(isSameIdentifier(id1, id2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for different ranges', () => {
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id1: TSESTree.Identifier = {
|
||||
name: 'foo',
|
||||
range: [0, 5],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id2: TSESTree.Identifier = {
|
||||
name: 'foo',
|
||||
range: [0, 6],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
expect(isSameIdentifier(id1, id2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if one identifier is undefined', () => {
|
||||
//@ts-expect-error -- ignore mock
|
||||
const id1: TSESTree.Identifier = {
|
||||
name: 'foo',
|
||||
range: [0, 5],
|
||||
type: AST_NODE_TYPES.Identifier,
|
||||
};
|
||||
expect(isSameIdentifier(id1, undefined)).toBe(false);
|
||||
expect(isSameIdentifier(undefined, id1)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if both identifiers are undefined', () => {
|
||||
expect(isSameIdentifier(undefined, undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { noGetStateInComp } from './index';
|
||||
|
||||
ruleTester.run('no-get-state-in-comp', noGetStateInComp, {
|
||||
valid: [
|
||||
{
|
||||
code: 'function App() { const s = useStore() ;return (<div></div>)}',
|
||||
filename: 'index.tsx',
|
||||
},
|
||||
{
|
||||
code: 'const App = () => { const s = useStore() ;return (<div></div>)}',
|
||||
filename: 'index.tsx',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'const App = () => { const s = useStore.getState() ;return (<div></div>)}',
|
||||
filename: 'index.tsx',
|
||||
errors: [{ messageId: 'noGetState' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
import { createRule, getZustandSetting, isNameMatchPattern } from '../utils';
|
||||
|
||||
export const noGetStateInComp = createRule({
|
||||
name: 'zustand/no-get-state-in-comp',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
schema: [],
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow use getState() in components.',
|
||||
},
|
||||
messages: {
|
||||
noGetState:
|
||||
'Avoid using {{storeName}}.getState() in react components. Use hooks instead.',
|
||||
},
|
||||
},
|
||||
|
||||
create: context => {
|
||||
const { storeNamePattern } = getZustandSetting(context.settings);
|
||||
|
||||
return {
|
||||
'BlockStatement > VariableDeclaration > VariableDeclarator > CallExpression > MemberExpression[property.name="getState"]'(
|
||||
node: TSESTree.MemberExpression,
|
||||
) {
|
||||
if (node.object.type === AST_NODE_TYPES.Identifier) {
|
||||
if (isNameMatchPattern(node.object.name, storeNamePattern)) {
|
||||
const blockStatement = node.parent.parent?.parent
|
||||
?.parent as TSESTree.BlockStatement;
|
||||
const last = blockStatement.body[blockStatement.body.length - 1];
|
||||
if (
|
||||
last.type === AST_NODE_TYPES.ReturnStatement &&
|
||||
last.argument?.type === AST_NODE_TYPES.JSXElement
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noGetState',
|
||||
data: {
|
||||
storeName: node.object.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { noStateMutation } from './index';
|
||||
|
||||
ruleTester.run('no-state-mutation', noStateMutation, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: 'const state = useFooStore.getState(); state.foo += 1',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noStateMutation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const state = useFooStore.getState(); state.foo.bar.baz = 1',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noStateMutation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const { foo: { foo: [{ foo: bar }] }} = useFooStore.getState();bar.bar = 1',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noStateMutation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore.getState().foo.bar.baz = 1',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noStateMutation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const state = useFooStore.getState(); const state2 = useBarStore.getState(); const fn = () => () => state.foo.bar.baz = 1',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noStateMutation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
createRule,
|
||||
extractIdentifiersFromPattern,
|
||||
isSameIdentifier,
|
||||
isNameMatchPattern,
|
||||
getZustandSetting,
|
||||
} from '../utils';
|
||||
|
||||
const GET_STATE = 'getState';
|
||||
|
||||
export const noStateMutation = createRule({
|
||||
name: 'zustand/no-state-mutation',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
schema: [],
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow mutate store state directly',
|
||||
},
|
||||
messages: {
|
||||
noStateMutation:
|
||||
'Do not mutate the store state directly. Instead, use the set or setState API',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const stateIds: TSESTree.Identifier[] = [];
|
||||
const idsToDetect: TSESTree.Identifier[] = [];
|
||||
let assignNode: TSESTree.AssignmentExpression | undefined;
|
||||
const { storeNamePattern } = getZustandSetting(context.settings);
|
||||
return {
|
||||
AssignmentExpression(node) {
|
||||
assignNode = node;
|
||||
if (node.left.type === 'MemberExpression') {
|
||||
let n = node.left;
|
||||
while (n.object && n.object.type === 'MemberExpression') {
|
||||
n = n.object;
|
||||
}
|
||||
if (
|
||||
n.object.type === 'CallExpression' &&
|
||||
n.object.callee.type === 'MemberExpression' &&
|
||||
n.object.callee.object.type === 'Identifier' &&
|
||||
isNameMatchPattern(n.object.callee.object.name, storeNamePattern)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noStateMutation',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (n.object.type === 'Identifier') {
|
||||
idsToDetect.push(n.object);
|
||||
}
|
||||
}
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (
|
||||
node.init &&
|
||||
node.init.type === 'CallExpression' &&
|
||||
node.init.callee.type === 'MemberExpression' &&
|
||||
node.init.callee.object.type === 'Identifier' &&
|
||||
isNameMatchPattern(node.init.callee.object.name, storeNamePattern) &&
|
||||
node.init.callee.property.type === 'Identifier' &&
|
||||
node.init.callee.property.name === GET_STATE
|
||||
) {
|
||||
const identifiers = extractIdentifiersFromPattern(node.id);
|
||||
stateIds.push(...identifiers);
|
||||
}
|
||||
},
|
||||
'Program:exit'() {
|
||||
if (assignNode) {
|
||||
idsToDetect.forEach(modifyObjId => {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(modifyObjId) as any,
|
||||
modifyObjId.name,
|
||||
);
|
||||
|
||||
if (
|
||||
stateIds.find(i => isSameIdentifier(variable?.identifiers[0], i))
|
||||
) {
|
||||
context.report({
|
||||
node: assignNode as TSESTree.AssignmentExpression,
|
||||
messageId: 'noStateMutation',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { preferCurryCreate } from '.';
|
||||
|
||||
const code = "import { create } from 'zustand';";
|
||||
|
||||
ruleTester.run('store-name-convention', preferCurryCreate, {
|
||||
valid: [
|
||||
{
|
||||
code: `${code}const store = create()()`,
|
||||
},
|
||||
{
|
||||
code: `${code} interface A {};const store = create<A>()()`,
|
||||
},
|
||||
{
|
||||
code: 'const create = () => {}; const store = create()',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `${code}const store = create()`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferCurryCreate',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'curryCreate',
|
||||
output: `${code}const store = create()()`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create<T>()`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferCurryCreate',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'curryCreate',
|
||||
output: `${code}const store = create<T>()()`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import { accessImportedIds, createRule, isSameIdentifier } from '../utils';
|
||||
|
||||
const STORE_CREATE_NAME = 'create';
|
||||
|
||||
export const preferCurryCreate = createRule({
|
||||
name: 'zustand/prefer-curry-create',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer using curry to create store pattern',
|
||||
},
|
||||
messages: {
|
||||
preferCurryCreate:
|
||||
'Do not use create(). Prefer using curry create pattern',
|
||||
curryCreate: 'Use curry create pattern',
|
||||
},
|
||||
schema: [],
|
||||
hasSuggestions: true,
|
||||
},
|
||||
create: accessImportedIds({
|
||||
[STORE_CREATE_NAME]: ['zustand'],
|
||||
})((context, _, ids) => {
|
||||
return {
|
||||
'VariableDeclarator > CallExpression'(node: TSESTree.CallExpression) {
|
||||
if (
|
||||
node.callee.type === AST_NODE_TYPES.Identifier &&
|
||||
node.callee.name === STORE_CREATE_NAME
|
||||
) {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(node),
|
||||
STORE_CREATE_NAME,
|
||||
);
|
||||
if (
|
||||
isSameIdentifier(
|
||||
variable?.identifiers[0],
|
||||
ids.get(STORE_CREATE_NAME) as TSESTree.Identifier,
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node: node.callee,
|
||||
messageId: 'preferCurryCreate',
|
||||
suggest: [
|
||||
{
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(
|
||||
node.typeArguments || node.callee,
|
||||
'()',
|
||||
);
|
||||
},
|
||||
messageId: 'curryCreate',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { preferSelector } from './index';
|
||||
|
||||
ruleTester.run('prefer-selector', preferSelector, {
|
||||
valid: [
|
||||
'foo()',
|
||||
'new Foo()',
|
||||
'useFooStore((s) => {})',
|
||||
'useFooStore(selector)',
|
||||
'useFooStore.getState()',
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'useFooStore()',
|
||||
errors: [{ messageId: 'preferSelector' }],
|
||||
},
|
||||
{
|
||||
code: 'const {a, b} = useFooStore()',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferSelector',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'useSelectorKeyValue',
|
||||
output:
|
||||
'const {a, b} = useFooStore((state) => ({a: state.a, b: state.b}))',
|
||||
},
|
||||
{
|
||||
messageId: 'useSelectorUnderlineAlias',
|
||||
output:
|
||||
'const {a, b} = useFooStore(({a: _a, b: _b}) => ({a: _a, b: _b}))',
|
||||
},
|
||||
{
|
||||
messageId: 'useSelectorDestruct',
|
||||
output: 'const {a, b} = useFooStore(({a, b}) => ({a, b}))',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const {a:c, b} = useFooStore()',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferSelector',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'useSelectorKeyValue',
|
||||
output:
|
||||
'const {a:c, b} = useFooStore((state) => ({a: state.a, b: state.b}))',
|
||||
},
|
||||
{
|
||||
messageId: 'useSelectorUnderlineAlias',
|
||||
output:
|
||||
'const {a:c, b} = useFooStore(({a: _a, b: _b}) => ({a: _a, b: _b}))',
|
||||
},
|
||||
{
|
||||
messageId: 'useSelectorDestruct',
|
||||
output: 'const {a:c, b} = useFooStore(({a, b}) => ({a, b}))',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const {a, ...b} = useFooStore()',
|
||||
errors: [{ messageId: 'preferSelector' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import { RuleFixer } from '@typescript-eslint/utils/ts-eslint';
|
||||
import { createRule, getZustandSetting, isNameMatchPattern } from '../utils';
|
||||
|
||||
const createFixer =
|
||||
({
|
||||
shouldFix,
|
||||
node,
|
||||
name,
|
||||
}: {
|
||||
shouldFix: boolean;
|
||||
node: TSESTree.CallExpression;
|
||||
name: string;
|
||||
}) =>
|
||||
({ paramsStr, retStr = paramsStr }: { paramsStr: string; retStr?: string }) =>
|
||||
(fixer: RuleFixer) => {
|
||||
if (!shouldFix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceText(node, `${name}((${paramsStr}) => (${retStr}))`);
|
||||
};
|
||||
|
||||
export const preferSelector = createRule({
|
||||
name: 'zustand/preferSelector',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
schema: [],
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer using selector function when calling {{ name }}',
|
||||
},
|
||||
messages: {
|
||||
preferSelector: 'Prefer using selector function when calling {{ name }}',
|
||||
useSelectorDestruct: 'Use selector function (object destruct style)',
|
||||
useSelectorKeyValue: 'Use selector function (object property style)',
|
||||
useSelectorUnderlineAlias:
|
||||
'Use selector function (underLine alias style)',
|
||||
},
|
||||
hasSuggestions: true,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const { storeNamePattern } = getZustandSetting(context.settings);
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier'
|
||||
) {
|
||||
const { name } = node.callee;
|
||||
if (isNameMatchPattern(name, storeNamePattern)) {
|
||||
if (node.arguments.length === 0) {
|
||||
const { parent } = node;
|
||||
|
||||
if (
|
||||
parent.type === 'VariableDeclarator' &&
|
||||
parent.id.type === 'ObjectPattern'
|
||||
) {
|
||||
let names: string[] = [];
|
||||
for (const p of parent.id.properties) {
|
||||
if (p.type === 'Property') {
|
||||
if (p.key.type === 'Identifier' && p.key.name) {
|
||||
names.push(p.key.name);
|
||||
}
|
||||
}
|
||||
if (p.type === 'RestElement') {
|
||||
names = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fixer = createFixer({
|
||||
shouldFix: !!names?.length,
|
||||
name,
|
||||
node,
|
||||
});
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'preferSelector',
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: 'useSelectorKeyValue',
|
||||
fix: fixer({
|
||||
paramsStr: 'state',
|
||||
retStr: `{${names
|
||||
.filter(Boolean)
|
||||
.map(it => `${it}: state.${it}`)
|
||||
.join(', ')}}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
messageId: 'useSelectorUnderlineAlias',
|
||||
fix: fixer({
|
||||
paramsStr: `{${names
|
||||
.map(it => `${it}: _${it}`)
|
||||
.join(', ')}}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
messageId: 'useSelectorDestruct',
|
||||
fix: fixer({
|
||||
paramsStr: `{${names
|
||||
.filter(Boolean)
|
||||
.map(it => `${it}`)
|
||||
.join(', ')}}`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'preferSelector',
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { preferShallow } from './index';
|
||||
|
||||
const importSnippet = "\nimport { useShallow } from 'zustand/react/shallow';\n";
|
||||
|
||||
ruleTester.run('prefer-shallow', preferShallow, {
|
||||
valid: [
|
||||
'foo()',
|
||||
'new Foo()',
|
||||
'useShallowedFooStore()',
|
||||
'useFooStore((s) => s.value)',
|
||||
'useFooStore(selector)', // 暂时豁免
|
||||
'useShallowFooStore(() => ({}))',
|
||||
'useFooStore(useShallow(() => ({})))',
|
||||
'useFooStore(useShallow(() => ([])))',
|
||||
'useFooStore.getState()',
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'useFooStore(() => { return ({}) })',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
output: `${importSnippet}useFooStore(useShallow(() => { return ({}) }))`,
|
||||
messageId: 'useShallow',
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => { return {} })',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
output: `${importSnippet}useFooStore(useShallow(() => { return {} }))`,
|
||||
messageId: 'useShallow',
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => ({}))',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
output: `${importSnippet}useFooStore(useShallow(() => ({})))`,
|
||||
messageId: 'useShallow',
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => { return ([]) })',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
output: `${importSnippet}useFooStore(useShallow(() => { return ([]) }))`,
|
||||
messageId: 'useShallow',
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => { return [] })',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'useShallow',
|
||||
output: `${importSnippet}useFooStore(useShallow(() => { return [] }))`,
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => ([]))',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'useShallow',
|
||||
output: `${importSnippet}useFooStore(useShallow(() => ([])))`,
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => { const a = {}; return a;})',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
output: `${importSnippet}useFooStore(useShallow(() => { const a = {}; return a;}))`,
|
||||
messageId: 'useShallow',
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'useFooStore(() => { const a = []; return a;})',
|
||||
errors: [
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
output: `${importSnippet}useFooStore(useShallow(() => { const a = []; return a;}))`,
|
||||
messageId: 'useShallow',
|
||||
},
|
||||
],
|
||||
messageId: 'preferShallow',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ASTUtils, AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
createRule,
|
||||
getZustandSetting,
|
||||
isNameMatchPattern,
|
||||
isObjLiteral,
|
||||
} from '../utils';
|
||||
|
||||
export const preferShallow = createRule({
|
||||
defaultOptions: [],
|
||||
name: 'zustand/preferShallow',
|
||||
meta: {
|
||||
schema: [],
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Prefer using `useShallow` when store selector return an object literal to reduce re-render',
|
||||
},
|
||||
messages: {
|
||||
preferShallow:
|
||||
'Prefer using `useShallow` when calling {{name}} selector return an object literal to reduce re-render',
|
||||
useShallow: 'Use `useShallow`',
|
||||
},
|
||||
hasSuggestions: true,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const problemNodes: TSESTree.CallExpression[] = [];
|
||||
let insetRange = [0, 0] as [number, number];
|
||||
const { storeNamePattern, shallowStoreNamePattern } = getZustandSetting(
|
||||
context.settings,
|
||||
);
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.range) {
|
||||
insetRange = node.range;
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier'
|
||||
) {
|
||||
const { name } = node.callee;
|
||||
|
||||
if (
|
||||
isNameMatchPattern(name, storeNamePattern) &&
|
||||
!isNameMatchPattern(name, shallowStoreNamePattern)
|
||||
) {
|
||||
if (node.arguments.length === 1) {
|
||||
const expr = node.arguments[0];
|
||||
if (
|
||||
expr.type === 'FunctionExpression' ||
|
||||
expr.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (expr.body.type === 'BlockStatement') {
|
||||
const ret = expr.body.body.find(
|
||||
i => i.type === 'ReturnStatement',
|
||||
) as TSESTree.ReturnStatement | undefined;
|
||||
|
||||
if (ret?.argument) {
|
||||
if (isObjLiteral(ret.argument)) {
|
||||
problemNodes.push(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ret.argument.type === 'Identifier') {
|
||||
const variable = ASTUtils.findVariable(
|
||||
context.sourceCode.getScope(ret),
|
||||
ret.argument.name,
|
||||
);
|
||||
|
||||
const n = variable?.defs[0].node;
|
||||
if (n?.type === AST_NODE_TYPES.VariableDeclarator) {
|
||||
if (isObjLiteral(n.init)) {
|
||||
problemNodes.push(node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isObjLiteral(expr.body)) {
|
||||
problemNodes.push(node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// deprecated use
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
problemNodes.forEach(node => {
|
||||
const { name } = node.callee as any;
|
||||
const expr = node.arguments[0];
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'preferShallow',
|
||||
data: { name },
|
||||
suggest: [
|
||||
{
|
||||
messageId: 'useShallow',
|
||||
fix: fixer => {
|
||||
return [
|
||||
fixer.insertTextBefore(expr, 'useShallow('),
|
||||
fixer.insertTextAfter(expr, ')'),
|
||||
fixer.insertTextAfterRange(
|
||||
insetRange,
|
||||
"\nimport { useShallow } from 'zustand/react/shallow';\n",
|
||||
),
|
||||
];
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { properStoreTyping } from '.';
|
||||
|
||||
const code = "import { create } from 'zustand';";
|
||||
|
||||
ruleTester.run('proper-store-typing', properStoreTyping, {
|
||||
valid: [
|
||||
{
|
||||
code: 'const foo = create()',
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create<T>()`,
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create<T>()()`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `${code}const store = create()`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'storeTyping',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()()`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'storeTyping',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
import { accessImportedIds, isSameIdentifier, createRule } from '../utils';
|
||||
|
||||
const STORE_CREATE_NAME = 'create';
|
||||
|
||||
export const properStoreTyping = createRule({
|
||||
name: 'zustand/proper-store-typing',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow creating a store without a type parameter',
|
||||
},
|
||||
messages: {
|
||||
storeTyping: 'Require a type parameter when creating a store',
|
||||
},
|
||||
schema: [],
|
||||
hasSuggestions: true,
|
||||
},
|
||||
create: accessImportedIds({
|
||||
[STORE_CREATE_NAME]: ['zustand'],
|
||||
})((context, _, ids) => {
|
||||
return {
|
||||
CallExpression(node: TSESTree.CallExpression) {
|
||||
if (
|
||||
node.callee.type === AST_NODE_TYPES.Identifier &&
|
||||
node.callee.name === STORE_CREATE_NAME
|
||||
) {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(node),
|
||||
STORE_CREATE_NAME,
|
||||
);
|
||||
// zustand create
|
||||
if (
|
||||
isSameIdentifier(
|
||||
variable?.identifiers[0],
|
||||
ids.get(STORE_CREATE_NAME),
|
||||
)
|
||||
) {
|
||||
if (!node.typeArguments) {
|
||||
context.report({
|
||||
node: node.callee,
|
||||
messageId: 'storeTyping',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { storeFilenameConvention } from './index';
|
||||
|
||||
const code = "import { create } from 'zustand'; const store = create()() ";
|
||||
|
||||
ruleTester.run('store-filename-convention', storeFilenameConvention, {
|
||||
valid: [
|
||||
{
|
||||
code,
|
||||
filename: 'foo-store.ts',
|
||||
},
|
||||
{
|
||||
code,
|
||||
filename: 'fooStore.ts',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code,
|
||||
filename: 'store.ts',
|
||||
errors: [{ messageId: 'nameConvention' }],
|
||||
},
|
||||
{
|
||||
code,
|
||||
filename: 'foo.ts',
|
||||
errors: [{ messageId: 'nameConvention' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import { accessImportedIds, createRule, isSameIdentifier } from '../utils';
|
||||
|
||||
const STORE_CREATE_NAME = 'create';
|
||||
|
||||
export interface OptionType {
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export const storeFilenameConvention = createRule<
|
||||
OptionType[],
|
||||
'nameConvention'
|
||||
>({
|
||||
defaultOptions: [{ pattern: '^([a-zA-Z0-9]+-?)+[sS]tore$' }],
|
||||
name: 'zustand/store-filename-convention',
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'The store filename must match naming convention',
|
||||
},
|
||||
messages: {
|
||||
nameConvention:
|
||||
'The filename of the file that creates a store must match pattern {{pattern}}',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: accessImportedIds<OptionType>({ [STORE_CREATE_NAME]: ['zustand'] })(
|
||||
(context, options, ids) => {
|
||||
return {
|
||||
CallExpression(node: TSESTree.CallExpression) {
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === STORE_CREATE_NAME
|
||||
) {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(node),
|
||||
STORE_CREATE_NAME,
|
||||
);
|
||||
|
||||
if (
|
||||
isSameIdentifier(
|
||||
ids.get(STORE_CREATE_NAME),
|
||||
variable?.identifiers[0],
|
||||
)
|
||||
) {
|
||||
const fileNamePattern = new RegExp(options[0].pattern);
|
||||
const filename = path.basename(
|
||||
context.filename,
|
||||
path.extname(context.filename),
|
||||
);
|
||||
if (!fileNamePattern.test(filename)) {
|
||||
context.report({
|
||||
loc: { line: 0, column: 0 },
|
||||
messageId: 'nameConvention',
|
||||
data: {
|
||||
pattern: fileNamePattern,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { storeNameConvention } from './index';
|
||||
|
||||
ruleTester.run('store-name-convention', storeNameConvention, {
|
||||
valid: [
|
||||
{
|
||||
code: "import { create } from 'zustand'; \n const useStore = create()",
|
||||
},
|
||||
{
|
||||
code: "import { create } from 'zustand'; \n const useFooStore = create()",
|
||||
},
|
||||
{
|
||||
code: "import { create } from 'zustand'; \n const createStore = () => { const useFooStore = create() }",
|
||||
},
|
||||
{
|
||||
code: "import { create } from 'foo'; \n const foo = create() ",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "import { create } from 'zustand';\n const foo = create()",
|
||||
errors: [{ messageId: 'nameConvention' }],
|
||||
},
|
||||
{
|
||||
code: "import { create } from 'zustand';\n createStore = () => {const foo = create()}",
|
||||
errors: [{ messageId: 'nameConvention' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import {
|
||||
accessImportedIds,
|
||||
createRule,
|
||||
isSameIdentifier,
|
||||
isNameMatchPattern,
|
||||
getZustandSetting,
|
||||
} from '../utils';
|
||||
|
||||
const STORE_CREATE_NAME = 'create';
|
||||
|
||||
export const storeNameConvention = createRule({
|
||||
name: 'zustand/name-convention',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
schema: [],
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'The store name must match the naming convention',
|
||||
},
|
||||
messages: {
|
||||
nameConvention: 'The store name must match pattern {{pattern}}',
|
||||
},
|
||||
},
|
||||
|
||||
create: accessImportedIds({ [STORE_CREATE_NAME]: ['zustand'] })(
|
||||
(context, _, ids) => {
|
||||
const { storeNamePattern } = getZustandSetting(context.settings);
|
||||
|
||||
return {
|
||||
'VariableDeclarator > CallExpression'(node: TSESTree.CallExpression) {
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === STORE_CREATE_NAME
|
||||
) {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(node),
|
||||
STORE_CREATE_NAME,
|
||||
);
|
||||
if (
|
||||
isSameIdentifier(
|
||||
ids.get(STORE_CREATE_NAME),
|
||||
variable?.identifiers[0],
|
||||
)
|
||||
) {
|
||||
const { parent } = node;
|
||||
if (
|
||||
parent.type === 'VariableDeclarator' &&
|
||||
parent.id.type === 'Identifier'
|
||||
) {
|
||||
if (!isNameMatchPattern(parent.id.name, storeNamePattern)) {
|
||||
context.report({
|
||||
node: parent,
|
||||
messageId: 'nameConvention',
|
||||
data: {
|
||||
pattern: storeNamePattern,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
),
|
||||
});
|
||||
30
frontend/infra/eslint-plugin/src/zustand/rules/tester.ts
Normal file
30
frontend/infra/eslint-plugin/src/zustand/rules/tester.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as vitest from 'vitest';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import parser from '@typescript-eslint/parser';
|
||||
|
||||
RuleTester.afterAll = vitest.afterAll;
|
||||
RuleTester.it = vitest.it;
|
||||
RuleTester.itOnly = vitest.it.only;
|
||||
RuleTester.describe = vitest.describe;
|
||||
|
||||
export const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser,
|
||||
},
|
||||
});
|
||||
148
frontend/infra/eslint-plugin/src/zustand/rules/utils.ts
Normal file
148
frontend/infra/eslint-plugin/src/zustand/rules/utils.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
import {
|
||||
RuleContext,
|
||||
RuleListener,
|
||||
SharedConfigurationSettings,
|
||||
} from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
const DEFAULT_STORE_NAME_PATTERN = '^use[a-zA-Z0-9_]*Store(Shallow)?$';
|
||||
const DEFAULT_SHALLOW_STORE_NAME_PATTERN =
|
||||
'^use(Shallow[a-zA-Z0-9_]*Store|[a-zA-Z0-9_]*StoreShallow)$';
|
||||
|
||||
export const isObjLiteral = (node?: TSESTree.Expression | null) =>
|
||||
!!node &&
|
||||
(node.type === 'ObjectExpression' || node.type === 'ArrayExpression');
|
||||
|
||||
export const isNameMatchPattern = (name: string, namePattern: string) =>
|
||||
new RegExp(namePattern).test(name);
|
||||
|
||||
export const extractIdentifiersFromPattern = (
|
||||
node: TSESTree.DestructuringPattern,
|
||||
) => {
|
||||
const identifiers: TSESTree.Identifier[] = [];
|
||||
|
||||
const extractIdentifiers = (node: TSESTree.DestructuringPattern) => {
|
||||
if (node.type === 'Identifier') {
|
||||
identifiers.push(node);
|
||||
} else if (node.type === AST_NODE_TYPES.ObjectPattern) {
|
||||
node.properties.forEach(prop => {
|
||||
if (prop.type === AST_NODE_TYPES.Property && prop.value) {
|
||||
extractIdentifiers(prop.value as TSESTree.DestructuringPattern);
|
||||
} else if (
|
||||
prop.type === AST_NODE_TYPES.RestElement &&
|
||||
prop.argument.type === AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
extractIdentifiers(prop.argument);
|
||||
}
|
||||
});
|
||||
} else if (node.type === AST_NODE_TYPES.ArrayPattern) {
|
||||
node.elements.forEach(element => {
|
||||
if (element) {
|
||||
extractIdentifiers(element);
|
||||
}
|
||||
});
|
||||
} else if (node.type === AST_NODE_TYPES.RestElement) {
|
||||
extractIdentifiers(node.argument);
|
||||
}
|
||||
};
|
||||
|
||||
extractIdentifiers(node);
|
||||
return identifiers;
|
||||
};
|
||||
|
||||
export const isSameIdentifier = (
|
||||
identifierA?: TSESTree.Identifier,
|
||||
identifierB?: TSESTree.Identifier,
|
||||
) =>
|
||||
!!(
|
||||
identifierA &&
|
||||
identifierB &&
|
||||
identifierA.name === identifierB.name &&
|
||||
identifierA.range?.[0] === identifierB.range?.[0] &&
|
||||
identifierA.range?.[1] === identifierB.range?.[1]
|
||||
);
|
||||
|
||||
export const createRule = ESLintUtils.RuleCreator(
|
||||
() => 'https://github.com/pmndrs/zustand',
|
||||
);
|
||||
|
||||
export const findCalleeNames = (
|
||||
node: TSESTree.CallExpressionArgument,
|
||||
names: string[] = [],
|
||||
) => {
|
||||
if (node && node.type === AST_NODE_TYPES.CallExpression) {
|
||||
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
||||
names.push(node.callee.name);
|
||||
}
|
||||
findCalleeNames(node.arguments[0], names);
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
type TypeIdsMap = Map<string, TSESTree.Identifier>;
|
||||
type ContextMap<T> = Readonly<RuleContext<string, readonly T[]>>;
|
||||
type CreateType<T> = (
|
||||
context: ContextMap<T>,
|
||||
optionsWithDefault: readonly T[],
|
||||
ids: TypeIdsMap,
|
||||
) => RuleListener;
|
||||
|
||||
export const accessImportedIds =
|
||||
<T>(ids: Record<string, string[]>) =>
|
||||
(originCreate: CreateType<T>) => {
|
||||
return ((context, options) => {
|
||||
const idsMap: TypeIdsMap = new Map();
|
||||
const listeners = originCreate(
|
||||
context as ContextMap<T>,
|
||||
options as T[],
|
||||
idsMap,
|
||||
);
|
||||
return {
|
||||
...listeners,
|
||||
ImportDeclaration(node) {
|
||||
node.specifiers.forEach(s => {
|
||||
if (
|
||||
s.type === AST_NODE_TYPES.ImportSpecifier &&
|
||||
Object.hasOwnProperty.call(ids, s.local.name) &&
|
||||
ids[s.local.name].includes(node.source.value) &&
|
||||
s.imported.type === AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
idsMap.set(s.local.name, s.imported);
|
||||
}
|
||||
});
|
||||
listeners.ImportDeclaration?.(node);
|
||||
},
|
||||
};
|
||||
}) as Parameters<typeof createRule>[0]['create'];
|
||||
};
|
||||
|
||||
export const getZustandSetting = (settings: SharedConfigurationSettings) => {
|
||||
const defaultSetting = {
|
||||
storeNamePattern: DEFAULT_STORE_NAME_PATTERN,
|
||||
shallowStoreNamePattern: DEFAULT_SHALLOW_STORE_NAME_PATTERN,
|
||||
};
|
||||
return {
|
||||
...defaultSetting,
|
||||
...(settings.zustand || {}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { devtoolsConfig } from '.';
|
||||
|
||||
const code = "import { devtools } from 'zustand/middleware';";
|
||||
|
||||
ruleTester.run('devtools-config', devtoolsConfig, {
|
||||
valid: [
|
||||
{
|
||||
code: 'devtools()',
|
||||
},
|
||||
{
|
||||
code: `${code}devtools(() => {}, {enabled:true,name:'name'})`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `${code}devtools(() => {});`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noEmptyCfg',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'addCfg',
|
||||
output: `${code}devtools(() => {},{name:'DEV_TOOLS_NAME_SPACE'});`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}devtools(() => {}, {});`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'nameCfg',
|
||||
},
|
||||
{
|
||||
messageId: 'enabledCfg',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import { accessImportedIds, createRule, isSameIdentifier } from '../utils';
|
||||
|
||||
const VAR_NAME = 'devtools';
|
||||
|
||||
export const devtoolsConfig = createRule({
|
||||
name: 'zustand/devToolsConfig',
|
||||
defaultOptions: [],
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'middleware devtools config',
|
||||
},
|
||||
messages: {
|
||||
noEmptyCfg: 'Middleware devtools need a config parameter',
|
||||
addCfg: 'Use config parameter.',
|
||||
nameCfg: 'Configure name fields to separate the namespace',
|
||||
enabledCfg:
|
||||
'Configure enabled fields and ensure the value is false when in production',
|
||||
},
|
||||
schema: [],
|
||||
hasSuggestions: true,
|
||||
},
|
||||
create: accessImportedIds({
|
||||
[VAR_NAME]: ['zustand/middleware', 'zustand/middleware/devtools'],
|
||||
})((context, _, ids) => {
|
||||
return {
|
||||
CallExpression(node: TSESTree.CallExpression) {
|
||||
if (
|
||||
node.callee.type === AST_NODE_TYPES.Identifier &&
|
||||
node.callee.name === VAR_NAME
|
||||
) {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(node),
|
||||
VAR_NAME,
|
||||
);
|
||||
|
||||
if (
|
||||
variable &&
|
||||
isSameIdentifier(variable?.identifiers[0], ids.get(VAR_NAME))
|
||||
) {
|
||||
const args = node.arguments[1];
|
||||
if (!args) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noEmptyCfg',
|
||||
suggest: [
|
||||
{
|
||||
messageId: 'addCfg',
|
||||
fix: fixer => {
|
||||
if (node.arguments[0]) {
|
||||
return fixer.insertTextAfter(
|
||||
node.arguments[0],
|
||||
",{name:'DEV_TOOLS_NAME_SPACE'}",
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (args.type === AST_NODE_TYPES.ObjectExpression) {
|
||||
const hasProperty = (key: string) =>
|
||||
args.properties.find(
|
||||
p =>
|
||||
p.type === AST_NODE_TYPES.Property &&
|
||||
p.key.type === AST_NODE_TYPES.Identifier &&
|
||||
p.key.name === key,
|
||||
);
|
||||
|
||||
if (!hasProperty('name')) {
|
||||
context.report({
|
||||
node: args,
|
||||
messageId: 'nameCfg',
|
||||
});
|
||||
}
|
||||
if (!hasProperty('enabled')) {
|
||||
context.report({
|
||||
node: args,
|
||||
messageId: 'enabledCfg',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ruleTester } from '../tester';
|
||||
import { preferMiddlewares } from '.';
|
||||
|
||||
const code = "import { create } from 'zustand';";
|
||||
|
||||
ruleTester.run('prefer-middlewares', preferMiddlewares, {
|
||||
valid: [
|
||||
{
|
||||
code: `${code}const store = create(m1())`,
|
||||
options: [{ middlewares: ['m1'] }],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()(m1())`,
|
||||
options: [{ middlewares: ['m1'] }],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create(m1(m2(() => ({}))))`,
|
||||
options: [{ middlewares: ['m1', 'm2'] }],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()(m1(m2(() => ({}))))`,
|
||||
options: [{ middlewares: ['m1', 'm2'] }],
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `${code}const store = create()`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferMiddlewares',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}import { devtools } from 'zustand/middleware';\nconst store = create(devtools())`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()()`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferMiddlewares',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}import { devtools } from 'zustand/middleware';\nconst store = create()(devtools())`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()(m1())`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferMiddlewares',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}import { devtools } from 'zustand/middleware';\nconst store = create()(devtools(m1()))`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()(m1(() => {}))`,
|
||||
options: [{ middlewares: ['m2'] }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferMiddlewares',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}const store = create()(m2(m1(() => {})))`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()(m1(() => {}))`,
|
||||
options: [
|
||||
{
|
||||
middlewares: [
|
||||
{ name: 'm2', suggestImport: 'import {m2} from "m2";' },
|
||||
],
|
||||
},
|
||||
],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferMiddlewares',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}import {m2} from "m2";const store = create()(m2(m1(() => {})))`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `${code}const store = create()(m1(() => {}))`,
|
||||
options: [
|
||||
{
|
||||
middlewares: [
|
||||
{ name: 'm2', suggestImport: 'import {m2} from "m2";' },
|
||||
{ name: 'm3', suggestImport: 'import {m3} from "m3";' },
|
||||
],
|
||||
},
|
||||
],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'preferMiddlewares',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}import {m2} from "m2";const store = create()(m2(m1(() => {})))`,
|
||||
},
|
||||
{
|
||||
messageId: 'applyMiddlewares',
|
||||
output: `${code}import {m3} from "m3";const store = create()(m3(m1(() => {})))`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
import { findVariable } from '@typescript-eslint/utils/ast-utils';
|
||||
import { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
||||
import {
|
||||
accessImportedIds,
|
||||
createRule,
|
||||
findCalleeNames,
|
||||
isSameIdentifier,
|
||||
} from '../utils';
|
||||
|
||||
const STORE_CREATE_NAME = 'create';
|
||||
|
||||
type Middleware =
|
||||
| string
|
||||
| { name: string; msg?: string; suggestImport?: string };
|
||||
export interface Option {
|
||||
middlewares: Middleware[];
|
||||
}
|
||||
|
||||
export const preferMiddlewares = createRule<
|
||||
Option[],
|
||||
'preferMiddlewares' | 'applyMiddlewares'
|
||||
>({
|
||||
name: 'zustand/prefer-middlewares',
|
||||
defaultOptions: [
|
||||
{
|
||||
middlewares: [
|
||||
{
|
||||
name: 'devtools',
|
||||
msg: 'Advise using devtools middleware for convenient debugging',
|
||||
suggestImport: "import { devtools } from 'zustand/middleware';\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Create store prefer using some middlewares',
|
||||
},
|
||||
messages: {
|
||||
preferMiddlewares: '{{ msgs }}',
|
||||
applyMiddlewares: 'Use {{ name }} middleware',
|
||||
},
|
||||
schema: {
|
||||
type: 'array',
|
||||
},
|
||||
hasSuggestions: true,
|
||||
},
|
||||
create: accessImportedIds<Option>({
|
||||
[STORE_CREATE_NAME]: ['zustand'],
|
||||
})((context, options, ids) => {
|
||||
let insetRange = [0, 0] as [number, number];
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.range) {
|
||||
insetRange = node.range;
|
||||
}
|
||||
},
|
||||
CallExpression(node: TSESTree.CallExpression) {
|
||||
if (
|
||||
node.callee.type === AST_NODE_TYPES.Identifier &&
|
||||
node.callee.name === STORE_CREATE_NAME
|
||||
) {
|
||||
const variable = findVariable(
|
||||
context.sourceCode.getScope(node),
|
||||
STORE_CREATE_NAME,
|
||||
);
|
||||
// zustand create
|
||||
|
||||
if (
|
||||
isSameIdentifier(
|
||||
variable?.identifiers[0],
|
||||
ids.get(STORE_CREATE_NAME),
|
||||
)
|
||||
) {
|
||||
const callExpr =
|
||||
node.arguments[0] ||
|
||||
(node.parent.type === AST_NODE_TYPES.CallExpression
|
||||
? node.parent.arguments[0]
|
||||
: undefined);
|
||||
|
||||
const names = findCalleeNames(callExpr);
|
||||
const mids = options[0].middlewares
|
||||
.map(it =>
|
||||
typeof it === 'object'
|
||||
? it
|
||||
: { name: it, msg: `advise using ${it}` },
|
||||
)
|
||||
.filter(m => !names.includes(m.name));
|
||||
|
||||
if (mids.length) {
|
||||
context.report({
|
||||
node: node.callee,
|
||||
messageId: 'preferMiddlewares',
|
||||
data: {
|
||||
msgs: mids.map(it => it.msg).join(';'),
|
||||
},
|
||||
suggest: mids.map(mid => {
|
||||
const n =
|
||||
node.parent.type === AST_NODE_TYPES.CallExpression
|
||||
? node.parent
|
||||
: node;
|
||||
|
||||
const insertRange: [number, number] | undefined = n.arguments
|
||||
.length
|
||||
? n.arguments[0].range
|
||||
: [n.range[1] - 1, n.range[1] - 1];
|
||||
|
||||
return {
|
||||
fix(fixer) {
|
||||
return [
|
||||
fixer.insertTextBeforeRange(
|
||||
insertRange,
|
||||
`${mid.name}(`,
|
||||
),
|
||||
fixer.insertTextAfterRange(insertRange, ')'),
|
||||
insetRange && mid.suggestImport
|
||||
? fixer.insertTextAfterRange(
|
||||
insetRange,
|
||||
mid.suggestImport,
|
||||
)
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
},
|
||||
messageId: 'applyMiddlewares',
|
||||
data: {
|
||||
name: mid.name,
|
||||
},
|
||||
};
|
||||
}) as ReportSuggestionArray<'applyMiddlewares'>,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
20
frontend/infra/eslint-plugin/tsconfig.build.json
Normal file
20
frontend/infra/eslint-plugin/tsconfig.build.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": false,
|
||||
"strictNullChecks": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["node"],
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"references": [
|
||||
{
|
||||
"path": "../../config/ts-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/infra/eslint-plugin/tsconfig.json
Normal file
15
frontend/infra/eslint-plugin/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
19
frontend/infra/eslint-plugin/tsconfig.misc.json
Normal file
19
frontend/infra/eslint-plugin/tsconfig.misc.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.node.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["vitest.config.mts", "**/*.test.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": false,
|
||||
"strictNullChecks": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
}
|
||||
}
|
||||
20
frontend/infra/eslint-plugin/vitest.config.mts
Normal file
20
frontend/infra/eslint-plugin/vitest.config.mts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig, coverageConfigDefaults } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
testTimeout: 30 * 1000,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
all: true,
|
||||
exclude: [
|
||||
...coverageConfigDefaults.exclude,
|
||||
'src/index.js',
|
||||
'src/index.ts',
|
||||
'src/zustand/index.js',
|
||||
'src/zustand/index.ts',
|
||||
],
|
||||
reporter: ['cobertura', 'text', 'html', 'clover', 'json', 'json-summary'],
|
||||
},
|
||||
},
|
||||
});
|
||||
68
frontend/infra/idl/idl-parser/README.md
Normal file
68
frontend/infra/idl/idl-parser/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# @coze-arch/idl-parser
|
||||
|
||||
idl parser
|
||||
|
||||
## Overview
|
||||
|
||||
This package is part of the Coze Studio monorepo and provides architecture functionality. It serves as a core component in the Coze ecosystem.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Add this package to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@coze-arch/idl-parser": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { /* exported functions/components */ } from '@coze-arch/idl-parser';
|
||||
|
||||
// Example usage
|
||||
// TODO: Add specific usage examples
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Core functionality for Coze Studio
|
||||
- TypeScript support
|
||||
- Modern ES modules
|
||||
|
||||
## API Reference
|
||||
|
||||
### Exports
|
||||
|
||||
- `*`
|
||||
|
||||
|
||||
For detailed API documentation, please refer to the TypeScript definitions.
|
||||
|
||||
## Development
|
||||
|
||||
This package is built with:
|
||||
|
||||
- TypeScript
|
||||
- Modern JavaScript
|
||||
- Vitest for testing
|
||||
- ESLint for code quality
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
24
frontend/infra/idl/idl-parser/__tests__/common.ts
Normal file
24
frontend/infra/idl/idl-parser/__tests__/common.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function filterKeys(obj: Record<string, any>, keys: string[]) {
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const key of keys) {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
|
||||
return newObj;
|
||||
}
|
||||
36
frontend/infra/idl/idl-parser/__tests__/demo.proto.ts
Normal file
36
frontend/infra/idl/idl-parser/__tests__/demo.proto.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import * as t from '../src/proto';
|
||||
|
||||
const content = `
|
||||
syntax = 'proto3';
|
||||
|
||||
// c1
|
||||
message Foo { // c2
|
||||
// c3
|
||||
int32 code = 1; // c4
|
||||
// c5
|
||||
string content = 2;
|
||||
// c6
|
||||
string message = 3; // c7
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(content);
|
||||
console.log(JSON.stringify(document, null, 2));
|
||||
42
frontend/infra/idl/idl-parser/__tests__/demo.thrift.ts
Normal file
42
frontend/infra/idl/idl-parser/__tests__/demo.thrift.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import * as t from '../src/thrift';
|
||||
import * as path from 'path';
|
||||
|
||||
const idl = `
|
||||
/*
|
||||
*/
|
||||
|
||||
struct UserDeleteDataMap {
|
||||
1: required UserDeleteData DeleteData
|
||||
2: string k2 (go.tag = 'json:\\"-\\"')
|
||||
}
|
||||
|
||||
/*
|
||||
We
|
||||
*/
|
||||
enum AvatarMetaType {
|
||||
UNKNOWN = 0, // 没有数据, 错误数据或者系统错误降级
|
||||
RANDOM = 1, // 在修改 or 创建时,用户未指定 name 或者选中推荐的文字时,程序随机选择的头像
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(idl);
|
||||
var c = path.join('a/b.thrift', './c.thrift');
|
||||
console.log(JSON.stringify(document, null, 2));
|
||||
213
frontend/infra/idl/idl-parser/__tests__/demo.unify.ts
Normal file
213
frontend/infra/idl/idl-parser/__tests__/demo.unify.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import * as t from '../src/unify/index';
|
||||
import * as path from 'path';
|
||||
|
||||
// const root = 'test/idl';
|
||||
// const idl = path.resolve(process.cwd(), root, 'dep/common.thrift');
|
||||
// const document = t.parse(idl, {
|
||||
// root,
|
||||
// namespaceRefer: true,
|
||||
// cache: false,
|
||||
// });
|
||||
// // document.body[4]
|
||||
// console.log('#gg', document);
|
||||
|
||||
const indexThriftContent = `
|
||||
namespace java com.unify_idx
|
||||
|
||||
include 'unify_dependent1.thrift'
|
||||
|
||||
typedef unify_dependent1.Foo TFoo
|
||||
|
||||
enum Gender {
|
||||
// male
|
||||
Male // male tail
|
||||
// female
|
||||
Female // female tail
|
||||
// mix
|
||||
Mix
|
||||
}
|
||||
|
||||
// const map<Gender, string> genderMap = {
|
||||
// Gender.Male: '男性',
|
||||
// Gender.Female: '女性',
|
||||
// }
|
||||
|
||||
union FuncRequest {
|
||||
1: unify_dependent1.Foo r_key1
|
||||
2: TFoo list (go.tag = "json:\\"-\\"")
|
||||
}
|
||||
`;
|
||||
|
||||
const dep1ThriftContent = `
|
||||
namespace js unify_dep1
|
||||
|
||||
typedef Foo Foo1
|
||||
|
||||
struct Foo {
|
||||
1: string f_key1
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
// const fileContentMap = {
|
||||
// 'unify_index.thrift': indexThriftContent,
|
||||
// 'unify_dependent1.thrift': dep1ThriftContent,
|
||||
// };
|
||||
|
||||
const indexProtoContent = `
|
||||
syntax = "proto3";
|
||||
|
||||
import "unify.dependent1.proto";
|
||||
|
||||
package a.b.c;
|
||||
|
||||
message Request {
|
||||
repeated string key1 = 1[(api.key) = 'f'];
|
||||
a.b.Foo key3 = 3;
|
||||
// message Sub {
|
||||
// enum Num {
|
||||
// ONE = 1;
|
||||
// }
|
||||
// // string k1 = 1;
|
||||
// Num k2 = 2;
|
||||
// }
|
||||
// Sub key2 = 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const dep1ProtoContent = `
|
||||
syntax = "proto3";
|
||||
|
||||
package a.b;
|
||||
|
||||
message Foo {
|
||||
string f_key1 = 1;
|
||||
message SubF {}
|
||||
SubF f_key2 = 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const fileContentMap = {
|
||||
'unify_index.proto': indexProtoContent,
|
||||
'unify.dependent1.proto': dep1ProtoContent,
|
||||
};
|
||||
|
||||
// const document = t.parse(
|
||||
// 'unify_index.proto',
|
||||
// {
|
||||
// root: '.',
|
||||
// // namespaceRefer: true,
|
||||
// },
|
||||
// fileContentMap
|
||||
// );
|
||||
// // document.body[4]
|
||||
// console.log(document);
|
||||
|
||||
const baseContent = `
|
||||
syntax = "proto3";
|
||||
package a.b;
|
||||
message Bar {
|
||||
message BarSub {
|
||||
enum NumBar {
|
||||
ONE = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const extraContent = `
|
||||
syntax = "proto3";
|
||||
package a.b;
|
||||
message Extra {}
|
||||
`;
|
||||
|
||||
const indexContent = `
|
||||
syntax = "proto3";
|
||||
|
||||
package a.b;
|
||||
import 'base.proto';
|
||||
import 'extra.proto';
|
||||
|
||||
message Foo {
|
||||
// message FooSub {
|
||||
// enum NumFoo {
|
||||
// TWO = 2;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Foo.FooSub.NumFoo k1 = 1;
|
||||
// FooSub.NumFoo k2 = 2;
|
||||
// FooSub k3 = 3;
|
||||
// repeated FooSub k4 = 4;
|
||||
// map<string, FooSub.NumFoo> k5 = 5;
|
||||
|
||||
// Bar.BarSub.NumBar k10 = 10;
|
||||
Bar.BarSub k11 = 11;
|
||||
// repeated Bar.BarSub.NumBar k12 = 12;
|
||||
// map<string, Bar.BarSub> k13 = 13;
|
||||
}
|
||||
|
||||
// message Bar {
|
||||
// message BarSub {
|
||||
// enum NumBar {
|
||||
// ONE = 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.proto',
|
||||
{ cache: false },
|
||||
{
|
||||
'index.proto': indexContent,
|
||||
'base.proto': baseContent,
|
||||
'extra.proto': extraContent,
|
||||
}
|
||||
);
|
||||
const statement = document.statements[0] as t.InterfaceWithFields;
|
||||
console.log(statement);
|
||||
// const baseContent = `
|
||||
// syntax = "proto3";
|
||||
// package a.b;
|
||||
// message Common {
|
||||
// }
|
||||
// `;
|
||||
|
||||
// const indexContent = `
|
||||
// syntax = "proto3";
|
||||
// message Foo {
|
||||
// google.protobuf.Any k1 = 1;
|
||||
// }
|
||||
// `;
|
||||
|
||||
// const document = t.parse(
|
||||
// 'index.proto',
|
||||
// { cache: false },
|
||||
// {
|
||||
// 'index.proto': indexContent,
|
||||
// // 'base.proto': baseContent,
|
||||
// }
|
||||
// );
|
||||
|
||||
// const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
// console.log(functions);
|
||||
@@ -0,0 +1,3 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Base {}
|
||||
@@ -0,0 +1 @@
|
||||
struct Base {}
|
||||
@@ -0,0 +1,3 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Basee {}
|
||||
@@ -0,0 +1 @@
|
||||
struct Basee {}
|
||||
@@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
package common;
|
||||
|
||||
import "base.proto";
|
||||
import "basee.proto";
|
||||
|
||||
message Common {}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace go common
|
||||
|
||||
include 'base.thrift'
|
||||
include 'basee.thrift'
|
||||
|
||||
struct Common {}
|
||||
4
frontend/infra/idl/idl-parser/__tests__/idl/error.proto
Normal file
4
frontend/infra/idl/idl-parser/__tests__/idl/error.proto
Normal file
@@ -0,0 +1,4 @@
|
||||
syntax = "proto3";
|
||||
message Foo {
|
||||
string k1 = 1;,
|
||||
}
|
||||
3
frontend/infra/idl/idl-parser/__tests__/idl/error.thrift
Normal file
3
frontend/infra/idl/idl-parser/__tests__/idl/error.thrift
Normal file
@@ -0,0 +1,3 @@
|
||||
struct Foo {
|
||||
1: string k1,,
|
||||
}
|
||||
5
frontend/infra/idl/idl-parser/__tests__/idl/index.proto
Normal file
5
frontend/infra/idl/idl-parser/__tests__/idl/index.proto
Normal file
@@ -0,0 +1,5 @@
|
||||
syntax = "proto3";
|
||||
|
||||
service Foo {
|
||||
option (api.uri_prefix) = "//example.com";
|
||||
}
|
||||
5
frontend/infra/idl/idl-parser/__tests__/idl/index.thrift
Normal file
5
frontend/infra/idl/idl-parser/__tests__/idl/index.thrift
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
service Foo {
|
||||
} (
|
||||
api.uri_prefix = 'https://example.com'
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "unify_dependent2.proto";
|
||||
import "./dep/common.proto";
|
||||
|
||||
package unify_dep1;
|
||||
|
||||
message Foo {
|
||||
string f_key1 = 1;
|
||||
common.Common f_key2 = 2;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
include 'unify_dependent2.thrift'
|
||||
include './dep/common.thrift'
|
||||
|
||||
namespace js unify_dep1
|
||||
|
||||
typedef Foo Foo1
|
||||
|
||||
struct Foo {
|
||||
1: string f_key1
|
||||
2: common.Common f_key2
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "./unify_dependent1.proto";
|
||||
|
||||
package unify_idx;
|
||||
|
||||
enum Number {
|
||||
ONE = 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
include './unify_dependent1.thrift'
|
||||
|
||||
enum Number {
|
||||
ONE = 1,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
include "./unify_base.thrift"
|
||||
include "./unify_base1.thrift"
|
||||
@@ -0,0 +1,29 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "./unify_dependent1.proto";
|
||||
import "unify_dependent2.proto";
|
||||
|
||||
package unify_idx;
|
||||
|
||||
// c0
|
||||
enum Gender {
|
||||
// c1
|
||||
MALE = 1; // c2
|
||||
// c3
|
||||
FEMAL = 2; // c4
|
||||
}
|
||||
|
||||
/* cm1 */
|
||||
message Request {
|
||||
// cm2
|
||||
// repeated string key1 = 1[(api.key) = 'f'];
|
||||
// unify_dep1.Foo key2 = 2;
|
||||
Number key3 = 3 [(api.position) = 'query'];
|
||||
}
|
||||
|
||||
service Example {
|
||||
option (api.uri_prefix) = "//example.com";
|
||||
rpc Biz1(Request) returns (Number) {
|
||||
option (api.uri) = '/api/biz1';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace go unify_idx
|
||||
|
||||
include './unify_dependent1.thrift'
|
||||
include 'unify_dependent2.thrift'
|
||||
|
||||
typedef unify_dependent1.Foo TFoo
|
||||
|
||||
union FuncRequest {
|
||||
1: unify_dependent1.Foo r_key1
|
||||
2: TFoo r_key2
|
||||
}
|
||||
|
||||
struct FuncResponse {
|
||||
1: unify_dependent2.Number key2
|
||||
}
|
||||
|
||||
service Example {
|
||||
FuncResponse Func(1: FuncRequest req)
|
||||
} (
|
||||
api.uri_prefix = 'https://example.com'
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "base.proto";
|
||||
|
||||
message Foo {
|
||||
Base key1 = 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
include 'base.thrift'
|
||||
|
||||
struct Foo {
|
||||
1: base.Base key1
|
||||
}
|
||||
12
frontend/infra/idl/idl-parser/__tests__/idl/weird.proto
Normal file
12
frontend/infra/idl/idl-parser/__tests__/idl/weird.proto
Normal file
@@ -0,0 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Common {}
|
||||
|
||||
service Example {
|
||||
rpc Func1 (Common) returns (Common) {
|
||||
option (google.api.http) = {
|
||||
get: "/ezo/web/v1/user_camp_result"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
140
frontend/infra/idl/idl-parser/__tests__/proto.field.test.ts
Normal file
140
frontend/infra/idl/idl-parser/__tests__/proto.field.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/proto';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('proto field', () => {
|
||||
it('should convert message field extenstions', () => {
|
||||
const idl = `
|
||||
syntax = "proto3";
|
||||
enum Numbers {
|
||||
ONE = 1;
|
||||
}
|
||||
message Foo {
|
||||
string k1 = 1 [(api.position) = "query"];
|
||||
string k2 = 2 [(api.position) = 'body'];
|
||||
string k3 = 3 [(api.position) = 'path'];
|
||||
string k4 = 4 [(api.position) = 'header'];
|
||||
string k5 = 5 [(api.position) = 'entire_body'];
|
||||
string k6 = 6 [(api.position) = 'raw_body', (aapi.position) = 'raw_body'];
|
||||
string k7 = 7 [(api.position) = 'status_code', (api.positionn) = 'raw_body'];
|
||||
string k10 = 10 [(api.key) = 'key10'];
|
||||
string k11 = 11 [(api.key) = 'k11'];
|
||||
bytes k12 = 12 [(api.web_type) = 'File'];
|
||||
int32 k21 = 21 [(api.query) = 'k21'];
|
||||
int32 k22 = 22 [(api.body) = 'k22'];
|
||||
int32 k23 = 23 [(api.path) = 'k23'];
|
||||
int32 k24 = 24 [(api.header) = 'k24'];
|
||||
int32 k25 = 25 [(api.entire_body) = 'key25'];
|
||||
int32 k26 = 26 [(api.raw_body) = 'key_26'];
|
||||
int32 k27 = 27 [(api.status_code) = 'key-27'];
|
||||
int32 k31 = 31 [(api.query) = 'key31', (api.web_type) = 'number', (api.position) = ''];
|
||||
int32 k32 = 32 [(api.position) = 'body', (api.key)='key32', (api.value_type) = 'any'];
|
||||
int32 k33 = 33 [(api.method) = 'POST', (api.position) = 'QUERY'];
|
||||
int32 k34 = 34 ;
|
||||
Numbers k35 = 35 [(api.position) = 'path'];
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ position: 'query' },
|
||||
{ position: 'body' },
|
||||
{ position: 'path' },
|
||||
{ position: 'header' },
|
||||
{ position: 'entire_body' },
|
||||
{ position: 'raw_body' },
|
||||
{ position: 'status_code' },
|
||||
{ key: 'key10' },
|
||||
{},
|
||||
{ web_type: 'File' },
|
||||
{ position: 'query' },
|
||||
{ position: 'body' },
|
||||
{ position: 'path' },
|
||||
{ position: 'header' },
|
||||
{ position: 'entire_body', key: 'key25' },
|
||||
{ position: 'raw_body', key: 'key_26' },
|
||||
{ position: 'status_code', key: 'key-27' },
|
||||
{ position: 'query', key: 'key31', web_type: 'number' },
|
||||
{ position: 'body', key: 'key32', value_type: 'any' },
|
||||
{},
|
||||
undefined,
|
||||
{ position: 'path' },
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const Foo = (document.root.nested || {}).Foo as t.MessageDefinition;
|
||||
const extensionConfigs = Object.values(Foo.fields).map(
|
||||
field => field.extensionConfig,
|
||||
);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert message field extenstions using old rules', () => {
|
||||
const idl = `
|
||||
syntax = "proto3";
|
||||
message Foo {
|
||||
int32 k1 = 1 [(api_req).query = 'k1'];
|
||||
int32 k2 = 2 [(api_req).body = 'k2'];
|
||||
int32 k3 = 3 [(api_req).path = 'k3'];
|
||||
int32 k4 = 4 [(api_req).header = 'k4'];
|
||||
int32 k6 = 5 [(api_req).raw_body = 'key5'];
|
||||
int32 k5 = 6 [(api_resp).header = 'key6'];
|
||||
int32 k7 = 7 [(api_resp).http_code = 'key7'];
|
||||
string k8 = 8 [(api_resp).body = 'k8'];
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ position: 'query' },
|
||||
{ position: 'body' },
|
||||
{ position: 'path' },
|
||||
{ position: 'header' },
|
||||
{ position: 'raw_body', key: 'key5' },
|
||||
{ position: 'header', key: 'key6' },
|
||||
{},
|
||||
{ position: 'body' },
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const Foo = (document.root.nested || {}).Foo as t.MessageDefinition;
|
||||
const extensionConfigs = Object.values(Foo.fields).map(
|
||||
field => field.extensionConfig,
|
||||
);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should throw an error when using invalid type for a path parameter', () => {
|
||||
const idl = `
|
||||
syntax = "proto3";
|
||||
message Foo {
|
||||
bool k1 = 1 [(api.position) = "path"];
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
const expected =
|
||||
"the type of path parameter 'k1' in 'Foo' should be string or integer";
|
||||
return expect(message).to.equal(expected);
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
frontend/infra/idl/idl-parser/__tests__/proto.index.test.ts
Normal file
77
frontend/infra/idl/idl-parser/__tests__/proto.index.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import * as t from '../src/proto';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('proto index', () => {
|
||||
it('should convert the file content', () => {
|
||||
const idl = path.resolve(__dirname, 'idl/index.proto');
|
||||
const expected = { uri_prefix: '//example.com' };
|
||||
const document = t.parse(idl);
|
||||
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
|
||||
return expect(Foo.extensionConfig).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should throw an error due to invalid file path', () => {
|
||||
const idl = path.resolve(__dirname, 'idl/indexx.proto');
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
return expect(message).to.includes('no such file:');
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
|
||||
it('should throw an syntax error', () => {
|
||||
const idl = `
|
||||
syntax = "proto3";
|
||||
message Foo {
|
||||
string k1 = 1;,
|
||||
}
|
||||
`;
|
||||
const expected = "illegal token ','(source:4:0)";
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
return expect(message).to.equal(expected);
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
|
||||
it('should throw an syntax error in the file content', () => {
|
||||
const idl = path.resolve(__dirname, 'idl/error.proto');
|
||||
const expected = '__tests__/idl/error.proto:3:0)';
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
return expect(message).to.includes(expected);
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
108
frontend/infra/idl/idl-parser/__tests__/proto.method.test.ts
Normal file
108
frontend/infra/idl/idl-parser/__tests__/proto.method.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/proto';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('proto method', () => {
|
||||
it('should convert method extenstions', () => {
|
||||
const idl = `
|
||||
syntax = 'proto3';
|
||||
message BizRequest {}
|
||||
message BizResponse {}
|
||||
service Foo {
|
||||
rpc Biz1(BizRequest) returns (BizResponse) {
|
||||
option (api.uri) = '/api/biz1';
|
||||
}
|
||||
rpc Biz2(BizRequest) returns (BizResponse) {
|
||||
option (api.method) = "POST";
|
||||
option (api.uri) = "/api/biz2";
|
||||
option (api.serializer) = "json";
|
||||
option (api.group) = 'user';
|
||||
}
|
||||
rpc Biz3(BizRequest) returns (BizResponse) {
|
||||
option (api.get) ='/api/biz3';
|
||||
option (api.serializer) ='form';
|
||||
}
|
||||
rpc Biz4(BizRequest) returns (BizResponse) {
|
||||
option (api.post) ='/api/biz4';
|
||||
option (api.serializer) ='urlencoded';
|
||||
}
|
||||
rpc Biz5(BizRequest) returns (BizResponse) {
|
||||
option (api.put) ='/api/biz5';
|
||||
}
|
||||
rpc Biz6(BizRequest) returns (BizResponse) {
|
||||
option (api.delete) ='/api/biz6';
|
||||
}
|
||||
rpc Biz7(BizRequest) returns (BizResponse);
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ uri: '/api/biz1' },
|
||||
{
|
||||
method: 'POST',
|
||||
uri: '/api/biz2',
|
||||
serializer: 'json',
|
||||
group: 'user',
|
||||
},
|
||||
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
|
||||
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
|
||||
{ method: 'PUT', uri: '/api/biz5' },
|
||||
{ method: 'DELETE', uri: '/api/biz6' },
|
||||
undefined,
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
|
||||
const extensionConfigs = Object.values(Foo.methods).map(
|
||||
func => func.extensionConfig,
|
||||
);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert method extenstions using old rules', () => {
|
||||
const idl = `
|
||||
syntax = 'proto3';
|
||||
message BizRequest {}
|
||||
message BizResponse {}
|
||||
service Foo {
|
||||
rpc Biz1(BizRequest) returns (BizResponse) {
|
||||
option (api_method).get = "/api/biz1";
|
||||
option (api_method).serializer = "json";
|
||||
}
|
||||
|
||||
rpc Biz2(BizRequest) returns (BizResponse) {
|
||||
option (pb_idl.api_method).post = "/api/biz2";
|
||||
option (pb_idl.api_method).serializer = "form";
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ method: 'GET', uri: '/api/biz1', serializer: 'json' },
|
||||
{ method: 'POST', uri: '/api/biz2', serializer: 'form' },
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
|
||||
const extensionConfigs = Object.values(Foo.methods).map(
|
||||
func => func.extensionConfig,
|
||||
);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/proto';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('proto service', () => {
|
||||
it('should convert service extenstions', () => {
|
||||
const idl = `
|
||||
syntax = "proto3";
|
||||
service Foo {
|
||||
option (api.uri_prefix) = "//example.com";
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = { uri_prefix: '//example.com' };
|
||||
const document = t.parse(idl);
|
||||
const Foo = (document.root.nested || {}).Foo as t.ServiceDefinition;
|
||||
return expect(Foo.extensionConfig).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert service extenstions with package', () => {
|
||||
const idl = `
|
||||
syntax = "proto3";
|
||||
package example;
|
||||
service Foo {
|
||||
option (api.uri_prefix) = "//example.com";
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = { uri_prefix: '//example.com' };
|
||||
const document = t.parse(idl);
|
||||
const Foo = ((document.root.nested || {}).example.nested || {})
|
||||
.Foo as t.ServiceDefinition;
|
||||
return expect(Foo.extensionConfig).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
55
frontend/infra/idl/idl-parser/__tests__/thrift.enum.test.ts
Normal file
55
frontend/infra/idl/idl-parser/__tests__/thrift.enum.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/thrift';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('thrift enum', () => {
|
||||
it('should convert enum member comments', () => {
|
||||
const idl = `
|
||||
enum Bar {
|
||||
// c1
|
||||
ONE = 1, // c2
|
||||
/* c3 */
|
||||
TWO = 2, /* c4 */
|
||||
// c5
|
||||
/* c6 */
|
||||
THTEE = 3, // c7
|
||||
/* c8
|
||||
c9 */
|
||||
FOUR = 4
|
||||
// c10
|
||||
FIVE = 5; /* c11 */
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
['c1', 'c2'],
|
||||
[['c3'], ['c4']],
|
||||
['c5', ['c6'], 'c7'],
|
||||
[['c8', ' c9']],
|
||||
['c10', ['c11']],
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { members } = document.body[0] as t.EnumDefinition;
|
||||
const comments = members.map(member =>
|
||||
member.comments.map(comment => comment.value),
|
||||
);
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
226
frontend/infra/idl/idl-parser/__tests__/thrift.field.test.ts
Normal file
226
frontend/infra/idl/idl-parser/__tests__/thrift.field.test.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/thrift';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('thrift field', () => {
|
||||
it('should convert struct field extenstions', () => {
|
||||
const idl = `
|
||||
enum Numbers {
|
||||
ONE = 1
|
||||
}
|
||||
struct Foo {
|
||||
1: string k1 (api.position = "query")
|
||||
2: string k2 (api.position = 'body', aapi.position = 'query')
|
||||
3: string k3 (api.position = 'path', api.positionn = 'query')
|
||||
4: string k4 (api.position = 'header')
|
||||
5: string k5 (api.position = 'entire_body')
|
||||
6: string k6 (api.position = 'raw_body')
|
||||
7: string k7 (api.position = 'status_code')
|
||||
10: string k10 (api.key = 'key10')
|
||||
11: string k11 (api.key = 'k11')
|
||||
12: binary k12 (api.web_type = 'File')
|
||||
13: string k13 (api.value_type = 'any')
|
||||
14: list<string> k14 (api.value_type = 'any')
|
||||
21: i32 k21 (api.query = 'k21[]')
|
||||
22: i32 k22 (api.body = 'k22')
|
||||
23: i32 k23 (api.path = 'k23')
|
||||
24: i32 k24 (api.header = 'k24')
|
||||
25: i32 k25 (api.entire_body = 'key25')
|
||||
26: i32 k26 (api.raw_body = 'key_26')
|
||||
27: i32 k27 (api.status_code = 'key-27')
|
||||
31: i32 k31 (api.query = 'key31', api.web_type = 'number', api.position = '')
|
||||
32: i32 k32 (api.position = 'body', api.key='key32', api.value_type = 'any')
|
||||
33: i64 k33 (api.body="kk33, omitempty")
|
||||
34: Numbers k34 (api.position = 'path')
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ position: 'query' },
|
||||
{ position: 'body' },
|
||||
{ position: 'path' },
|
||||
{ position: 'header' },
|
||||
{ position: 'entire_body' },
|
||||
{ position: 'raw_body' },
|
||||
{ position: 'status_code' },
|
||||
{ key: 'key10' },
|
||||
{},
|
||||
{ web_type: 'File' },
|
||||
{ value_type: 'any' },
|
||||
{ value_type: 'any' },
|
||||
{ position: 'query' },
|
||||
{ position: 'body' },
|
||||
{ position: 'path' },
|
||||
{ position: 'header' },
|
||||
{ position: 'entire_body', key: 'key25' },
|
||||
{ position: 'raw_body', key: 'key_26' },
|
||||
{ position: 'status_code', key: 'key-27' },
|
||||
{ position: 'query', key: 'key31', web_type: 'number' },
|
||||
{ position: 'body', key: 'key32', value_type: 'any' },
|
||||
{ position: 'body', key: 'kk33', tag: 'omitempty' },
|
||||
{ position: 'path' },
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { fields } = document.body[1] as t.InterfaceWithFields;
|
||||
const extensionConfigs = fields.map(field => field.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert union field extenstions', () => {
|
||||
const idl = `
|
||||
union Foo {
|
||||
1: string k1 (api.position = "query")
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [{ position: 'query' }];
|
||||
|
||||
const document = t.parse(idl, { reviseTailComment: false });
|
||||
const { fields } = document.body[0] as t.InterfaceWithFields;
|
||||
const extensionConfigs = fields.map(field => field.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert struct field extenstions using agw specification', () => {
|
||||
const idl = `
|
||||
struct Foo {
|
||||
1: string k1 (agw.source = 'query')
|
||||
2: string k2 (agw.source = 'body')
|
||||
3: string k3 (agw.source = 'path')
|
||||
4: string k4 (agw.source = 'header')
|
||||
5: string k5 (agw.source = 'raw_body')
|
||||
6: string k6 (agw.target = 'header')
|
||||
7: string k7 (agw.target = 'body')
|
||||
7: string k7 (agw.target = 'http_code')
|
||||
10: string k10 (agw.key = 'key10')
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ position: 'query' },
|
||||
{ position: 'body' },
|
||||
{ position: 'path' },
|
||||
{ position: 'header' },
|
||||
{ position: 'raw_body' },
|
||||
{ position: 'header' },
|
||||
{ position: 'body' },
|
||||
{ position: 'status_code' },
|
||||
{ key: 'key10' },
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { fields } = document.body[0] as t.InterfaceWithFields;
|
||||
const extensionConfigs = fields.map(field => field.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert struct field extenstions using golang tag', () => {
|
||||
const idl = `
|
||||
struct Foo {
|
||||
1: string k1 (go.tag = "json:\\"key1\\"")
|
||||
2: string k2 (go.tag = 'json:"key2,omitempty"')
|
||||
3: string k3 (go.tag = 'jsonn:"key2,omitempty"')
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [{ key: 'key1' }, { key: 'key2', tag: 'omitempty' }, {}];
|
||||
const document = t.parse(idl);
|
||||
const { fields } = document.body[0] as t.InterfaceWithFields;
|
||||
const extensionConfigs = fields.map(field => field.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should throw an error when using invalid type for a path parameter', () => {
|
||||
const idl = `
|
||||
struct Foo {
|
||||
1: bool k1 (api.position = 'path')
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
const expected =
|
||||
"the type of path parameter 'k1' in 'Foo' should be string or integer";
|
||||
return expect(message).to.equal(expected);
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
|
||||
it('should revise field comments', () => {
|
||||
const idl = `
|
||||
struct Foo {
|
||||
// c1
|
||||
1: string k1 // c2
|
||||
/* c3 */
|
||||
2: string k2 /* c4 */
|
||||
// c5
|
||||
/* c6 */
|
||||
3: string k3 // c7
|
||||
/* c8
|
||||
c9 */
|
||||
4: string k4
|
||||
// c10
|
||||
5: string k5; /* c11 */
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
['c1', 'c2'],
|
||||
[['c3'], ['c4']],
|
||||
['c5', ['c6'], 'c7'],
|
||||
[['c8', ' c9']],
|
||||
['c10', ['c11']],
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { fields } = document.body[0] as t.InterfaceWithFields;
|
||||
const comments = fields.map(field =>
|
||||
field.comments.map(comment => comment.value),
|
||||
);
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should revise empty field comments', () => {
|
||||
const idl = `
|
||||
/*
|
||||
*/
|
||||
struct Foo {
|
||||
/**/
|
||||
1: string k1
|
||||
/* */
|
||||
2: string k2
|
||||
/** */
|
||||
3: string k3
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [[['']], [['']], [['']]];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { fields } = document.body[0] as t.InterfaceWithFields;
|
||||
const comments = fields.map(field =>
|
||||
field.comments.map(comment => comment.value),
|
||||
);
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
116
frontend/infra/idl/idl-parser/__tests__/thrift.function.test.ts
Normal file
116
frontend/infra/idl/idl-parser/__tests__/thrift.function.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/thrift';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('thrift function', () => {
|
||||
it('should convert function extenstions', () => {
|
||||
const idl = `
|
||||
service Foo {
|
||||
BizResponse Biz1(1: BizRequest req) (api.uri = '/api/biz1')
|
||||
BizResponse Biz2(1: BizRequest req) (
|
||||
api.uri = '/api/biz2',
|
||||
api.serializer = 'json',
|
||||
api.method = 'POST',
|
||||
api.group="user"
|
||||
)
|
||||
BizResponse Biz3(1: BizRequest req) (api.get = '/api/biz3', api.serializer='form')
|
||||
BizResponse Biz4(1: BizRequest req) (api.post = '/api/biz4', api.serializer='urlencoded')
|
||||
BizResponse Biz5(1: BizRequest req) (api.put = '/api/biz5', api.method = 'post')
|
||||
BizResponse Biz6(1: BizRequest req) (api.delete = '/api/biz6', api.serializer='wow')
|
||||
BizResponse Biz7(1: BizRequest req)
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ uri: '/api/biz1' },
|
||||
{
|
||||
uri: '/api/biz2',
|
||||
serializer: 'json',
|
||||
method: 'POST',
|
||||
group: 'user',
|
||||
},
|
||||
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
|
||||
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
|
||||
{ method: 'PUT', uri: '/api/biz5' },
|
||||
{ method: 'DELETE', uri: '/api/biz6' },
|
||||
undefined,
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { functions } = document.body[0] as t.ServiceDefinition;
|
||||
const extensionConfigs = functions.map(func => func.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert function extenstions using agw specification', () => {
|
||||
const idl = `
|
||||
service Foo {
|
||||
BizResponse Biz1(1: BizRequest req) (agw.uri = '/api/biz1')
|
||||
BizResponse Biz2(1: BizRequest req) (
|
||||
agw.uri = '/api/biz2',
|
||||
agw.method = 'POST',
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ uri: '/api/biz1' },
|
||||
{ uri: '/api/biz2', method: 'POST' },
|
||||
];
|
||||
|
||||
const document = t.parse(idl, { reviseTailComment: false });
|
||||
const { functions } = document.body[0] as t.ServiceDefinition;
|
||||
const extensionConfigs = functions.map(func => func.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should revise function comments', () => {
|
||||
const idl = `
|
||||
service Foo {
|
||||
// c1
|
||||
BizResponse Biz1(1: BizRequest req) // c2
|
||||
/* c3 */
|
||||
BizResponse Biz2(1: BizRequest req) /* c4 */
|
||||
// c5
|
||||
/* c6 */
|
||||
BizResponse Biz3(1: BizRequest req) // c7
|
||||
/* c8
|
||||
c9 */
|
||||
BizResponse Biz4(1: BizRequest req)
|
||||
// c10
|
||||
BizResponse Biz5(1: BizRequest req); /* c11 */
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
['c1', 'c2'],
|
||||
[['c3'], ['c4']],
|
||||
['c5', ['c6'], 'c7'],
|
||||
[['c8', ' c9']],
|
||||
['c10', ['c11']],
|
||||
];
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { functions } = document.body[0] as t.ServiceDefinition;
|
||||
const comments = functions.map(func =>
|
||||
func.comments.map(comment => comment.value),
|
||||
);
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
79
frontend/infra/idl/idl-parser/__tests__/thrift.index.test.ts
Normal file
79
frontend/infra/idl/idl-parser/__tests__/thrift.index.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import * as t from '../src/thrift';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('thrift index', () => {
|
||||
it('should convert the file content', () => {
|
||||
const idl = path.resolve(__dirname, 'idl/index.thrift');
|
||||
const expected = { uri_prefix: 'https://example.com' };
|
||||
|
||||
const document = t.parse(idl);
|
||||
const { extensionConfig } = document.body[0] as t.ServiceDefinition;
|
||||
return expect(extensionConfig).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should throw an error due to invalid file path', () => {
|
||||
const idl = path.resolve(__dirname, 'idl/indexx.thrift');
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
return expect(message).to.includes('no such file:');
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
|
||||
it('should throw an syntax error', () => {
|
||||
const idl = `
|
||||
struct Foo {
|
||||
1: string k1,,
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = 'FieldType expected but found: CommaToken(source:3:';
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
return expect(message).to.include(expected);
|
||||
}
|
||||
|
||||
return expect(true).to.equal(false);
|
||||
});
|
||||
|
||||
it('should throw an syntax error in the file content', () => {
|
||||
const idl = path.resolve(__dirname, 'idl/error.thrift');
|
||||
|
||||
const expected = '__tests__/idl/error.thrift:2:16)';
|
||||
|
||||
try {
|
||||
t.parse(idl);
|
||||
} catch (err) {
|
||||
const { message } = err;
|
||||
return expect(message).includes(expected);
|
||||
}
|
||||
|
||||
return expect(true).equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src/thrift';
|
||||
|
||||
describe('ferry-parser', () => {
|
||||
describe('thrift service', () => {
|
||||
it('should convert service extenstions', () => {
|
||||
const idl = `
|
||||
service Foo {
|
||||
} (api.uri_prefix = 'https://example.com')
|
||||
`;
|
||||
|
||||
const expected = { uri_prefix: 'https://example.com' };
|
||||
const document = t.parse(idl);
|
||||
const { extensionConfig } = document.body[0] as t.ServiceDefinition;
|
||||
return expect(extensionConfig).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
7
frontend/infra/idl/idl-parser/__tests__/tsconfig.json
Normal file
7
frontend/infra/idl/idl-parser/__tests__/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
174
frontend/infra/idl/idl-parser/__tests__/unify.enum.test.ts
Normal file
174
frontend/infra/idl/idl-parser/__tests__/unify.enum.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src';
|
||||
import { filterKeys } from './common';
|
||||
|
||||
describe('unify-parser', () => {
|
||||
describe('thrift enum', () => {
|
||||
it('should convert enum', () => {
|
||||
const content = `
|
||||
enum Number {
|
||||
ONE = 1,
|
||||
TWO,
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { members } = document.statements[0] as t.EnumDefinition;
|
||||
return expect(members.length).to.eql(2);
|
||||
});
|
||||
|
||||
it('should resolve enum name', () => {
|
||||
const content = `
|
||||
enum Number {
|
||||
ONE = 1,
|
||||
TWO,
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { name } = document.statements[0] as t.EnumDefinition;
|
||||
expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
|
||||
value: 'Number',
|
||||
namespaceValue: 'root.Number',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not resolve enum name', () => {
|
||||
const content = `
|
||||
enum Number {
|
||||
ONE = 1,
|
||||
TWO,
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ namespaceRefer: false, cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { name } = document.statements[0] as t.EnumDefinition;
|
||||
expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
|
||||
value: 'Number',
|
||||
namespaceValue: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should revise enum comments', () => {
|
||||
const content = `
|
||||
enum Number {
|
||||
// c1
|
||||
ONE = 1, // c2
|
||||
/* c3 */
|
||||
TWO,/* c4 */
|
||||
// c5
|
||||
/* c6 */
|
||||
FOUR = 4; // c7
|
||||
/* c8
|
||||
c9 */
|
||||
FIVE;
|
||||
SIX,
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
['c1', 'c2'],
|
||||
[['c3'], ['c4']],
|
||||
['c5', ['c6'], 'c7'],
|
||||
[['c8', ' c9']],
|
||||
[],
|
||||
];
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { members } = document.statements[0] as t.EnumDefinition;
|
||||
const comments = members.map(field =>
|
||||
field.comments.map(comment => comment.value),
|
||||
);
|
||||
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should revise enum comments without dot', () => {
|
||||
const content = `
|
||||
enum Number {
|
||||
// c1
|
||||
ONE = 1 // c2
|
||||
/* c3 */
|
||||
TWO/* c4 */
|
||||
// c5
|
||||
/* c6 */
|
||||
FOUR = 4 // c7
|
||||
/* c8
|
||||
c9 */
|
||||
FIVE
|
||||
SIX
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
['c1', 'c2'],
|
||||
[['c3'], ['c4']],
|
||||
['c5', ['c6'], 'c7'],
|
||||
[['c8', ' c9']],
|
||||
[],
|
||||
];
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { members } = document.statements[0] as t.EnumDefinition;
|
||||
const comments = members.map(field =>
|
||||
field.comments.map(comment => comment.value),
|
||||
);
|
||||
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proto enum', () => {
|
||||
it('should convert enum', () => {
|
||||
const content = `
|
||||
enum Number {
|
||||
ONE = 1;
|
||||
TWO = 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.proto',
|
||||
{ cache: false },
|
||||
{ 'index.proto': content },
|
||||
);
|
||||
const { members } = document.statements[0] as t.EnumDefinition;
|
||||
return expect(members.length).to.eql(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
1510
frontend/infra/idl/idl-parser/__tests__/unify.field.test.ts
Normal file
1510
frontend/infra/idl/idl-parser/__tests__/unify.field.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
343
frontend/infra/idl/idl-parser/__tests__/unify.function.test.ts
Normal file
343
frontend/infra/idl/idl-parser/__tests__/unify.function.test.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as t from '../src';
|
||||
import { filterKeys } from './common';
|
||||
|
||||
describe('unify-parser', () => {
|
||||
describe('thrift function', () => {
|
||||
it('should convert function extenstions', () => {
|
||||
const content = `
|
||||
service Foo {
|
||||
BizResponse Biz1(1: BizRequest req) (api.uri = '/api/biz1')
|
||||
BizResponse Biz2(1: BizRequest req) (
|
||||
api.uri = '/api/biz2',
|
||||
api.serializer = 'json',
|
||||
api.method = 'POST',
|
||||
api.group="user"
|
||||
)
|
||||
BizResponse Biz3(1: BizRequest req) (api.get = '/api/biz3', api.serializer='form')
|
||||
BizResponse Biz4(1: BizRequest req) (api.post = '/api/biz4', api.serializer='urlencoded')
|
||||
BizResponse Biz5(1: BizRequest req) (api.put = '/api/biz5', api.method = 'post', api.version='1')
|
||||
BizResponse Biz6(1: BizRequest req) (api.delete = '/api/biz6', api.serializer='wow', api.custom = '{"priority":1}')
|
||||
BizResponse Biz7(1: BizRequest req)
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ uri: '/api/biz1' },
|
||||
{
|
||||
uri: '/api/biz2',
|
||||
serializer: 'json',
|
||||
method: 'POST',
|
||||
group: 'user',
|
||||
},
|
||||
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
|
||||
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
|
||||
{ method: 'PUT', uri: '/api/biz5', version: '1' },
|
||||
{ method: 'DELETE', uri: '/api/biz6', custom: '{"priority":1}' },
|
||||
{},
|
||||
];
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
const extensionConfigs = functions.map(func => func.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should convert function extenstions using agw specification', () => {
|
||||
const content = `
|
||||
service Foo {
|
||||
BizResponse Biz1(1: BizRequest req) (agw.uri = '/api/biz1')
|
||||
BizResponse Biz2(1: BizRequest req) (
|
||||
agw.uri = '/api/biz2',
|
||||
agw.method = 'POST',
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ uri: '/api/biz1' },
|
||||
{ uri: '/api/biz2', method: 'POST' },
|
||||
];
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
const extensionConfigs = functions.map(func => func.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should revise function comments', () => {
|
||||
const content = `
|
||||
service Foo {
|
||||
// c1
|
||||
BizResponse Biz1(1: BizRequest req) // c2
|
||||
/* c3 */
|
||||
BizResponse Biz2(1: BizRequest req) /* c4 */
|
||||
// c5
|
||||
/* c6 */
|
||||
BizResponse Biz3(1: BizRequest req) // c7
|
||||
/* c8
|
||||
c9 */
|
||||
BizResponse Biz4(1: BizRequest req)
|
||||
BizResponse Biz5(1: BizRequest req)
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
['c1', 'c2'],
|
||||
[['c3'], ['c4']],
|
||||
['c5', ['c6'], 'c7'],
|
||||
[['c8', ' c9']],
|
||||
[],
|
||||
];
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
const comments = functions.map(func =>
|
||||
func.comments.map(comment => comment.value),
|
||||
);
|
||||
return expect(comments).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should resolve function name', () => {
|
||||
const content = `
|
||||
service Foo {
|
||||
BizResponse Biz1(1: BizRequest req) // c2
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{ 'index.thrift': content },
|
||||
);
|
||||
const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
const { name } = functions[0];
|
||||
return expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
|
||||
value: 'Biz1',
|
||||
namespaceValue: 'root.Biz1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve func type', () => {
|
||||
const baseContent = `
|
||||
namespace go test_base
|
||||
struct Response {}
|
||||
`;
|
||||
const funcContent = `
|
||||
include "./base.thrift"
|
||||
service Foo {
|
||||
base.Response Biz1(1: BizRequest req) // c2
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.thrift',
|
||||
{ cache: false },
|
||||
{
|
||||
'base.thrift': baseContent,
|
||||
'index.thrift': funcContent,
|
||||
},
|
||||
);
|
||||
|
||||
const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
const identifier = functions[0].returnType as t.Identifier;
|
||||
return expect(filterKeys(identifier, ['value', 'namespaceValue'])).to.eql(
|
||||
{
|
||||
value: 'base.Response',
|
||||
namespaceValue: 'test_base.Response',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proto method', () => {
|
||||
it('should convert method extenstions', () => {
|
||||
const content = `
|
||||
syntax = 'proto3';
|
||||
message BizRequest {}
|
||||
message BizResponse {}
|
||||
service Foo {
|
||||
rpc Biz1(BizRequest) returns (BizResponse) {
|
||||
option (api.uri) = '/api/biz1';
|
||||
}
|
||||
rpc Biz2(BizRequest) returns (BizResponse) {
|
||||
option (api.method) = "POST";
|
||||
option (api.uri) = "/api/biz2";
|
||||
option (api.serializer) = "json";
|
||||
option (api.group) = 'user';
|
||||
}
|
||||
rpc Biz3(BizRequest) returns (BizResponse) {
|
||||
option (api.get) ='/api/biz3';
|
||||
option (api.serializer) ='form';
|
||||
}
|
||||
rpc Biz4(BizRequest) returns (BizResponse) {
|
||||
option (api.post) ='/api/biz4';
|
||||
option (api.serializer) ='urlencoded';
|
||||
}
|
||||
rpc Biz5(BizRequest) returns (BizResponse) {
|
||||
option (api.put) ='/api/biz5';
|
||||
}
|
||||
rpc Biz6(BizRequest) returns (BizResponse) {
|
||||
option (api.delete) ='/api/biz6';
|
||||
}
|
||||
rpc Biz7(BizRequest) returns (BizResponse);
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = [
|
||||
{ uri: '/api/biz1' },
|
||||
{
|
||||
method: 'POST',
|
||||
uri: '/api/biz2',
|
||||
serializer: 'json',
|
||||
group: 'user',
|
||||
},
|
||||
{ method: 'GET', uri: '/api/biz3', serializer: 'form' },
|
||||
{ method: 'POST', uri: '/api/biz4', serializer: 'urlencoded' },
|
||||
{ method: 'PUT', uri: '/api/biz5' },
|
||||
{ method: 'DELETE', uri: '/api/biz6' },
|
||||
{},
|
||||
];
|
||||
|
||||
const document = t.parse(
|
||||
'index.proto',
|
||||
{ cache: false },
|
||||
{ 'index.proto': content },
|
||||
);
|
||||
const { functions } = document.statements[2] as t.ServiceDefinition;
|
||||
const extensionConfigs = functions.map(func => func.extensionConfig);
|
||||
return expect(extensionConfigs).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should resolve method name', () => {
|
||||
const content = `
|
||||
syntax = 'proto3';
|
||||
message BizRequest {}
|
||||
message BizResponse {}
|
||||
service Foo {
|
||||
rpc Biz1(BizRequest) returns (BizResponse) {
|
||||
option (api.uri) = '/api/biz1';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.proto',
|
||||
{ cache: false },
|
||||
{ 'index.proto': content },
|
||||
);
|
||||
const { functions } = document.statements[2] as t.ServiceDefinition;
|
||||
const { name } = functions[0];
|
||||
return expect(filterKeys(name, ['value', 'namespaceValue'])).to.eql({
|
||||
value: 'Biz1',
|
||||
namespaceValue: 'root.Foo.Biz1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve response type', () => {
|
||||
const baseContent = `
|
||||
syntax = 'proto3';
|
||||
package test_base;
|
||||
message Response {}
|
||||
`;
|
||||
|
||||
const funcContent = `
|
||||
import "base.proto";
|
||||
syntax = 'proto3';
|
||||
message BizRequest {}
|
||||
service Foo {
|
||||
rpc Biz1(BizRequest) returns (test_base.Response) {
|
||||
option (api.uri) = '/api/biz1';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.proto',
|
||||
{ cache: false },
|
||||
{
|
||||
'base.proto': baseContent,
|
||||
'index.proto': funcContent,
|
||||
},
|
||||
);
|
||||
|
||||
const { functions } = document.statements[1] as t.ServiceDefinition;
|
||||
const identifier = functions[0].returnType as t.Identifier;
|
||||
return expect(filterKeys(identifier, ['value', 'namespaceValue'])).to.eql(
|
||||
{
|
||||
value: 'base.Response',
|
||||
namespaceValue: 'test_base.Response',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve response type within the same namespace', () => {
|
||||
const baseContent = `
|
||||
syntax = 'proto3';
|
||||
package same;
|
||||
message Response {}
|
||||
message Request {}
|
||||
`;
|
||||
|
||||
const funcContent = `
|
||||
import "base.proto";
|
||||
syntax = 'proto3';
|
||||
package same;
|
||||
service Foo {
|
||||
rpc Biz1(Request) returns (same.Response) {
|
||||
option (api.uri) = '/api/biz1';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const document = t.parse(
|
||||
'index.proto',
|
||||
{ cache: false },
|
||||
{
|
||||
'base.proto': baseContent,
|
||||
'index.proto': funcContent,
|
||||
},
|
||||
);
|
||||
|
||||
const { functions } = document.statements[0] as t.ServiceDefinition;
|
||||
const returnType = functions[0].returnType as t.Identifier;
|
||||
const requestType = functions[0].fields[0].fieldType as t.Identifier;
|
||||
expect(filterKeys(requestType, ['value', 'namespaceValue'])).to.eql({
|
||||
value: 'base.Request',
|
||||
namespaceValue: 'same.Request',
|
||||
});
|
||||
|
||||
expect(filterKeys(returnType, ['value', 'namespaceValue'])).to.eql({
|
||||
value: 'base.Response',
|
||||
namespaceValue: 'same.Response',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user