@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,104 @@
|
|
|
1
|
+
function findHttpCallDecoratorExpression(decorators) {
|
|
2
|
+
for (const decorator of decorators) {
|
|
3
|
+
const expression = decorator.expression
|
|
4
|
+
if (expression.type === 'Identifier' && expression.name === 'HttpCall') {
|
|
5
|
+
return expression
|
|
6
|
+
}
|
|
7
|
+
if (expression.type !== 'CallExpression') {
|
|
8
|
+
continue
|
|
9
|
+
}
|
|
10
|
+
if (expression.callee.type !== 'Identifier' || expression.callee.name !== 'HttpCall') {
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
return expression
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getRouteViolation(decoratorExpression) {
|
|
19
|
+
if (decoratorExpression.type === 'Identifier') {
|
|
20
|
+
return 'missingRoute'
|
|
21
|
+
}
|
|
22
|
+
if (decoratorExpression.arguments.length === 0) {
|
|
23
|
+
return 'missingRoute'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const firstArgument = decoratorExpression.arguments[0]
|
|
27
|
+
if (firstArgument.type !== 'Literal' || typeof firstArgument.value !== 'string') {
|
|
28
|
+
return 'routeNotLiteral'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (firstArgument.value.trim().length === 0) {
|
|
32
|
+
return 'emptyRoute'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getMethodViolation(decoratorExpression) {
|
|
39
|
+
if (decoratorExpression.arguments.length < 2) {
|
|
40
|
+
return 'missingMethod'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const secondArgument = decoratorExpression.arguments[1]
|
|
44
|
+
if (secondArgument.type !== 'Literal' || typeof secondArgument.value !== 'string') {
|
|
45
|
+
return 'methodNotLiteral'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (secondArgument.value.trim().length === 0) {
|
|
49
|
+
return 'emptyMethod'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
meta: {
|
|
57
|
+
type: 'problem',
|
|
58
|
+
docs: { description: 'Require HttpCall decorator to include route and method' },
|
|
59
|
+
schema: [],
|
|
60
|
+
messages: {
|
|
61
|
+
missingRoute: "Method '{{methodName}}' uses @HttpCall but is missing route argument",
|
|
62
|
+
missingMethod: "Method '{{methodName}}' uses @HttpCall but is missing method argument",
|
|
63
|
+
routeNotLiteral: "Method '{{methodName}}' uses @HttpCall but route must be a string literal",
|
|
64
|
+
methodNotLiteral: "Method '{{methodName}}' uses @HttpCall but method must be a string literal",
|
|
65
|
+
emptyRoute: "Method '{{methodName}}' uses @HttpCall but route must be non-empty",
|
|
66
|
+
emptyMethod: "Method '{{methodName}}' uses @HttpCall but method must be non-empty",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
create(context) {
|
|
70
|
+
return {
|
|
71
|
+
MethodDefinition(node) {
|
|
72
|
+
if (node.key.type !== 'Identifier' || !node.decorators) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const decoratorExpression = findHttpCallDecoratorExpression(node.decorators)
|
|
77
|
+
if (!decoratorExpression) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const violation = getRouteViolation(decoratorExpression)
|
|
82
|
+
if (!violation) {
|
|
83
|
+
const methodViolation = getMethodViolation(decoratorExpression)
|
|
84
|
+
if (!methodViolation) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
context.report({
|
|
89
|
+
node: node.key,
|
|
90
|
+
messageId: methodViolation,
|
|
91
|
+
data: { methodName: node.key.name },
|
|
92
|
+
})
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
context.report({
|
|
97
|
+
node: node.key,
|
|
98
|
+
messageId: violation,
|
|
99
|
+
data: { methodName: node.key.name },
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
|
|
7
|
+
return decorators.some((decorator) => {
|
|
8
|
+
const expression = decorator.expression
|
|
9
|
+
if (expression.type === 'Identifier') {
|
|
10
|
+
return expression.name === decoratorName
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
expression.type === 'CallExpression' &&
|
|
14
|
+
expression.callee.type === 'Identifier' &&
|
|
15
|
+
expression.callee.name === decoratorName
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPublicInstanceMethod(member) {
|
|
21
|
+
if (member.type !== 'MethodDefinition') {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
if (member.kind === 'constructor' || member.kind === 'get' || member.kind === 'set') {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
if (member.static) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
if (member.accessibility === 'private' || member.accessibility === 'protected') {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
return member.key.type === 'Identifier'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
meta: {
|
|
38
|
+
type: 'problem',
|
|
39
|
+
docs: {description: 'Require public methods in HttpClient classes to have HttpCall decorator',},
|
|
40
|
+
schema: [],
|
|
41
|
+
messages: {
|
|
42
|
+
missingHttpCall:
|
|
43
|
+
"Public method '{{methodName}}' in @HttpClient class '{{className}}' must have @HttpCall",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
return {
|
|
48
|
+
ClassDeclaration(node) {
|
|
49
|
+
if (hasDecoratorNamed(node.decorators, 'Ignore')) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!node.id || !hasDecoratorNamed(node.decorators, 'HttpClient')) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const member of node.body.body) {
|
|
58
|
+
if (hasDecoratorNamed(member.decorators, 'Ignore')) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (!isPublicInstanceMethod(member)) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (hasDecoratorNamed(member.decorators, 'HttpCall')) {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
context.report({
|
|
69
|
+
node: member.key,
|
|
70
|
+
messageId: 'missingHttpCall',
|
|
71
|
+
data: {
|
|
72
|
+
className: node.id.name,
|
|
73
|
+
methodName: member.key.name,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function findHttpClientDecoratorExpression(decorators) {
|
|
2
|
+
for (const decorator of decorators) {
|
|
3
|
+
const expression = decorator.expression
|
|
4
|
+
if (expression.type === 'Identifier' && expression.name === 'HttpClient') {
|
|
5
|
+
return expression
|
|
6
|
+
}
|
|
7
|
+
if (expression.type !== 'CallExpression') {
|
|
8
|
+
continue
|
|
9
|
+
}
|
|
10
|
+
if (expression.callee.type !== 'Identifier' || expression.callee.name !== 'HttpClient') {
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
return expression
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getServiceNameViolation(callExpression) {
|
|
19
|
+
if (callExpression.type === 'Identifier') {
|
|
20
|
+
return 'missingServiceName'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (callExpression.arguments.length === 0) {
|
|
24
|
+
return 'missingServiceName'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const firstArgument = callExpression.arguments[0]
|
|
28
|
+
if (firstArgument.type !== 'Literal' || typeof firstArgument.value !== 'string') {
|
|
29
|
+
return 'serviceNameNotLiteral'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (firstArgument.value.trim().length === 0) {
|
|
33
|
+
return 'emptyServiceName'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'problem',
|
|
42
|
+
docs: { description: 'Require HttpClient decorator to include service name' },
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
missingServiceName:
|
|
46
|
+
"Class '{{className}}' uses @HttpClient but is missing service name argument",
|
|
47
|
+
serviceNameNotLiteral:
|
|
48
|
+
"Class '{{className}}' uses @HttpClient but service name must be a string literal",
|
|
49
|
+
emptyServiceName: "Class '{{className}}' uses @HttpClient but service name must be non-empty",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
create(context) {
|
|
53
|
+
return {
|
|
54
|
+
ClassDeclaration(node) {
|
|
55
|
+
if (!node.id || !node.decorators) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const decoratorCall = findHttpClientDecoratorExpression(node.decorators)
|
|
60
|
+
if (!decoratorCall) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const violation = getServiceNameViolation(decoratorCall)
|
|
65
|
+
if (!violation) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
context.report({
|
|
70
|
+
node: node.id,
|
|
71
|
+
messageId: violation,
|
|
72
|
+
data: { className: node.id.name },
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const requireComponentDecorator = require('./require-component-decorator.cjs')
|
|
2
|
+
const apiControllerRequiresRouteAndMethod = require('./api-controller-requires-route-and-method.cjs')
|
|
3
|
+
const eventRequiresTypeProperty = require('./event-requires-type-property.cjs')
|
|
4
|
+
const eventHandlerRequiresSubscribedEvents = require('./event-handler-requires-subscribed-events.cjs')
|
|
5
|
+
const uiPageRequiresRoute = require('./ui-page-requires-route.cjs')
|
|
6
|
+
const eventPublisherMethodSignature = require('./event-publisher-method-signature.cjs')
|
|
7
|
+
const httpClientRequiresRemoteName = require('./http-client-requires-remote-name.cjs')
|
|
8
|
+
const httpCallRequiresRoute = require('./http-call-requires-route.cjs')
|
|
9
|
+
const httpCallRequiresHttpClientContainer = require('./http-call-requires-http-client-container.cjs')
|
|
10
|
+
const httpClientPublicMethodsRequireHttpCall = require('./http-client-public-methods-require-http-call.cjs')
|
|
11
|
+
const noFetchOutsideHttpClient = require('./no-fetch-outside-http-client.cjs')
|
|
12
|
+
|
|
13
|
+
const restrictedHttpClientImports = [
|
|
14
|
+
'axios',
|
|
15
|
+
'got',
|
|
16
|
+
'node-fetch',
|
|
17
|
+
'undici',
|
|
18
|
+
'superagent',
|
|
19
|
+
'node:http',
|
|
20
|
+
'node:https',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function toRestrictedImportPath(packageName) {
|
|
24
|
+
return {
|
|
25
|
+
name: packageName,
|
|
26
|
+
message: `Do not import '${packageName}' directly. Use @HttpClient/@HttpCall conventions for HTTP boundary code.`,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toRestrictedImportPattern(packageName) {
|
|
31
|
+
return {
|
|
32
|
+
group: [`${packageName}/*`],
|
|
33
|
+
message: `Do not import subpaths from '${packageName}'. Use @HttpClient/@HttpCall conventions for HTTP boundary code.`,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
rules: {
|
|
39
|
+
'require-component-decorator': requireComponentDecorator,
|
|
40
|
+
'api-controller-requires-route-and-method': apiControllerRequiresRouteAndMethod,
|
|
41
|
+
'event-requires-type-property': eventRequiresTypeProperty,
|
|
42
|
+
'event-handler-requires-subscribed-events': eventHandlerRequiresSubscribedEvents,
|
|
43
|
+
'ui-page-requires-route': uiPageRequiresRoute,
|
|
44
|
+
'event-publisher-method-signature': eventPublisherMethodSignature,
|
|
45
|
+
'http-client-requires-service-name': httpClientRequiresRemoteName,
|
|
46
|
+
'http-call-requires-route': httpCallRequiresRoute,
|
|
47
|
+
'http-call-requires-http-client-container': httpCallRequiresHttpClientContainer,
|
|
48
|
+
'http-client-public-methods-require-http-call': httpClientPublicMethodsRequireHttpCall,
|
|
49
|
+
'no-fetch-outside-http-client': noFetchOutsideHttpClient,
|
|
50
|
+
},
|
|
51
|
+
configs: {
|
|
52
|
+
'http-client-import-boundary': {
|
|
53
|
+
rules: {
|
|
54
|
+
'no-restricted-imports': [
|
|
55
|
+
'error',
|
|
56
|
+
{
|
|
57
|
+
paths: restrictedHttpClientImports.map(toRestrictedImportPath),
|
|
58
|
+
patterns: restrictedHttpClientImports.map(toRestrictedImportPattern),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils'
|
|
2
|
+
|
|
3
|
+
interface Plugin {
|
|
4
|
+
rules: {
|
|
5
|
+
'require-component-decorator': TSESLint.RuleModule<'missingDecorator'>
|
|
6
|
+
'api-controller-requires-route-and-method': TSESLint.RuleModule<
|
|
7
|
+
| 'missingRoute'
|
|
8
|
+
| 'missingMethod'
|
|
9
|
+
| 'routeNotLiteral'
|
|
10
|
+
| 'methodNotLiteral'
|
|
11
|
+
| 'invalidHttpMethod'
|
|
12
|
+
>
|
|
13
|
+
'event-requires-type-property': TSESLint.RuleModule<'missingType' | 'typeNotLiteral'>
|
|
14
|
+
'event-handler-requires-subscribed-events': TSESLint.RuleModule<
|
|
15
|
+
'missingSubscribedEvents' | 'subscribedEventsNotLiteralArray'
|
|
16
|
+
>
|
|
17
|
+
'ui-page-requires-route': TSESLint.RuleModule<'missingRoute' | 'routeNotLiteral'>
|
|
18
|
+
'event-publisher-method-signature': TSESLint.RuleModule<
|
|
19
|
+
| 'missingParameter'
|
|
20
|
+
| 'tooManyParameters'
|
|
21
|
+
| 'missingTypeAnnotation'
|
|
22
|
+
| 'notTypeReference'
|
|
23
|
+
| 'invalidEventType'
|
|
24
|
+
| 'nonPublicMethod'
|
|
25
|
+
>
|
|
26
|
+
'http-client-requires-service-name': TSESLint.RuleModule<
|
|
27
|
+
'missingServiceName' | 'serviceNameNotLiteral' | 'emptyServiceName'
|
|
28
|
+
>
|
|
29
|
+
'http-call-requires-route': TSESLint.RuleModule<
|
|
30
|
+
| 'missingRoute'
|
|
31
|
+
| 'missingMethod'
|
|
32
|
+
| 'routeNotLiteral'
|
|
33
|
+
| 'methodNotLiteral'
|
|
34
|
+
| 'emptyRoute'
|
|
35
|
+
| 'emptyMethod'
|
|
36
|
+
>
|
|
37
|
+
'http-call-requires-http-client-container': TSESLint.RuleModule<'missingHttpClientContainer'>
|
|
38
|
+
'http-client-public-methods-require-http-call': TSESLint.RuleModule<'missingHttpCall'>
|
|
39
|
+
'no-fetch-outside-http-client': TSESLint.RuleModule<'fetchOutsideHttpClient'>
|
|
40
|
+
}
|
|
41
|
+
configs: {
|
|
42
|
+
'http-client-import-boundary': {
|
|
43
|
+
rules: {
|
|
44
|
+
'no-restricted-imports': [
|
|
45
|
+
'error',
|
|
46
|
+
{
|
|
47
|
+
paths: Array<{ name: string; message: string }>
|
|
48
|
+
patterns: Array<{ group: string[]; message: string }>
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare const plugin: Plugin
|
|
57
|
+
export default plugin
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function hasDecorator(node, decoratorName) {
|
|
2
|
+
if (!node.decorators || node.decorators.length === 0) {
|
|
3
|
+
return false
|
|
4
|
+
}
|
|
5
|
+
return node.decorators.some(
|
|
6
|
+
(d) => d.expression.type === 'Identifier' && d.expression.name === decoratorName,
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findInstanceProperty(classNode, propertyName) {
|
|
11
|
+
/* v8 ignore next 3 -- ESLint guarantees ClassDeclaration.body exists per estree spec; structurally unreachable */
|
|
12
|
+
if (!classNode.body || !classNode.body.body) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
return classNode.body.body.find((member) => {
|
|
16
|
+
return (
|
|
17
|
+
member.type === 'PropertyDefinition' &&
|
|
18
|
+
member.static !== true &&
|
|
19
|
+
member.key &&
|
|
20
|
+
member.key.type === 'Identifier' &&
|
|
21
|
+
member.key.name === propertyName
|
|
22
|
+
)
|
|
23
|
+
}) || null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasStringLiteralValue(property) {
|
|
27
|
+
/* v8 ignore next 3 -- callers (api-controller, ui-page rules) always guard with findInstanceProperty before calling; null property is structurally unreachable in current call sites */
|
|
28
|
+
if (!property || !property.value) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
return property.value.type === 'Literal' && typeof property.value.value === 'string'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasStringLiteralArrayValue(property) {
|
|
35
|
+
/* v8 ignore next 3 -- callers always guard with findInstanceProperty before calling; null property is structurally unreachable in current call sites */
|
|
36
|
+
if (!property || !property.value) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
if (property.value.type !== 'ArrayExpression') {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
return property.value.elements.every(
|
|
43
|
+
(element) => element && element.type === 'Literal' && typeof element.value === 'string'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getLiteralValue(property) {
|
|
48
|
+
/* v8 ignore next 3 -- callers always guard with hasStringLiteralValue before calling; non-literal property is structurally unreachable in current call sites */
|
|
49
|
+
if (!property || !property.value || property.value.type !== 'Literal') {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
return property.value.value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getValueTypeDescription(property) {
|
|
56
|
+
/* v8 ignore next 3 -- callers always guard with findInstanceProperty before calling; null property is structurally unreachable in current call sites */
|
|
57
|
+
if (!property || !property.value) {
|
|
58
|
+
return 'undefined'
|
|
59
|
+
}
|
|
60
|
+
const valueType = property.value.type
|
|
61
|
+
if (valueType === 'Identifier') {
|
|
62
|
+
return `variable reference '${property.value.name}'`
|
|
63
|
+
}
|
|
64
|
+
if (valueType === 'MemberExpression') {
|
|
65
|
+
return 'member expression (possibly an enum)'
|
|
66
|
+
}
|
|
67
|
+
if (valueType === 'CallExpression') {
|
|
68
|
+
return 'function call'
|
|
69
|
+
}
|
|
70
|
+
if (valueType === 'TemplateLiteral') {
|
|
71
|
+
return 'template literal'
|
|
72
|
+
}
|
|
73
|
+
return valueType
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
hasDecorator,
|
|
78
|
+
findInstanceProperty,
|
|
79
|
+
hasStringLiteralValue,
|
|
80
|
+
hasStringLiteralArrayValue,
|
|
81
|
+
getLiteralValue,
|
|
82
|
+
getValueTypeDescription,
|
|
83
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 getEnclosingClassDeclaration(sourceCode, node) {
|
|
20
|
+
const ancestors = sourceCode.getAncestors(node)
|
|
21
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
22
|
+
const ancestor = ancestors[index]
|
|
23
|
+
if (ancestor.type === 'ClassDeclaration') {
|
|
24
|
+
return ancestor
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: { description: 'Disallow fetch usage outside HttpClient classes' },
|
|
34
|
+
schema: [],
|
|
35
|
+
messages: {
|
|
36
|
+
fetchOutsideHttpClient:
|
|
37
|
+
"Call to '{{calleeName}}' is only allowed inside classes decorated with @HttpClient",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
return {
|
|
42
|
+
CallExpression(node) {
|
|
43
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== 'fetch') {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const enclosingClass = getEnclosingClassDeclaration(context.sourceCode, node)
|
|
48
|
+
if (enclosingClass && hasDecoratorNamed(enclosingClass.decorators, 'HttpClient')) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
messageId: 'fetchOutsideHttpClient',
|
|
55
|
+
data: { calleeName: 'fetch' },
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const COMPONENT_DECORATORS = [
|
|
2
|
+
'DomainOpContainer',
|
|
3
|
+
'APIContainer',
|
|
4
|
+
'EventHandlerContainer',
|
|
5
|
+
'EventPublisherContainer',
|
|
6
|
+
'UseCase',
|
|
7
|
+
'Event',
|
|
8
|
+
'UI',
|
|
9
|
+
'Ignore',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const CALL_EXPRESSION_DECORATORS = ['Custom']
|
|
13
|
+
|
|
14
|
+
function getDecoratorName(expression) {
|
|
15
|
+
if (expression.type === 'Identifier') {
|
|
16
|
+
return expression.name
|
|
17
|
+
}
|
|
18
|
+
if (expression.type === 'CallExpression' && expression.callee.type === 'Identifier') {
|
|
19
|
+
return expression.callee.name
|
|
20
|
+
}
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasComponentDecorator(decorators) {
|
|
25
|
+
return decorators.some((decorator) => {
|
|
26
|
+
const name = getDecoratorName(decorator.expression)
|
|
27
|
+
if (name === null) return false
|
|
28
|
+
return COMPONENT_DECORATORS.includes(name) || CALL_EXPRESSION_DECORATORS.includes(name)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'problem',
|
|
35
|
+
docs: { description: 'Require classes to have a component decorator' },
|
|
36
|
+
schema: [],
|
|
37
|
+
messages: {
|
|
38
|
+
missingDecorator:
|
|
39
|
+
"Class '{{className}}' requires a component decorator. Add one of: @UseCase, @Event, @UI, @DomainOpContainer, @APIContainer, @EventHandlerContainer, @EventPublisherContainer, @Custom('type'), or @Ignore",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
return {
|
|
44
|
+
ClassDeclaration(node) {
|
|
45
|
+
if (!node.id) return
|
|
46
|
+
if (hasComponentDecorator(node.decorators)) return
|
|
47
|
+
|
|
48
|
+
context.report({
|
|
49
|
+
node: node.id,
|
|
50
|
+
messageId: 'missingDecorator',
|
|
51
|
+
data: { className: node.id.name },
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
type GenericRuleModule = import('@typescript-eslint/utils').TSESLint.RuleModule<
|
|
2
|
+
string,
|
|
3
|
+
readonly unknown[]
|
|
4
|
+
>
|
|
5
|
+
|
|
6
|
+
declare module './api-controller-requires-route-and-method.cjs' {
|
|
7
|
+
const rule: GenericRuleModule
|
|
8
|
+
export default rule
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module './event-handler-requires-subscribed-events.cjs' {
|
|
12
|
+
const rule: GenericRuleModule
|
|
13
|
+
export default rule
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module './event-publisher-method-signature.cjs' {
|
|
17
|
+
const rule: GenericRuleModule
|
|
18
|
+
export default rule
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module './event-requires-type-property.cjs' {
|
|
22
|
+
const rule: GenericRuleModule
|
|
23
|
+
export default rule
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module './http-call-requires-http-client-container.cjs' {
|
|
27
|
+
const rule: GenericRuleModule
|
|
28
|
+
export default rule
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module './http-call-requires-route.cjs' {
|
|
32
|
+
const rule: GenericRuleModule
|
|
33
|
+
export default rule
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module './http-client-public-methods-require-http-call.cjs' {
|
|
37
|
+
const rule: GenericRuleModule
|
|
38
|
+
export default rule
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare module './http-client-requires-remote-name.cjs' {
|
|
42
|
+
const rule: GenericRuleModule
|
|
43
|
+
export default rule
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare module './no-fetch-outside-http-client.cjs' {
|
|
47
|
+
const rule: GenericRuleModule
|
|
48
|
+
export default rule
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare module './require-component-decorator.cjs' {
|
|
52
|
+
const rule: GenericRuleModule
|
|
53
|
+
export default rule
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare module './ui-page-requires-route.cjs' {
|
|
57
|
+
const rule: GenericRuleModule
|
|
58
|
+
export default rule
|
|
59
|
+
}
|