feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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 },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user