feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -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 },
},
],
},
],
});

View File

@@ -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,
};
},
};

View File

@@ -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"' },
},
],
},
],
});

View File

@@ -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),
},
});
}
});
},
};
},
};

View File

@@ -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 },
},
],
},
],
});

View File

@@ -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 },
});
}
}
},
};
},
};

View File

@@ -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' },
},
],
},
],
}),
);

View File

@@ -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 },
});
}
});
},
};
},
};

View File

@@ -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',
},
],
},
],
});

View File

@@ -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',
});
},
};
},
};

View File

@@ -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 },
},
],
},
],
});

View 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})`);
},
});
}
},
};
},
};

View File

@@ -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,
});
}
}
},
};
},
};

View File

@@ -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],
});

View File

@@ -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);
},
);
});

View File

@@ -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;
}

View File

@@ -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: '' },
},
],
},
],
}),
);

View File

@@ -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);
});
});
},
};
},
};

View File

@@ -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',
});
}
}
},
};
},
};

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true
},
"include": ["react.tsx"]
}

View File

@@ -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: [],
});

View File

@@ -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;
},
);

View File

@@ -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' },
},
],
},
],
});

View File

@@ -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 },
});
}
},
};
},
};