@living-architecture/riviere-extract-conventions 0.3.24 → 0.3.26
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/interfaces.d.ts +59 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/interfaces.js +1 -0
- package/package.json +2 -2
- package/src/default-extraction.config.json +15 -0
- package/src/eslint/api-controller-requires-route-and-method.cjs +85 -0
- package/src/eslint/api-controller-requires-route-and-method.d.cts +6 -0
- package/src/eslint/event-handler-requires-subscribed-events.cjs +47 -0
- package/src/eslint/event-handler-requires-subscribed-events.d.cts +6 -0
- package/src/eslint/event-requires-type-property.cjs +47 -0
- package/src/eslint/event-requires-type-property.d.cts +6 -0
- package/src/eslint/index.cjs +13 -1
- package/src/eslint/index.d.ts +12 -0
- package/src/eslint/interface-ast-predicates.cjs +143 -0
- package/src/eslint/ui-page-requires-route.cjs +47 -0
- package/src/eslint/ui-page-requires-route.d.cts +6 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { DomainOpContainer, APIContainer, EventHandlerContainer, UseCase, Event, UI, DomainOp, APIEndpoint, EventHandler, Custom, Ignore, getCustomType, } from './decorators';
|
|
2
|
+
export type { HttpMethod, APIControllerDef, EventDef, EventHandlerDef, UIPageDef, DomainOpContainerDef, } from './interfaces';
|
|
2
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,YAAY,EACZ,qBAAqB,EAErB,OAAO,EACP,KAAK,EACL,EAAE,EAEF,QAAQ,EACR,WAAW,EACX,YAAY,EAEZ,MAAM,EACN,MAAM,EAEN,aAAa,GACd,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,YAAY,EACZ,qBAAqB,EAErB,OAAO,EACP,KAAK,EACL,EAAE,EAEF,QAAQ,EACR,WAAW,EACX,YAAY,EAEZ,MAAM,EACN,MAAM,EAEN,aAAa,GACd,MAAM,cAAc,CAAA;AAGrB,YAAY,EACV,UAAU,EACV,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,SAAS,EACT,oBAAoB,GACrB,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP methods supported for API controllers.
|
|
3
|
+
*/
|
|
4
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
5
|
+
/**
|
|
6
|
+
* Interface for API controller classes.
|
|
7
|
+
* Classes implementing this interface represent HTTP endpoint handlers.
|
|
8
|
+
*
|
|
9
|
+
* Required properties:
|
|
10
|
+
* - route: The URL path for this endpoint
|
|
11
|
+
* - method: The HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
12
|
+
* - handle: The request handler function
|
|
13
|
+
*/
|
|
14
|
+
export interface APIControllerDef {
|
|
15
|
+
readonly route: string;
|
|
16
|
+
readonly method: HttpMethod;
|
|
17
|
+
handle(req: Request, res: Response): void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Interface for domain event classes.
|
|
21
|
+
* Classes implementing this interface represent events that have occurred in the domain.
|
|
22
|
+
*
|
|
23
|
+
* Required properties:
|
|
24
|
+
* - type: A unique identifier for this event type (should be a string literal)
|
|
25
|
+
*/
|
|
26
|
+
export interface EventDef {
|
|
27
|
+
readonly type: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Interface for event handler classes.
|
|
31
|
+
* Classes implementing this interface subscribe to and process domain events.
|
|
32
|
+
*
|
|
33
|
+
* Required properties:
|
|
34
|
+
* - subscribedEvents: Array of event type names this handler processes
|
|
35
|
+
* - handle: The event processing function
|
|
36
|
+
*/
|
|
37
|
+
export interface EventHandlerDef {
|
|
38
|
+
readonly subscribedEvents: readonly string[];
|
|
39
|
+
handle(event: unknown): void | Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Interface for UI page classes.
|
|
43
|
+
* Classes implementing this interface represent routable UI pages.
|
|
44
|
+
*
|
|
45
|
+
* Required properties:
|
|
46
|
+
* - route: The URL path for this page
|
|
47
|
+
*/
|
|
48
|
+
export interface UIPageDef {
|
|
49
|
+
readonly route: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Marker interface for domain operation container classes.
|
|
53
|
+
* Classes implementing this interface contain domain operations as methods.
|
|
54
|
+
* No required properties - the class itself serves as the container.
|
|
55
|
+
*/
|
|
56
|
+
export interface DomainOpContainerDef {
|
|
57
|
+
readonly __brand?: 'DomainOpContainerDef';
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=interfaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEpE;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAC;AAEjD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAC;AAEnD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAA;CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@living-architecture/riviere-extract-conventions",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -32,6 +32,6 @@
|
|
|
32
32
|
"!**/*.tsbuildinfo"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@living-architecture/riviere-extract-config": "0.3.
|
|
35
|
+
"@living-architecture/riviere-extract-config": "0.3.5"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"from": "@living-architecture/riviere-extract-conventions"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
},
|
|
17
|
+
"extract": {
|
|
18
|
+
"apiType": { "literal": "REST" }
|
|
16
19
|
}
|
|
17
20
|
},
|
|
18
21
|
"useCase": {
|
|
@@ -33,6 +36,9 @@
|
|
|
33
36
|
"from": "@living-architecture/riviere-extract-conventions"
|
|
34
37
|
}
|
|
35
38
|
}
|
|
39
|
+
},
|
|
40
|
+
"extract": {
|
|
41
|
+
"operationName": { "fromMethodName": true }
|
|
36
42
|
}
|
|
37
43
|
},
|
|
38
44
|
"event": {
|
|
@@ -42,6 +48,9 @@
|
|
|
42
48
|
"name": "Event",
|
|
43
49
|
"from": "@living-architecture/riviere-extract-conventions"
|
|
44
50
|
}
|
|
51
|
+
},
|
|
52
|
+
"extract": {
|
|
53
|
+
"eventName": { "fromClassName": true }
|
|
45
54
|
}
|
|
46
55
|
},
|
|
47
56
|
"eventHandler": {
|
|
@@ -53,6 +62,9 @@
|
|
|
53
62
|
"from": "@living-architecture/riviere-extract-conventions"
|
|
54
63
|
}
|
|
55
64
|
}
|
|
65
|
+
},
|
|
66
|
+
"extract": {
|
|
67
|
+
"subscribedEvents": { "fromGenericArg": { "interface": "IEventHandler", "position": 0 } }
|
|
56
68
|
}
|
|
57
69
|
},
|
|
58
70
|
"ui": {
|
|
@@ -62,6 +74,9 @@
|
|
|
62
74
|
"name": "UI",
|
|
63
75
|
"from": "@living-architecture/riviere-extract-conventions"
|
|
64
76
|
}
|
|
77
|
+
},
|
|
78
|
+
"extract": {
|
|
79
|
+
"route": { "fromProperty": { "name": "route", "kind": "static" } }
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const {
|
|
2
|
+
implementsInterface,
|
|
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 APIControllerDef implementations to have route and method with literal values' },
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
missingRoute: "Class '{{className}}' implements APIControllerDef but is missing 'route' property",
|
|
18
|
+
missingMethod: "Class '{{className}}' implements APIControllerDef 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 (!implementsInterface(node, 'APIControllerDef')) 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
|
+
implementsInterface,
|
|
3
|
+
findInstanceProperty,
|
|
4
|
+
hasLiteralArrayValue,
|
|
5
|
+
getValueTypeDescription,
|
|
6
|
+
} = require('./interface-ast-predicates.cjs')
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: { description: 'Require EventHandlerDef implementations to have subscribedEvents array with literal values' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingSubscribedEvents: "Class '{{className}}' implements EventHandlerDef 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 -- ClassDeclaration always has id (name) */
|
|
22
|
+
if (!node.id) return
|
|
23
|
+
if (!implementsInterface(node, 'EventHandlerDef')) 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 (!hasLiteralArrayValue(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,47 @@
|
|
|
1
|
+
const {
|
|
2
|
+
implementsInterface,
|
|
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 EventDef implementations to have type property with literal value' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingType: "Class '{{className}}' implements EventDef 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 (!implementsInterface(node, 'EventDef')) 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
|
+
}
|
package/src/eslint/index.cjs
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
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')
|
|
2
6
|
|
|
3
|
-
module.exports = {
|
|
7
|
+
module.exports = {
|
|
8
|
+
rules: {
|
|
9
|
+
'require-component-decorator': requireComponentDecorator,
|
|
10
|
+
'api-controller-requires-route-and-method': apiControllerRequiresRouteAndMethod,
|
|
11
|
+
'event-requires-type-property': eventRequiresTypeProperty,
|
|
12
|
+
'event-handler-requires-subscribed-events': eventHandlerRequiresSubscribedEvents,
|
|
13
|
+
'ui-page-requires-route': uiPageRequiresRoute,
|
|
14
|
+
},
|
|
15
|
+
}
|
package/src/eslint/index.d.ts
CHANGED
|
@@ -3,6 +3,18 @@ import type { TSESLint } from '@typescript-eslint/utils'
|
|
|
3
3
|
interface Plugin {
|
|
4
4
|
rules: {
|
|
5
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'>
|
|
6
18
|
}
|
|
7
19
|
}
|
|
8
20
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for ESLint rules that check interface implementations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a class implements a specific interface by name.
|
|
7
|
+
* @param {object} node - ClassDeclaration AST node
|
|
8
|
+
* @param {string} interfaceName - Name of interface to check
|
|
9
|
+
* @returns {boolean} True if class implements the interface
|
|
10
|
+
*/
|
|
11
|
+
function implementsInterface(node, interfaceName) {
|
|
12
|
+
if (!node.implements || node.implements.length === 0) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
return node.implements.some((impl) => {
|
|
16
|
+
// Handle simple identifier: implements APIControllerDef
|
|
17
|
+
if (impl.expression && impl.expression.type === 'Identifier') {
|
|
18
|
+
return impl.expression.name === interfaceName
|
|
19
|
+
}
|
|
20
|
+
// Handle qualified name: implements SomeModule.APIControllerDef
|
|
21
|
+
/* v8 ignore next 4 -- defensive check: implements array always has expression */
|
|
22
|
+
if (impl.expression && impl.expression.type === 'TSQualifiedName') {
|
|
23
|
+
return impl.expression.right.name === interfaceName
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Finds an instance property (non-static) by name in class body.
|
|
31
|
+
* @param {object} classNode - ClassDeclaration AST node
|
|
32
|
+
* @param {string} propertyName - Name of property to find
|
|
33
|
+
* @returns {object|null} PropertyDefinition node or null
|
|
34
|
+
*/
|
|
35
|
+
function findInstanceProperty(classNode, propertyName) {
|
|
36
|
+
/* v8 ignore next 3 -- defensive check: ClassDeclaration always has body in practice */
|
|
37
|
+
if (!classNode.body || !classNode.body.body) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
return classNode.body.body.find((member) => {
|
|
41
|
+
return (
|
|
42
|
+
member.type === 'PropertyDefinition' &&
|
|
43
|
+
member.static !== true &&
|
|
44
|
+
member.key &&
|
|
45
|
+
member.key.type === 'Identifier' &&
|
|
46
|
+
member.key.name === propertyName
|
|
47
|
+
)
|
|
48
|
+
}) || null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Checks if a property has a literal value (string, number, boolean).
|
|
53
|
+
* @param {object} property - PropertyDefinition AST node
|
|
54
|
+
* @returns {boolean} True if value is a literal
|
|
55
|
+
*/
|
|
56
|
+
/* v8 ignore start -- exported utility for consumers, not used internally */
|
|
57
|
+
function hasLiteralValue(property) {
|
|
58
|
+
if (!property || !property.value) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
return property.value.type === 'Literal'
|
|
62
|
+
}
|
|
63
|
+
/* v8 ignore stop */
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks if a property has a string literal value specifically.
|
|
67
|
+
* @param {object} property - PropertyDefinition AST node
|
|
68
|
+
* @returns {boolean} True if value is a string literal
|
|
69
|
+
*/
|
|
70
|
+
function hasStringLiteralValue(property) {
|
|
71
|
+
/* v8 ignore next 3 -- defensive check: rules always check property existence first */
|
|
72
|
+
if (!property || !property.value) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
return property.value.type === 'Literal' && typeof property.value.value === 'string'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if a property has an array literal value with only literal elements.
|
|
80
|
+
* @param {object} property - PropertyDefinition AST node
|
|
81
|
+
* @returns {boolean} True if value is an array of literals
|
|
82
|
+
*/
|
|
83
|
+
function hasLiteralArrayValue(property) {
|
|
84
|
+
/* v8 ignore next 3 -- defensive check: rules always check property existence first */
|
|
85
|
+
if (!property || !property.value) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
if (property.value.type !== 'ArrayExpression') {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
return property.value.elements.every(
|
|
92
|
+
(element) => element && element.type === 'Literal'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gets the literal value from a property, or null if not a literal.
|
|
98
|
+
* @param {object} property - PropertyDefinition AST node
|
|
99
|
+
* @returns {*} The literal value or null
|
|
100
|
+
*/
|
|
101
|
+
function getLiteralValue(property) {
|
|
102
|
+
/* v8 ignore next 3 -- defensive check: rules always check property existence first */
|
|
103
|
+
if (!property || !property.value || property.value.type !== 'Literal') {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
return property.value.value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gets a human-readable type description for error messages.
|
|
111
|
+
* @param {object} property - PropertyDefinition AST node
|
|
112
|
+
* @returns {string} Type description
|
|
113
|
+
*/
|
|
114
|
+
function getValueTypeDescription(property) {
|
|
115
|
+
/* v8 ignore next 3 -- defensive check: only called when property exists */
|
|
116
|
+
if (!property || !property.value) {
|
|
117
|
+
return 'undefined'
|
|
118
|
+
}
|
|
119
|
+
const valueType = property.value.type
|
|
120
|
+
if (valueType === 'Identifier') {
|
|
121
|
+
return `variable reference '${property.value.name}'`
|
|
122
|
+
}
|
|
123
|
+
if (valueType === 'MemberExpression') {
|
|
124
|
+
return 'member expression (possibly an enum)'
|
|
125
|
+
}
|
|
126
|
+
if (valueType === 'CallExpression') {
|
|
127
|
+
return 'function call'
|
|
128
|
+
}
|
|
129
|
+
if (valueType === 'TemplateLiteral') {
|
|
130
|
+
return 'template literal'
|
|
131
|
+
}
|
|
132
|
+
return valueType
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
implementsInterface,
|
|
137
|
+
findInstanceProperty,
|
|
138
|
+
hasLiteralValue,
|
|
139
|
+
hasStringLiteralValue,
|
|
140
|
+
hasLiteralArrayValue,
|
|
141
|
+
getLiteralValue,
|
|
142
|
+
getValueTypeDescription,
|
|
143
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const {
|
|
2
|
+
implementsInterface,
|
|
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 UIPageDef implementations to have route property with literal value' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingRoute: "Class '{{className}}' implements UIPageDef but is missing 'route' property",
|
|
15
|
+
routeNotLiteral: "Class '{{className}}' has 'route' 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 (!implementsInterface(node, 'UIPageDef')) return
|
|
24
|
+
|
|
25
|
+
const className = node.id.name
|
|
26
|
+
const routeProperty = findInstanceProperty(node, 'route')
|
|
27
|
+
|
|
28
|
+
if (!routeProperty) {
|
|
29
|
+
context.report({
|
|
30
|
+
node: node.id,
|
|
31
|
+
messageId: 'missingRoute',
|
|
32
|
+
data: { className },
|
|
33
|
+
})
|
|
34
|
+
} else if (!hasStringLiteralValue(routeProperty)) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: routeProperty,
|
|
37
|
+
messageId: 'routeNotLiteral',
|
|
38
|
+
data: {
|
|
39
|
+
className,
|
|
40
|
+
actualType: getValueTypeDescription(routeProperty),
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|