@living-architecture/riviere-extract-conventions-published-language 0.7.0
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 +190 -0
- package/README.md +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/published-language/decorators.d.ts +83 -0
- package/dist/published-language/decorators.d.ts.map +1 -0
- package/dist/published-language/decorators.js +130 -0
- package/package.json +44 -0
- package/src/published-language/default-extraction.config.json +153 -0
- package/src/published-language/eslint-plugin/api-controller-requires-route-and-method.cjs +85 -0
- package/src/published-language/eslint-plugin/api-controller-requires-route-and-method.d.cts +6 -0
- package/src/published-language/eslint-plugin/event-handler-requires-subscribed-events.cjs +47 -0
- package/src/published-language/eslint-plugin/event-handler-requires-subscribed-events.d.cts +6 -0
- package/src/published-language/eslint-plugin/event-publisher-method-signature.cjs +179 -0
- package/src/published-language/eslint-plugin/event-publisher-method-signature.d.cts +6 -0
- package/src/published-language/eslint-plugin/event-requires-type-property.cjs +47 -0
- package/src/published-language/eslint-plugin/event-requires-type-property.d.cts +6 -0
- package/src/published-language/eslint-plugin/http-call-requires-http-client-container.cjs +68 -0
- package/src/published-language/eslint-plugin/http-call-requires-http-client-container.d.cts +5 -0
- package/src/published-language/eslint-plugin/http-call-requires-route.cjs +104 -0
- package/src/published-language/eslint-plugin/http-call-requires-route.d.cts +5 -0
- package/src/published-language/eslint-plugin/http-client-public-methods-require-http-call.cjs +80 -0
- package/src/published-language/eslint-plugin/http-client-public-methods-require-http-call.d.cts +5 -0
- package/src/published-language/eslint-plugin/http-client-requires-remote-name.cjs +77 -0
- package/src/published-language/eslint-plugin/http-client-requires-remote-name.d.cts +5 -0
- package/src/published-language/eslint-plugin/index.cjs +64 -0
- package/src/published-language/eslint-plugin/index.d.ts +57 -0
- package/src/published-language/eslint-plugin/interface-ast-predicates.cjs +83 -0
- package/src/published-language/eslint-plugin/no-fetch-outside-http-client.cjs +60 -0
- package/src/published-language/eslint-plugin/no-fetch-outside-http-client.d.cts +5 -0
- package/src/published-language/eslint-plugin/require-component-decorator.cjs +56 -0
- package/src/published-language/eslint-plugin/require-component-decorator.d.cts +4 -0
- package/src/published-language/eslint-plugin/rule-module-declarations.d.ts +59 -0
- package/src/published-language/eslint-plugin/ui-page-requires-route.cjs +47 -0
- package/src/published-language/eslint-plugin/ui-page-requires-route.d.cts +6 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://living-architecture.dev/schemas/extraction-config.schema.json",
|
|
3
|
+
"modules": [
|
|
4
|
+
{
|
|
5
|
+
"name": "default",
|
|
6
|
+
"domain": "default",
|
|
7
|
+
"path": ".",
|
|
8
|
+
"glob": "**/*.ts",
|
|
9
|
+
"api": {
|
|
10
|
+
"find": "methods",
|
|
11
|
+
"where": {
|
|
12
|
+
"inClassWith": {
|
|
13
|
+
"hasDecorator": {
|
|
14
|
+
"name": "APIContainer",
|
|
15
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"extract": {
|
|
20
|
+
"apiType": { "literal": "REST" },
|
|
21
|
+
"route": { "fromProperty": { "name": "route", "kind": "instance" } },
|
|
22
|
+
"method": { "fromProperty": { "name": "method", "kind": "instance" } }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"useCase": {
|
|
26
|
+
"find": "classes",
|
|
27
|
+
"where": {
|
|
28
|
+
"hasDecorator": {
|
|
29
|
+
"name": "UseCase",
|
|
30
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"domainOp": {
|
|
35
|
+
"find": "methods",
|
|
36
|
+
"where": {
|
|
37
|
+
"inClassWith": {
|
|
38
|
+
"hasDecorator": {
|
|
39
|
+
"name": "DomainOpContainer",
|
|
40
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"extract": {
|
|
45
|
+
"operationName": { "fromMethodName": true }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"event": {
|
|
49
|
+
"find": "classes",
|
|
50
|
+
"where": {
|
|
51
|
+
"hasDecorator": {
|
|
52
|
+
"name": "Event",
|
|
53
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"extract": {
|
|
57
|
+
"eventName": { "fromClassName": true }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"eventHandler": {
|
|
61
|
+
"find": "methods",
|
|
62
|
+
"where": {
|
|
63
|
+
"inClassWith": {
|
|
64
|
+
"hasDecorator": {
|
|
65
|
+
"name": "EventHandlerContainer",
|
|
66
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"extract": {
|
|
71
|
+
"subscribedEvents": { "fromProperty": { "name": "subscribedEvents", "kind": "instance" } }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"ui": {
|
|
75
|
+
"find": "classes",
|
|
76
|
+
"where": {
|
|
77
|
+
"hasDecorator": {
|
|
78
|
+
"name": "UI",
|
|
79
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"extract": {
|
|
83
|
+
"route": { "fromProperty": { "name": "route", "kind": "static" } }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"customTypes": {
|
|
87
|
+
"eventPublisher": {
|
|
88
|
+
"find": "methods",
|
|
89
|
+
"where": {
|
|
90
|
+
"inClassWith": {
|
|
91
|
+
"hasDecorator": {
|
|
92
|
+
"name": "EventPublisherContainer",
|
|
93
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"extract": {
|
|
98
|
+
"publishedEventType": { "fromParameterType": { "position": 0 } }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"httpCall": {
|
|
102
|
+
"find": "methods",
|
|
103
|
+
"where": {
|
|
104
|
+
"and": [
|
|
105
|
+
{
|
|
106
|
+
"hasDecorator": {
|
|
107
|
+
"name": "HttpCall",
|
|
108
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"inClassWith": {
|
|
113
|
+
"hasDecorator": {
|
|
114
|
+
"name": "HttpClient",
|
|
115
|
+
"from": "@living-architecture/riviere-extract-conventions-published-language"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
"extract": {
|
|
122
|
+
"serviceName": {
|
|
123
|
+
"fromClassDecoratorArg": {
|
|
124
|
+
"decorator": "HttpClient",
|
|
125
|
+
"position": 0
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"route": {
|
|
129
|
+
"fromDecoratorArg": {
|
|
130
|
+
"decorator": "HttpCall",
|
|
131
|
+
"position": 0
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"method": {
|
|
135
|
+
"fromDecoratorArg": {
|
|
136
|
+
"decorator": "HttpCall",
|
|
137
|
+
"position": 1
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"connections": {
|
|
146
|
+
"eventPublishers": [
|
|
147
|
+
{
|
|
148
|
+
"fromType": "eventPublisher",
|
|
149
|
+
"metadataKey": "publishedEventType"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const {
|
|
2
|
+
hasDecorator,
|
|
3
|
+
findInstanceProperty,
|
|
4
|
+
hasStringLiteralValue,
|
|
5
|
+
getLiteralValue,
|
|
6
|
+
getValueTypeDescription,
|
|
7
|
+
} = require('./interface-ast-predicates.cjs')
|
|
8
|
+
|
|
9
|
+
const VALID_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: { description: 'Require @APIContainer classes to have route and method with literal values' },
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
missingRoute: "Class '{{className}}' decorated with @APIContainer but is missing 'route' property",
|
|
18
|
+
missingMethod: "Class '{{className}}' decorated with @APIContainer but is missing 'method' property",
|
|
19
|
+
routeNotLiteral: "Class '{{className}}' has 'route' property but value must be a string literal, not {{actualType}}",
|
|
20
|
+
methodNotLiteral: "Class '{{className}}' has 'method' property but value must be a string literal, not {{actualType}}",
|
|
21
|
+
invalidHttpMethod: "Class '{{className}}' has invalid HTTP method '{{value}}'. Must be one of: GET, POST, PUT, PATCH, DELETE",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
ClassDeclaration(node) {
|
|
27
|
+
/* v8 ignore next -- ClassDeclaration always has id (name) */
|
|
28
|
+
if (!node.id) return
|
|
29
|
+
if (!hasDecorator(node, 'APIContainer')) return
|
|
30
|
+
|
|
31
|
+
const className = node.id.name
|
|
32
|
+
const routeProperty = findInstanceProperty(node, 'route')
|
|
33
|
+
const methodProperty = findInstanceProperty(node, 'method')
|
|
34
|
+
|
|
35
|
+
// Check route property
|
|
36
|
+
if (!routeProperty) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: node.id,
|
|
39
|
+
messageId: 'missingRoute',
|
|
40
|
+
data: { className },
|
|
41
|
+
})
|
|
42
|
+
} else if (!hasStringLiteralValue(routeProperty)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: routeProperty,
|
|
45
|
+
messageId: 'routeNotLiteral',
|
|
46
|
+
data: {
|
|
47
|
+
className,
|
|
48
|
+
actualType: getValueTypeDescription(routeProperty),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check method property
|
|
54
|
+
if (!methodProperty) {
|
|
55
|
+
context.report({
|
|
56
|
+
node: node.id,
|
|
57
|
+
messageId: 'missingMethod',
|
|
58
|
+
data: { className },
|
|
59
|
+
})
|
|
60
|
+
} else if (!hasStringLiteralValue(methodProperty)) {
|
|
61
|
+
context.report({
|
|
62
|
+
node: methodProperty,
|
|
63
|
+
messageId: 'methodNotLiteral',
|
|
64
|
+
data: {
|
|
65
|
+
className,
|
|
66
|
+
actualType: getValueTypeDescription(methodProperty),
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
} else {
|
|
70
|
+
const methodValue = getLiteralValue(methodProperty)
|
|
71
|
+
if (!VALID_HTTP_METHODS.includes(methodValue)) {
|
|
72
|
+
context.report({
|
|
73
|
+
node: methodProperty,
|
|
74
|
+
messageId: 'invalidHttpMethod',
|
|
75
|
+
data: {
|
|
76
|
+
className,
|
|
77
|
+
value: methodValue,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const {
|
|
2
|
+
hasDecorator,
|
|
3
|
+
findInstanceProperty,
|
|
4
|
+
hasStringLiteralArrayValue,
|
|
5
|
+
getValueTypeDescription,
|
|
6
|
+
} = require('./interface-ast-predicates.cjs')
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: { description: 'Require @EventHandlerContainer classes to have subscribedEvents array with literal values' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingSubscribedEvents: "Class '{{className}}' has @EventHandlerContainer but is missing 'subscribedEvents' property",
|
|
15
|
+
subscribedEventsNotLiteralArray: "Class '{{className}}' has 'subscribedEvents' property but value must be an array of string literals, not {{actualType}}",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
ClassDeclaration(node) {
|
|
21
|
+
/* v8 ignore next -- ESLint ClassDeclaration visitor guarantees node.id exists per estree spec; structurally unreachable */
|
|
22
|
+
if (!node.id) return
|
|
23
|
+
if (!hasDecorator(node, 'EventHandlerContainer')) return
|
|
24
|
+
|
|
25
|
+
const className = node.id.name
|
|
26
|
+
const subscribedEventsProperty = findInstanceProperty(node, 'subscribedEvents')
|
|
27
|
+
|
|
28
|
+
if (!subscribedEventsProperty) {
|
|
29
|
+
context.report({
|
|
30
|
+
node: node.id,
|
|
31
|
+
messageId: 'missingSubscribedEvents',
|
|
32
|
+
data: { className },
|
|
33
|
+
})
|
|
34
|
+
} else if (!hasStringLiteralArrayValue(subscribedEventsProperty)) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: subscribedEventsProperty,
|
|
37
|
+
messageId: 'subscribedEventsNotLiteralArray',
|
|
38
|
+
data: {
|
|
39
|
+
className,
|
|
40
|
+
actualType: getValueTypeDescription(subscribedEventsProperty),
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const { hasDecorator } = require('./interface-ast-predicates.cjs')
|
|
2
|
+
|
|
3
|
+
let getParserServices = null
|
|
4
|
+
try {
|
|
5
|
+
getParserServices = require('@typescript-eslint/utils').ESLintUtils.getParserServices
|
|
6
|
+
/* v8 ignore next -- soft-require: package always present in this repo */
|
|
7
|
+
} catch { /* type checking unavailable */ }
|
|
8
|
+
|
|
9
|
+
function getTypeReferenceName(typeAnnotation) {
|
|
10
|
+
const typeName = typeAnnotation.typeName
|
|
11
|
+
if (typeName.type === 'TSQualifiedName') {
|
|
12
|
+
return typeName.right.name
|
|
13
|
+
}
|
|
14
|
+
return typeName.name
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function checkEventTypeShape(checker, services, param, typeAnnotation) {
|
|
18
|
+
if (!checker || !services) return null
|
|
19
|
+
|
|
20
|
+
const tsParam = services.esTreeNodeToTSNodeMap.get(param)
|
|
21
|
+
const paramType = checker.getTypeAtLocation(tsParam)
|
|
22
|
+
const typeProperty = paramType.getProperty('type')
|
|
23
|
+
const referenceName = getTypeReferenceName(typeAnnotation)
|
|
24
|
+
|
|
25
|
+
if (!typeProperty) {
|
|
26
|
+
return referenceName
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const typePropertyType = checker.getTypeOfSymbol(typeProperty)
|
|
30
|
+
const stringType = checker.getStringType()
|
|
31
|
+
if (!checker.isTypeAssignableTo(typePropertyType, stringType)) {
|
|
32
|
+
return referenceName
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function checkMethod(context, member, className, checker, services) {
|
|
39
|
+
const methodName = member.key.name
|
|
40
|
+
|
|
41
|
+
if (member.accessibility === 'private' || member.accessibility === 'protected') {
|
|
42
|
+
context.report({
|
|
43
|
+
node: member.key,
|
|
44
|
+
messageId: 'nonPublicMethod',
|
|
45
|
+
data: {
|
|
46
|
+
methodName,
|
|
47
|
+
className,
|
|
48
|
+
accessibility: member.accessibility,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const params = member.value.params
|
|
55
|
+
|
|
56
|
+
if (params.length === 0) {
|
|
57
|
+
context.report({
|
|
58
|
+
node: member.key,
|
|
59
|
+
messageId: 'missingParameter',
|
|
60
|
+
data: {
|
|
61
|
+
methodName,
|
|
62
|
+
className,
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (params.length > 1) {
|
|
69
|
+
context.report({
|
|
70
|
+
node: member.key,
|
|
71
|
+
messageId: 'tooManyParameters',
|
|
72
|
+
data: {
|
|
73
|
+
methodName,
|
|
74
|
+
className,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const param = params[0]
|
|
81
|
+
const typeAnnotation = param.typeAnnotation && param.typeAnnotation.typeAnnotation
|
|
82
|
+
|
|
83
|
+
if (!typeAnnotation) {
|
|
84
|
+
context.report({
|
|
85
|
+
node: member.key,
|
|
86
|
+
messageId: 'missingTypeAnnotation',
|
|
87
|
+
data: {
|
|
88
|
+
methodName,
|
|
89
|
+
className,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeAnnotation.type !== 'TSTypeReference') {
|
|
96
|
+
context.report({
|
|
97
|
+
node: member.key,
|
|
98
|
+
messageId: 'notTypeReference',
|
|
99
|
+
data: {
|
|
100
|
+
methodName,
|
|
101
|
+
className,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const failedTypeName = checkEventTypeShape(checker, services, param, typeAnnotation)
|
|
108
|
+
if (failedTypeName) {
|
|
109
|
+
context.report({
|
|
110
|
+
node: member.key,
|
|
111
|
+
messageId: 'invalidEventType',
|
|
112
|
+
data: {
|
|
113
|
+
typeName: failedTypeName,
|
|
114
|
+
methodName,
|
|
115
|
+
className,
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isMethodToCheck(member) {
|
|
122
|
+
return (
|
|
123
|
+
member.type === 'MethodDefinition' &&
|
|
124
|
+
member.kind !== 'constructor' &&
|
|
125
|
+
member.kind !== 'get' &&
|
|
126
|
+
member.kind !== 'set'
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
meta: {
|
|
132
|
+
type: 'problem',
|
|
133
|
+
docs: {
|
|
134
|
+
description: 'Require @EventPublisherContainer methods to have exactly one typed event parameter',
|
|
135
|
+
requiresTypeChecking: true,
|
|
136
|
+
examples: {
|
|
137
|
+
valid: "@EventPublisherContainer class OrderPublisher { publish(event: OrderPlacedEvent): void {} }",
|
|
138
|
+
invalid: "@EventPublisherContainer class OrderPublisher { publish(): void {} }",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
schema: [],
|
|
142
|
+
messages: {
|
|
143
|
+
missingParameter: "Method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have exactly one parameter",
|
|
144
|
+
tooManyParameters: "Method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have exactly one parameter",
|
|
145
|
+
missingTypeAnnotation: "Parameter of method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have a type annotation",
|
|
146
|
+
notTypeReference: "Parameter of method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have a type reference annotation",
|
|
147
|
+
invalidEventType: "Parameter type '{{typeName}}' of method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have a 'type: string' property",
|
|
148
|
+
nonPublicMethod: "Method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must be public, but is {{accessibility}}",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
create(context) {
|
|
152
|
+
let services = null
|
|
153
|
+
let checker = null
|
|
154
|
+
/* v8 ignore start -- graceful degradation: getParserServices always available in this repo */
|
|
155
|
+
if (getParserServices) {
|
|
156
|
+
try {
|
|
157
|
+
services = getParserServices(context, true)
|
|
158
|
+
checker = services.program?.getTypeChecker() ?? null
|
|
159
|
+
} catch { /* no type info */ }
|
|
160
|
+
}
|
|
161
|
+
/* v8 ignore stop */
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
ClassDeclaration(node) {
|
|
165
|
+
/* v8 ignore next -- ClassDeclaration always has id (name) */
|
|
166
|
+
if (!node.id) return
|
|
167
|
+
if (!hasDecorator(node, 'EventPublisherContainer')) return
|
|
168
|
+
|
|
169
|
+
const className = node.id.name
|
|
170
|
+
|
|
171
|
+
for (const member of node.body.body) {
|
|
172
|
+
if (isMethodToCheck(member)) {
|
|
173
|
+
checkMethod(context, member, className, checker, services)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils'
|
|
2
|
+
|
|
3
|
+
type MessageIds = 'missingParameter' | 'tooManyParameters' | 'missingTypeAnnotation' | 'notTypeReference' | 'invalidEventType' | 'nonPublicMethod'
|
|
4
|
+
|
|
5
|
+
declare const rule: TSESLint.RuleModule<MessageIds>
|
|
6
|
+
export default rule
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const {
|
|
2
|
+
hasDecorator,
|
|
3
|
+
findInstanceProperty,
|
|
4
|
+
hasStringLiteralValue,
|
|
5
|
+
getValueTypeDescription,
|
|
6
|
+
} = require('./interface-ast-predicates.cjs')
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: { description: 'Require @Event classes to have type property with literal value' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingType: "Class '{{className}}' decorated with @Event but is missing 'type' property",
|
|
15
|
+
typeNotLiteral: "Class '{{className}}' has 'type' property but value must be a string literal, not {{actualType}}",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
ClassDeclaration(node) {
|
|
21
|
+
/* v8 ignore next -- ClassDeclaration always has id (name) */
|
|
22
|
+
if (!node.id) return
|
|
23
|
+
if (!hasDecorator(node, 'Event')) return
|
|
24
|
+
|
|
25
|
+
const className = node.id.name
|
|
26
|
+
const typeProperty = findInstanceProperty(node, 'type')
|
|
27
|
+
|
|
28
|
+
if (!typeProperty) {
|
|
29
|
+
context.report({
|
|
30
|
+
node: node.id,
|
|
31
|
+
messageId: 'missingType',
|
|
32
|
+
data: { className },
|
|
33
|
+
})
|
|
34
|
+
} else if (!hasStringLiteralValue(typeProperty)) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: typeProperty,
|
|
37
|
+
messageId: 'typeNotLiteral',
|
|
38
|
+
data: {
|
|
39
|
+
className,
|
|
40
|
+
actualType: getValueTypeDescription(typeProperty),
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function hasDecoratorNamed(decorators, decoratorName) {
|
|
2
|
+
/* v8 ignore next 3 -- typescript-eslint provides an array for decorators on class and method nodes; undefined is structurally unreachable in current parser output */
|
|
3
|
+
if (!decorators) {
|
|
4
|
+
return false
|
|
5
|
+
}
|
|
6
|
+
return decorators.some((decorator) => {
|
|
7
|
+
const expression = decorator.expression
|
|
8
|
+
if (expression.type === 'Identifier') {
|
|
9
|
+
return expression.name === decoratorName
|
|
10
|
+
}
|
|
11
|
+
return (
|
|
12
|
+
expression.type === 'CallExpression' &&
|
|
13
|
+
expression.callee.type === 'Identifier' &&
|
|
14
|
+
expression.callee.name === decoratorName
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isMethodWithHttpCallDecorator(member) {
|
|
20
|
+
if (member.type !== 'MethodDefinition') {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if (member.key.type !== 'Identifier') {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
return hasDecoratorNamed(member.decorators, 'HttpCall')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
docs: { description: 'Require HttpCall methods to be inside HttpClient classes' },
|
|
33
|
+
schema: [],
|
|
34
|
+
messages: {
|
|
35
|
+
missingHttpClientContainer:
|
|
36
|
+
"Method '{{methodName}}' uses @HttpCall but its class '{{className}}' is missing @HttpClient",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
return {
|
|
41
|
+
ClassDeclaration(node) {
|
|
42
|
+
if (!node.id) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hasHttpClientDecorator = hasDecoratorNamed(node.decorators, 'HttpClient')
|
|
47
|
+
if (hasHttpClientDecorator) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const member of node.body.body) {
|
|
52
|
+
if (!isMethodWithHttpCallDecorator(member)) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
context.report({
|
|
57
|
+
node: member.key,
|
|
58
|
+
messageId: 'missingHttpClientContainer',
|
|
59
|
+
data: {
|
|
60
|
+
className: node.id.name,
|
|
61
|
+
methodName: member.key.name,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|