@apptoolkit/harness 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +32 -2
- package/harness.js +104 -0
- package/package.json +21 -4
- package/payload/HARNESS.md +21 -0
- package/payload/author-style/README.md +56 -0
- package/payload/author-style/biome-core.json +57 -0
- package/payload/author-style/biome-react.json +9 -0
- package/payload/author-style/biome-sql.json +9 -0
- package/payload/author-style/bun.lock +37 -0
- package/payload/author-style/check.llm.md +17 -0
- package/payload/author-style/check.test.llm.md +17 -0
- package/payload/author-style/check.test.ts +238 -0
- package/payload/author-style/check.ts +181 -0
- package/payload/author-style/member-spacing.test.ts +172 -0
- package/payload/author-style/member-spacing.ts +106 -0
- package/payload/author-style/package.json +18 -0
- package/payload/author-style/plugins/class-member-order.grit +133 -0
- package/payload/author-style/plugins/event-handler-names.grit +110 -0
- package/payload/author-style/plugins/explicit-arrow-body.grit +15 -0
- package/payload/author-style/plugins/explicit-source-constructs.grit +10 -0
- package/payload/author-style/plugins/react-arrow-body.grit +85 -0
- package/payload/author-style/plugins/sql-style.grit +125 -0
- package/payload/author-style/react-module.llm.md +17 -0
- package/payload/author-style/react-module.test.ts +150 -0
- package/payload/author-style/react-module.ts +255 -0
- package/payload/author-style/source-policy.llm.md +17 -0
- package/payload/author-style/source-policy.test.ts +70 -0
- package/payload/author-style/source-policy.ts +215 -0
- package/payload/author-style/spacing.llm.md +17 -0
- package/payload/author-style/spacing.test.ts +174 -0
- package/payload/author-style/spacing.ts +211 -0
- package/payload/harness/POLICY.md +33 -0
- package/payload/harness/PROJECT.md +26 -0
- package/payload/harness/REVIEW.md +17 -0
- package/payload/harness/TEMPLATES.md +20 -0
- package/payload/harness/WORKFLOWS.md +65 -0
- package/payload/harness/runtime/ZOD-LICENSE +21 -0
- package/payload/harness/runtime/harness.mjs +104 -0
- package/payload/harness/services/README.md +26 -0
- package/payload/harness/services/penpot.json +7 -0
- package/payload/harness/services/plane.json +9 -0
- package/payload/harness/workflows/W01.md +13 -0
- package/payload/harness/workflows/W02.md +13 -0
- package/payload/harness/workflows/W03.md +13 -0
- package/payload/harness/workflows/W04.md +13 -0
- package/payload/harness/workflows/W05.md +13 -0
- package/payload/harness/workflows/W06.md +13 -0
- package/payload/harness/workflows/W07.md +13 -0
- package/payload/harness/workflows/W08.md +13 -0
- package/payload/harness/workflows/W09.md +13 -0
- package/payload/harness/workflows/W10.md +13 -0
- package/payload/harness/workflows/W11.md +13 -0
- package/payload/harness/workflows/W12.md +13 -0
- package/payload/harness/workflows/W13.md +13 -0
- package/payload/harness/workflows/W14.md +13 -0
- package/payload/harness/workflows/W15.md +13 -0
- package/payload/harness/workflows/W16.md +13 -0
- package/payload/harness/workflows/W17.md +13 -0
- package/payload/harness/workflows/W18.md +13 -0
- package/payload/harness/workflows/W19.md +13 -0
- package/payload/harness/workflows/W20.md +13 -0
- package/payload/harness/workflows/W21.md +15 -0
- package/payload/harness/workflows/W22.md +13 -0
- package/payload/harness/workflows/W23.md +21 -0
- package/payload/harness/workflows/W24.md +13 -0
- package/payload/harness/workflows/W25.md +13 -0
- package/payload/harness/workflows/W26.md +13 -0
- package/payload/harness/workflows/W27.md +13 -0
- package/payload/harness/workflows/W28.md +13 -0
- package/payload/harness/workflows/W29.md +13 -0
- package/payload/harness/workflows/W30.md +13 -0
- package/payload/harness/workflows/W31.md +13 -0
- package/payload/harness/workflows/W32.md +13 -0
- package/payload/harness/workflows/W33.md +13 -0
- package/payload/harness/workflows/W34.md +13 -0
- package/payload/harness/workflows/W35.md +13 -0
- package/payload/harness/workflows/W36.md +13 -0
- package/payload/harness/workflows/W37.md +13 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { findMemberSpacingIssues } from './member-spacing.ts';
|
|
5
|
+
|
|
6
|
+
const memberMessage = 'Leave one blank line between class members.';
|
|
7
|
+
const overloadMessage = 'Keep adjacent signatures and the implementation of an overload together.';
|
|
8
|
+
|
|
9
|
+
function check(lines: readonly string[]) {
|
|
10
|
+
return findMemberSpacingIssues(lines.join('\n'), 'example.ts');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('requires one blank line between authored properties, methods and accessors', () => {
|
|
14
|
+
assert.deepEqual(
|
|
15
|
+
check([
|
|
16
|
+
'class Example {',
|
|
17
|
+
' public value = 1;',
|
|
18
|
+
' public other = 2;',
|
|
19
|
+
'',
|
|
20
|
+
'',
|
|
21
|
+
' public get label() { return this.value; }',
|
|
22
|
+
'',
|
|
23
|
+
' public set label(value: number) { this.value = value; }',
|
|
24
|
+
'}',
|
|
25
|
+
]),
|
|
26
|
+
[
|
|
27
|
+
{ line: 3, column: 3, message: memberMessage },
|
|
28
|
+
{ line: 6, column: 3, message: memberMessage },
|
|
29
|
+
],
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('groups same-name overloads and constructor overloads with their implementations', () => {
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
check([
|
|
36
|
+
'class Example {',
|
|
37
|
+
' public constructor(value: string);',
|
|
38
|
+
' public constructor(value: number);',
|
|
39
|
+
' public constructor(public value: string | number) {}',
|
|
40
|
+
'',
|
|
41
|
+
' public lookup(value: string): string;',
|
|
42
|
+
" public 'lookup'(value: number): number;",
|
|
43
|
+
" public ['lookup'](value: string | number): string | number { return value; }",
|
|
44
|
+
'',
|
|
45
|
+
' public static lookup(value: string): string;',
|
|
46
|
+
' public static lookup(value: number): number;',
|
|
47
|
+
' public static lookup(value: string | number): string | number { return value; }',
|
|
48
|
+
'}',
|
|
49
|
+
]),
|
|
50
|
+
[],
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('reports breaks within overloads and separates distinct overload groups', () => {
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
check([
|
|
57
|
+
'class Example {',
|
|
58
|
+
' public constructor(value: string);',
|
|
59
|
+
'',
|
|
60
|
+
' public constructor(public value: string) {}',
|
|
61
|
+
' public lookup(value: string): string;',
|
|
62
|
+
'',
|
|
63
|
+
' public lookup(value: number): number;',
|
|
64
|
+
' public lookup(value: string | number): string | number { return value; }',
|
|
65
|
+
'',
|
|
66
|
+
' public static lookup(value: string): string;',
|
|
67
|
+
'',
|
|
68
|
+
' public static lookup(value: string): string { return value; }',
|
|
69
|
+
'}',
|
|
70
|
+
]),
|
|
71
|
+
[
|
|
72
|
+
{ line: 4, column: 3, message: overloadMessage },
|
|
73
|
+
{ line: 5, column: 3, message: memberMessage },
|
|
74
|
+
{ line: 7, column: 3, message: overloadMessage },
|
|
75
|
+
{ line: 12, column: 3, message: overloadMessage },
|
|
76
|
+
],
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('keeps bodyless members separate if a matching adjacent member is absent', () => {
|
|
81
|
+
assert.deepEqual(
|
|
82
|
+
check([
|
|
83
|
+
'abstract class Example {',
|
|
84
|
+
' public abstract read(value: string): string;',
|
|
85
|
+
' public static read(value: string): string { return value; }',
|
|
86
|
+
' public abstract write(value: string): void;',
|
|
87
|
+
' public value = 1;',
|
|
88
|
+
'',
|
|
89
|
+
' public writeOther(): void {}',
|
|
90
|
+
'}',
|
|
91
|
+
]),
|
|
92
|
+
[
|
|
93
|
+
{ line: 3, column: 3, message: memberMessage },
|
|
94
|
+
{ line: 4, column: 3, message: memberMessage },
|
|
95
|
+
{ line: 5, column: 3, message: memberMessage },
|
|
96
|
+
],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('groups overload declarations in ambient and abstract classes without requiring a body', () => {
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
check([
|
|
103
|
+
'declare class Ambient {',
|
|
104
|
+
' public lookup(value: string): string;',
|
|
105
|
+
' public lookup(value: number): number;',
|
|
106
|
+
'}',
|
|
107
|
+
'abstract class Example {',
|
|
108
|
+
' public abstract lookup(value: string): string;',
|
|
109
|
+
'',
|
|
110
|
+
' public abstract lookup(value: number): number;',
|
|
111
|
+
'}',
|
|
112
|
+
]),
|
|
113
|
+
[{ line: 8, column: 3, message: overloadMessage }],
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('matches overloads with the same computed or private name', () => {
|
|
118
|
+
assert.deepEqual(
|
|
119
|
+
check([
|
|
120
|
+
'class Example {',
|
|
121
|
+
' public [Symbol.iterator](value: string): string;',
|
|
122
|
+
' public [Symbol.iterator](value: string): string { return value; }',
|
|
123
|
+
'',
|
|
124
|
+
' #lookup(value: string): string;',
|
|
125
|
+
' #lookup(value: string): string { return value; }',
|
|
126
|
+
'}',
|
|
127
|
+
]),
|
|
128
|
+
[],
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('counts spacing outside comments and preserves comment ownership with CRLF', () => {
|
|
133
|
+
const source = [
|
|
134
|
+
'class Example {',
|
|
135
|
+
' public value = 1; /* trailing',
|
|
136
|
+
'',
|
|
137
|
+
' details */',
|
|
138
|
+
' /* leading',
|
|
139
|
+
'',
|
|
140
|
+
' details */',
|
|
141
|
+
' public next = 2;',
|
|
142
|
+
'',
|
|
143
|
+
' // Attached to the following member.',
|
|
144
|
+
' public final = 3;',
|
|
145
|
+
'}',
|
|
146
|
+
].join('\r\n');
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(findMemberSpacingIssues(source, 'example.ts'), [{ line: 8, column: 3, message: memberMessage }]);
|
|
149
|
+
assert.deepEqual(findMemberSpacingIssues(source.replace('*/\r\n /*', '*/\r\n \t\r\n /*'), 'example.ts'), []);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('visits class expressions and nested classes in source order and ignores non-class members', () => {
|
|
153
|
+
const source = [
|
|
154
|
+
'interface Shape { x: number; y: number }',
|
|
155
|
+
'const shape = { x: 1, y: 2 };',
|
|
156
|
+
'const Example = class {',
|
|
157
|
+
' public nested = class {',
|
|
158
|
+
' public first = 1;',
|
|
159
|
+
' public second = 2;',
|
|
160
|
+
' };',
|
|
161
|
+
' static { run(); }',
|
|
162
|
+
'}',
|
|
163
|
+
'class Empty { ; }',
|
|
164
|
+
'class Single { public value = 1; }',
|
|
165
|
+
'export const view = <button>Save</button>;',
|
|
166
|
+
].join('\n');
|
|
167
|
+
|
|
168
|
+
assert.deepEqual(findMemberSpacingIssues(source, 'example.tsx'), [
|
|
169
|
+
{ line: 6, column: 5, message: memberMessage },
|
|
170
|
+
{ line: 8, column: 3, message: memberMessage },
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
type MemberSpacingIssue = { line: number; column: number; message: string };
|
|
4
|
+
type OverloadMember = ts.ConstructorDeclaration | ts.MethodDeclaration;
|
|
5
|
+
|
|
6
|
+
function isOverloadMember(member: ts.ClassElement): member is OverloadMember {
|
|
7
|
+
return ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function methodName(member: ts.MethodDeclaration, file: ts.SourceFile): string {
|
|
11
|
+
const name = member.name;
|
|
12
|
+
|
|
13
|
+
if (ts.isComputedPropertyName(name)) {
|
|
14
|
+
if (ts.isStringLiteral(name.expression) || ts.isNumericLiteral(name.expression)) {
|
|
15
|
+
return `literal:${name.expression.text}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return `computed:${name.getText(file)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (ts.isPrivateIdentifier(name)) {
|
|
22
|
+
return `private:${name.text}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return `literal:${name.text}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function belongsToOverload(signature: OverloadMember, following: OverloadMember, file: ts.SourceFile): boolean {
|
|
29
|
+
if (ts.isConstructorDeclaration(signature) || ts.isConstructorDeclaration(following)) {
|
|
30
|
+
return ts.isConstructorDeclaration(signature) && ts.isConstructorDeclaration(following);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const signatureStatic = ts.getCombinedModifierFlags(signature) & ts.ModifierFlags.Static;
|
|
34
|
+
const followingStatic = ts.getCombinedModifierFlags(following) & ts.ModifierFlags.Static;
|
|
35
|
+
|
|
36
|
+
return signatureStatic === followingStatic && methodName(signature, file) === methodName(following, file);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findGroupedSignatures(members: readonly ts.ClassElement[], file: ts.SourceFile): Set<ts.ClassElement> {
|
|
40
|
+
const signatures = new Set<ts.ClassElement>();
|
|
41
|
+
let following: OverloadMember | undefined;
|
|
42
|
+
|
|
43
|
+
for (const member of members.toReversed()) {
|
|
44
|
+
if (isOverloadMember(member)) {
|
|
45
|
+
if (!member.body && following && belongsToOverload(member, following, file)) {
|
|
46
|
+
signatures.add(member);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
following = member;
|
|
50
|
+
} else {
|
|
51
|
+
following = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return signatures;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function countBlankLines(previous: ts.ClassElement, current: ts.ClassElement, file: ts.SourceFile): number {
|
|
59
|
+
const trailingComments = ts.getTrailingCommentRanges(file.text, previous.end);
|
|
60
|
+
const previousEnd = trailingComments?.at(-1)?.end ?? previous.end;
|
|
61
|
+
const leadingComments = ts.getLeadingCommentRanges(file.text, previousEnd);
|
|
62
|
+
const currentStart = leadingComments?.[0]?.pos ?? current.getStart(file);
|
|
63
|
+
const gap = file.text.slice(previousEnd, currentStart);
|
|
64
|
+
|
|
65
|
+
return gap
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.slice(1, -1)
|
|
68
|
+
.filter((line) => /^[^\S\r\n]*$/.test(line)).length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function findMemberSpacingIssues(source: string, fileName: string): readonly MemberSpacingIssue[] {
|
|
72
|
+
const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
|
|
73
|
+
const issues: MemberSpacingIssue[] = [];
|
|
74
|
+
|
|
75
|
+
function visit(node: ts.Node): void {
|
|
76
|
+
if (ts.isClassLike(node)) {
|
|
77
|
+
const members = node.members.filter((member) => !ts.isSemicolonClassElement(member));
|
|
78
|
+
const signatures = findGroupedSignatures(members, file);
|
|
79
|
+
let previous: ts.ClassElement | undefined;
|
|
80
|
+
|
|
81
|
+
for (const member of members) {
|
|
82
|
+
if (previous) {
|
|
83
|
+
const inOverloadGroup = signatures.has(previous);
|
|
84
|
+
const requiredBlankLines = inOverloadGroup ? 0 : 1;
|
|
85
|
+
|
|
86
|
+
if (countBlankLines(previous, member, file) !== requiredBlankLines) {
|
|
87
|
+
const { line, character } = file.getLineAndCharacterOfPosition(member.getStart(file));
|
|
88
|
+
const message = inOverloadGroup
|
|
89
|
+
? 'Keep adjacent signatures and the implementation of an overload together.'
|
|
90
|
+
: 'Leave one blank line between class members.';
|
|
91
|
+
|
|
92
|
+
issues.push({ line: line + 1, column: character + 1, message });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
previous = member;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ts.forEachChild(node, visit);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
visit(file);
|
|
104
|
+
|
|
105
|
+
return issues.sort((left, right) => left.line - right.line || left.column - right.column);
|
|
106
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "harness-author-style",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"packageManager": "bun@1.4.2",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"check": "bun --no-env-file --no-install check.ts",
|
|
8
|
+
"test": "bun test"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"bun": ">=1.4.2"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@biomejs/biome": "2.5.15",
|
|
15
|
+
"typescript": "5.9.3",
|
|
16
|
+
"zod": "4.6.7"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
language js(typescript, jsx)
|
|
2
|
+
|
|
3
|
+
pattern property($modifiers) {
|
|
4
|
+
or {
|
|
5
|
+
JsPropertyClassMember(modifiers=$modifiers, name=$name),
|
|
6
|
+
TsPropertySignatureClassMember(modifiers=$modifiers, name=$name),
|
|
7
|
+
TsInitializedPropertySignatureClassMember(modifiers=$modifiers, name=$name)
|
|
8
|
+
} where { not $name <: JsPrivateClassMemberName() }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pattern method($modifiers) {
|
|
12
|
+
or {
|
|
13
|
+
JsMethodClassMember(modifiers=$modifiers, name=$name),
|
|
14
|
+
JsGetterClassMember(modifiers=$modifiers, name=$name),
|
|
15
|
+
JsSetterClassMember(modifiers=$modifiers, name=$name),
|
|
16
|
+
TsMethodSignatureClassMember(modifiers=$modifiers, name=$name),
|
|
17
|
+
TsGetterSignatureClassMember(modifiers=$modifiers, name=$name),
|
|
18
|
+
TsSetterSignatureClassMember(modifiers=$modifiers, name=$name)
|
|
19
|
+
} where { not $name <: JsPrivateClassMemberName() }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pattern internal() {
|
|
23
|
+
contains TsAccessibilityModifier() as $accessibility where { $accessibility <: r"(?:private|protected)" }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pattern private_static_property() {
|
|
27
|
+
property($modifiers) where { $modifiers <: contains JsStaticModifier(), $modifiers <: internal() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pattern public_static_property() {
|
|
31
|
+
property($modifiers) where { $modifiers <: contains JsStaticModifier(), not $modifiers <: internal() }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pattern private_static_method() {
|
|
35
|
+
method($modifiers) where { $modifiers <: contains JsStaticModifier(), $modifiers <: internal() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pattern public_static_method() {
|
|
39
|
+
method($modifiers) where { $modifiers <: contains JsStaticModifier(), not $modifiers <: internal() }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pattern constructor_member() {
|
|
43
|
+
or {
|
|
44
|
+
JsConstructorClassMember(),
|
|
45
|
+
TsConstructorSignatureClassMember()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pattern private_property() {
|
|
50
|
+
property($modifiers) where { not $modifiers <: contains JsStaticModifier(), $modifiers <: internal() }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pattern public_property() {
|
|
54
|
+
property($modifiers) where { not $modifiers <: contains JsStaticModifier(), not $modifiers <: internal() }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pattern private_method() {
|
|
58
|
+
method($modifiers) where { not $modifiers <: contains JsStaticModifier(), $modifiers <: internal() }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pattern public_method() {
|
|
62
|
+
method($modifiers) where { not $modifiers <: contains JsStaticModifier(), not $modifiers <: internal() }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
or {
|
|
66
|
+
property(),
|
|
67
|
+
method(),
|
|
68
|
+
constructor_member()
|
|
69
|
+
} as $second where {
|
|
70
|
+
$second <: after $first,
|
|
71
|
+
$first <: or {
|
|
72
|
+
property(),
|
|
73
|
+
method(),
|
|
74
|
+
constructor_member()
|
|
75
|
+
},
|
|
76
|
+
or {
|
|
77
|
+
and { $second <: private_static_property(), not $first <: private_static_property() },
|
|
78
|
+
and {
|
|
79
|
+
$second <: public_static_property(),
|
|
80
|
+
not $first <: or {
|
|
81
|
+
private_static_property(),
|
|
82
|
+
public_static_property()
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
and {
|
|
86
|
+
$second <: private_static_method(),
|
|
87
|
+
$first <: or {
|
|
88
|
+
public_static_method(),
|
|
89
|
+
constructor_member(),
|
|
90
|
+
private_property(),
|
|
91
|
+
public_property(),
|
|
92
|
+
private_method(),
|
|
93
|
+
public_method()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
and {
|
|
97
|
+
$second <: public_static_method(),
|
|
98
|
+
$first <: or {
|
|
99
|
+
constructor_member(),
|
|
100
|
+
private_property(),
|
|
101
|
+
public_property(),
|
|
102
|
+
private_method(),
|
|
103
|
+
public_method()
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
and {
|
|
107
|
+
$second <: constructor_member(),
|
|
108
|
+
$first <: or {
|
|
109
|
+
private_property(),
|
|
110
|
+
public_property(),
|
|
111
|
+
private_method(),
|
|
112
|
+
public_method()
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
and {
|
|
116
|
+
$second <: private_property(),
|
|
117
|
+
$first <: or {
|
|
118
|
+
public_property(),
|
|
119
|
+
private_method(),
|
|
120
|
+
public_method()
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
and {
|
|
124
|
+
$second <: public_property(),
|
|
125
|
+
$first <: or {
|
|
126
|
+
private_method(),
|
|
127
|
+
public_method()
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
and { $second <: private_method(), $first <: public_method() }
|
|
131
|
+
},
|
|
132
|
+
register_diagnostic(span=$second, message="Follow the class member order. Move this member before the preceding group.", severity="error")
|
|
133
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
language js(typescript, jsx)
|
|
2
|
+
|
|
3
|
+
pattern implementation() {
|
|
4
|
+
or {
|
|
5
|
+
JsArrowFunctionExpression(),
|
|
6
|
+
JsFunctionExpression(),
|
|
7
|
+
JsCallExpression(callee=$hook, arguments=JsCallArguments(args=[$callback, $dependencies])) where {
|
|
8
|
+
$hook <: or {
|
|
9
|
+
`useCallback`,
|
|
10
|
+
`React.useCallback`
|
|
11
|
+
},
|
|
12
|
+
$callback <: or {
|
|
13
|
+
JsArrowFunctionExpression(),
|
|
14
|
+
JsFunctionExpression()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pattern local_handler($name) {
|
|
21
|
+
or {
|
|
22
|
+
JsFunctionDeclaration(id=$name, body=JsFunctionBody()),
|
|
23
|
+
JsVariableDeclarator(id=$name, initializer=JsInitializerClause(expression=implementation()))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pattern call_target($name) { JsCallExpression(callee=JsIdentifierExpression() as $callee) where { $callee <: $name } }
|
|
28
|
+
|
|
29
|
+
pattern adapted_call($name) {
|
|
30
|
+
or {
|
|
31
|
+
call_target($name),
|
|
32
|
+
JsAwaitExpression(argument=call_target($name)),
|
|
33
|
+
JsUnaryExpression(argument=$call) as $unary where {
|
|
34
|
+
$unary <: r"(?s)void\b.*",
|
|
35
|
+
$call <: or {
|
|
36
|
+
call_target($name),
|
|
37
|
+
JsAwaitExpression(argument=call_target($name))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pattern callback_target($name) {
|
|
44
|
+
or {
|
|
45
|
+
JsIdentifierExpression() as $reference where { $reference <: $name },
|
|
46
|
+
JsArrowFunctionExpression(body=adapted_call($name)),
|
|
47
|
+
JsArrowFunctionExpression(body=JsParenthesizedExpression(expression=adapted_call($name)))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pattern jsx_reference($name) {
|
|
52
|
+
JsxAttribute(name=$attribute, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$callback))) where {
|
|
53
|
+
$attribute <: r"on[A-Z].*",
|
|
54
|
+
$callback <: or {
|
|
55
|
+
callback_target($name),
|
|
56
|
+
form_reference($name)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pattern form_reference($name) {
|
|
62
|
+
JsCallExpression(callee=$factory, arguments=JsCallArguments(args=$arguments)) where {
|
|
63
|
+
$factory <: or {
|
|
64
|
+
`handleSubmit`,
|
|
65
|
+
JsStaticMemberExpression(member=$method) where { $method <: r"handleSubmit" }
|
|
66
|
+
},
|
|
67
|
+
$arguments <: or {
|
|
68
|
+
[callback_target($name)],
|
|
69
|
+
[callback_target($name), $invalid],
|
|
70
|
+
[$valid, callback_target($name)]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pattern native_reference($name, $items) {
|
|
76
|
+
JsCallExpression(callee=$callee, arguments=JsCallArguments(args=$arguments)) where {
|
|
77
|
+
$callee <: r"(?s)(?:window|document)\s*\.\s*addEventListener",
|
|
78
|
+
$arguments <: or {
|
|
79
|
+
[$event, callback_target($name)],
|
|
80
|
+
[$event, callback_target($name), $options]
|
|
81
|
+
},
|
|
82
|
+
not $items <: contains JsIdentifierBinding() as $target where { $target <: r"(?:window|document)" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
local_handler($name) as $declaration where {
|
|
87
|
+
not $name <: r"handle[A-Z][A-Za-z0-9]*",
|
|
88
|
+
$declaration <: within JsModule(items=$items),
|
|
89
|
+
$items <: contains or {
|
|
90
|
+
jsx_reference($name),
|
|
91
|
+
native_reference($name, $items)
|
|
92
|
+
},
|
|
93
|
+
not $items <: contains JsIdentifierBinding() as $parameter where {
|
|
94
|
+
$parameter <: $name,
|
|
95
|
+
$parameter <: within JsParameters()
|
|
96
|
+
},
|
|
97
|
+
not $items <: contains JsArrowFunctionExpression(parameters=JsIdentifierBinding() as $parameter) where {
|
|
98
|
+
$parameter <: $name
|
|
99
|
+
},
|
|
100
|
+
not $items <: contains JsIdentifierBinding() as $imported where {
|
|
101
|
+
$imported <: $name,
|
|
102
|
+
$imported <: within JsImport()
|
|
103
|
+
},
|
|
104
|
+
not $items <: contains JsIdentifierBinding() as $caught where { $caught <: $name, $caught <: within JsCatchClause() },
|
|
105
|
+
not $items <: contains JsVariableDeclarator(id=$binding, initializer=$initializer) where {
|
|
106
|
+
not $initializer <: JsInitializerClause(expression=implementation()),
|
|
107
|
+
$binding <: contains JsIdentifierBinding() as $forwarded where { $forwarded <: $name }
|
|
108
|
+
},
|
|
109
|
+
register_diagnostic(span=$name, message="Name locally implemented event handlers with a descriptive handle prefix, such as handleDelete.", severity="error")
|
|
110
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
language js(typescript, jsx)
|
|
2
|
+
|
|
3
|
+
JsArrowFunctionExpression(body=$body) where {
|
|
4
|
+
not $body <: or {
|
|
5
|
+
JsFunctionBody(),
|
|
6
|
+
JsxTagExpression(),
|
|
7
|
+
JsParenthesizedExpression(expression=JsxTagExpression())
|
|
8
|
+
},
|
|
9
|
+
$body <: or {
|
|
10
|
+
r"(?s).*\n.*",
|
|
11
|
+
contains JsConditionalExpression(),
|
|
12
|
+
contains JsSequenceExpression()
|
|
13
|
+
},
|
|
14
|
+
register_diagnostic(span=$body, message="Use a block and an explicit return for a multiline or branching arrow body.", severity="error")
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
language js(typescript, jsx)
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
TsUnknownType() as $type where {
|
|
5
|
+
register_diagnostic(span=$type, message="Use an explicit domain type and validate external values at the boundary.", severity="error")
|
|
6
|
+
},
|
|
7
|
+
or { JsPreUpdateExpression(), JsPostUpdateExpression() } as $expression where {
|
|
8
|
+
register_diagnostic(span=$expression, message="Use an explicit update and preserve the original value if the expression consumes it.", severity="error")
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
language js(typescript, jsx)
|
|
2
|
+
|
|
3
|
+
pattern bare_call() {
|
|
4
|
+
or {
|
|
5
|
+
JsCallExpression(),
|
|
6
|
+
JsParenthesizedExpression(expression=JsCallExpression())
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
pattern void_call() {
|
|
11
|
+
JsUnaryExpression(argument=$argument) as $unary where {
|
|
12
|
+
$unary <: r"(?s)void\b.*",
|
|
13
|
+
$argument <: or {
|
|
14
|
+
bare_call(),
|
|
15
|
+
JsAwaitExpression(argument=bare_call())
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pattern handler_call() {
|
|
21
|
+
or {
|
|
22
|
+
bare_call(),
|
|
23
|
+
JsAwaitExpression(argument=bare_call()),
|
|
24
|
+
void_call(),
|
|
25
|
+
JsParenthesizedExpression(expression=JsAwaitExpression(argument=bare_call())),
|
|
26
|
+
JsParenthesizedExpression(expression=void_call())
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pattern inline_handler() {
|
|
31
|
+
handler_call() as $body where {
|
|
32
|
+
not $body <: contains JsFunctionBody(),
|
|
33
|
+
not $body <: contains JsFunctionExpression(),
|
|
34
|
+
not $body <: contains JsSequenceExpression(),
|
|
35
|
+
not $body <: contains JsAssignmentExpression(),
|
|
36
|
+
not $body <: contains JsPreUpdateExpression(),
|
|
37
|
+
not $body <: contains JsPostUpdateExpression(),
|
|
38
|
+
not $body <: contains JsArrowFunctionExpression(body=JsConditionalExpression()),
|
|
39
|
+
not $body <: contains JsArrowFunctionExpression(body=JsParenthesizedExpression(expression=JsConditionalExpression()))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pattern jsx_handler($callback) {
|
|
44
|
+
JsxAttribute(name=$name, initializer=JsxAttributeInitializerClause(value=JsxExpressionAttributeValue(expression=$callback))) where {
|
|
45
|
+
$name <: r"on[A-Z].*"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pattern compact_return($expression) {
|
|
50
|
+
JsFunctionBody(statements=[JsReturnStatement(argument=$expression)]) as $body where {
|
|
51
|
+
$expression <: inline_handler(),
|
|
52
|
+
not $body <: r"(?s).*(?:/\*|//).*"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
or {
|
|
57
|
+
JsArrowFunctionExpression(body=$body) as $arrow where {
|
|
58
|
+
not $body <: JsFunctionBody(),
|
|
59
|
+
not $body <: or {
|
|
60
|
+
JsxTagExpression(),
|
|
61
|
+
JsParenthesizedExpression(expression=JsxTagExpression())
|
|
62
|
+
},
|
|
63
|
+
not and { $body <: handler_call(), $arrow <: within jsx_handler($arrow) },
|
|
64
|
+
$body <: or {
|
|
65
|
+
r"(?s).*\n.*",
|
|
66
|
+
contains JsConditionalExpression(),
|
|
67
|
+
contains JsSequenceExpression()
|
|
68
|
+
},
|
|
69
|
+
register_diagnostic(span=$body, message="Use a block and an explicit return for a multiline or branching arrow body.", severity="error", fix_kind="safe"),
|
|
70
|
+
$body => `{ return $body; }`
|
|
71
|
+
},
|
|
72
|
+
jsx_handler(JsArrowFunctionExpression(body=$body)) where {
|
|
73
|
+
$body <: compact_return($expression),
|
|
74
|
+
register_diagnostic(span=$body, message="Keep an inline JSX handler as one call expression. Extract logic into a named component-level useCallback.", severity="error", fix_kind="safe"),
|
|
75
|
+
$body => `$expression`
|
|
76
|
+
},
|
|
77
|
+
jsx_handler(JsArrowFunctionExpression(body=$body)) where {
|
|
78
|
+
not $body <: inline_handler(),
|
|
79
|
+
not $body <: compact_return(),
|
|
80
|
+
register_diagnostic(span=$body, message="Keep an inline JSX handler as one call expression. Extract logic into a named component-level useCallback.", severity="error")
|
|
81
|
+
},
|
|
82
|
+
jsx_handler(JsFunctionExpression() as $callback) where {
|
|
83
|
+
register_diagnostic(span=$callback, message="Use a named component-level useCallback instead of an inline JSX function.", severity="error")
|
|
84
|
+
}
|
|
85
|
+
}
|