@nordcraft/search 1.0.45 → 1.0.46
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/problems.worker.js +9 -3
- package/dist/problems.worker.js.map +1 -1
- package/dist/rules/logic/noStaticNodeCondition.js +29 -0
- package/dist/rules/logic/noStaticNodeCondition.js.map +1 -0
- package/dist/rules/logic/noStaticNodeCondition.test.js +274 -0
- package/dist/rules/logic/noStaticNodeCondition.test.js.map +1 -0
- package/dist/rules/logic/noUnnecessaryConditionFalsy.js +5 -1
- package/dist/rules/logic/noUnnecessaryConditionFalsy.js.map +1 -1
- package/dist/rules/logic/noUnnecessaryConditionTruthy.js +8 -4
- package/dist/rules/logic/noUnnecessaryConditionTruthy.js.map +1 -1
- package/dist/rules/logic/noUnnecessaryConditionTruthy.test.js +33 -4
- package/dist/rules/logic/noUnnecessaryConditionTruthy.test.js.map +1 -1
- package/dist/rules/noReferenceNodeRule.js +10 -8
- package/dist/rules/noReferenceNodeRule.js.map +1 -1
- package/dist/rules/noReferenceNodeRule.test.js +1 -1
- package/dist/rules/style/invalidStyleSyntaxRule.js +2 -2
- package/dist/rules/style/invalidStyleSyntaxRule.test.js +1 -1
- package/dist/util/contextlessEvaluateFormula.js +77 -0
- package/dist/util/contextlessEvaluateFormula.js.map +1 -0
- package/dist/util/contextlessEvaluateFormula.test.js +152 -0
- package/dist/util/contextlessEvaluateFormula.test.js.map +1 -0
- package/dist/util/removeUnused.fix.js +18 -1
- package/dist/util/removeUnused.fix.js.map +1 -1
- package/package.json +2 -2
- package/src/problems.worker.ts +16 -5
- package/src/rules/logic/noStaticNodeCondition.test.ts +290 -0
- package/src/rules/logic/noStaticNodeCondition.ts +43 -0
- package/src/rules/logic/noUnnecessaryConditionFalsy.ts +5 -4
- package/src/rules/logic/noUnnecessaryConditionTruthy.test.ts +37 -4
- package/src/rules/logic/noUnnecessaryConditionTruthy.ts +10 -8
- package/src/rules/noReferenceNodeRule.test.ts +1 -1
- package/src/rules/noReferenceNodeRule.ts +17 -10
- package/src/rules/style/invalidStyleSyntaxRule.test.ts +1 -1
- package/src/rules/style/invalidStyleSyntaxRule.ts +3 -3
- package/src/types.d.ts +3 -0
- package/src/util/contextlessEvaluateFormula.test.ts +190 -0
- package/src/util/contextlessEvaluateFormula.ts +110 -0
- package/src/util/removeUnused.fix.ts +29 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FixFunction, NodeType, Rule } from '../../types'
|
|
2
|
+
import { contextlessEvaluateFormula } from '../../util/contextlessEvaluateFormula'
|
|
3
|
+
import {
|
|
4
|
+
removeFromPathFix,
|
|
5
|
+
removeNodeFromPathFix,
|
|
6
|
+
} from '../../util/removeUnused.fix'
|
|
7
|
+
|
|
8
|
+
export const noStaticNodeCondition: Rule<{
|
|
9
|
+
result: ReturnType<typeof contextlessEvaluateFormula>['result']
|
|
10
|
+
}> = {
|
|
11
|
+
code: 'no-static-node-condition',
|
|
12
|
+
level: 'warning',
|
|
13
|
+
category: 'Quality',
|
|
14
|
+
visit: (report, { path: nodePath, value, nodeType }) => {
|
|
15
|
+
if (nodeType !== 'component-node' || !value.condition) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { isStatic, result } = contextlessEvaluateFormula(value.condition)
|
|
20
|
+
if (isStatic) {
|
|
21
|
+
const conditionPath = [...nodePath, 'condition']
|
|
22
|
+
// - if truthy: "Condition is always true, you can safely remove the condition as it will always be rendered."
|
|
23
|
+
// - if falsy: "Condition is always false, you can safely remove the entire node as it will never be rendered."
|
|
24
|
+
report(
|
|
25
|
+
conditionPath,
|
|
26
|
+
{
|
|
27
|
+
result,
|
|
28
|
+
},
|
|
29
|
+
Boolean(result) === true ? ['remove-condition'] : ['remove-node'],
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
fixes: {
|
|
34
|
+
'remove-condition': removeFromPathFix,
|
|
35
|
+
'remove-node': (({ path, ...data }) =>
|
|
36
|
+
removeNodeFromPathFix({
|
|
37
|
+
path: path.slice(0, -1),
|
|
38
|
+
...data,
|
|
39
|
+
})) as FixFunction<NodeType>,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type NoStaticNodeConditionRuleFix = 'remove-condition' | 'remove-node'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Rule } from '../../types'
|
|
2
|
+
import { contextlessEvaluateFormula } from '../../util/contextlessEvaluateFormula'
|
|
2
3
|
|
|
3
4
|
export const noUnnecessaryConditionFalsy: Rule = {
|
|
4
5
|
code: 'no-unnecessary-condition-falsy',
|
|
@@ -10,10 +11,10 @@ export const noUnnecessaryConditionFalsy: Rule = {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
if (
|
|
13
|
-
value.arguments.some(
|
|
14
|
-
(arg)
|
|
15
|
-
|
|
16
|
-
)
|
|
14
|
+
value.arguments.some((arg) => {
|
|
15
|
+
const { result, isStatic } = contextlessEvaluateFormula(arg.formula)
|
|
16
|
+
return isStatic && Boolean(result) === false
|
|
17
|
+
})
|
|
17
18
|
) {
|
|
18
19
|
report(path)
|
|
19
20
|
}
|
|
@@ -32,7 +32,41 @@ describe('noUnnecessaryConditionTruthy', () => {
|
|
|
32
32
|
},
|
|
33
33
|
],
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
},
|
|
36
|
+
classes: {},
|
|
37
|
+
events: {},
|
|
38
|
+
tag: 'div',
|
|
39
|
+
children: [],
|
|
40
|
+
style: {},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
formulas: {},
|
|
44
|
+
apis: {},
|
|
45
|
+
attributes: {},
|
|
46
|
+
variables: {},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
rules: [noUnnecessaryConditionTruthy],
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(problems).toHaveLength(1)
|
|
55
|
+
expect(problems[0].code).toBe('no-unnecessary-condition-truthy')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('should report unnecessary truthy conditions when a value is of type object or array as they are always consider truthy', () => {
|
|
59
|
+
const problems = Array.from(
|
|
60
|
+
searchProject({
|
|
61
|
+
files: {
|
|
62
|
+
components: {
|
|
63
|
+
test: {
|
|
64
|
+
name: 'test',
|
|
65
|
+
nodes: {
|
|
66
|
+
root: {
|
|
67
|
+
type: 'element',
|
|
68
|
+
attrs: {
|
|
69
|
+
test1: {
|
|
36
70
|
type: 'or',
|
|
37
71
|
arguments: [
|
|
38
72
|
{
|
|
@@ -43,7 +77,7 @@ describe('noUnnecessaryConditionTruthy', () => {
|
|
|
43
77
|
},
|
|
44
78
|
],
|
|
45
79
|
},
|
|
46
|
-
|
|
80
|
+
test2: {
|
|
47
81
|
type: 'or',
|
|
48
82
|
arguments: [
|
|
49
83
|
{
|
|
@@ -73,10 +107,9 @@ describe('noUnnecessaryConditionTruthy', () => {
|
|
|
73
107
|
}),
|
|
74
108
|
)
|
|
75
109
|
|
|
76
|
-
expect(problems).toHaveLength(
|
|
110
|
+
expect(problems).toHaveLength(2)
|
|
77
111
|
expect(problems[0].code).toBe('no-unnecessary-condition-truthy')
|
|
78
112
|
expect(problems[1].code).toBe('no-unnecessary-condition-truthy')
|
|
79
|
-
expect(problems[2].code).toBe('no-unnecessary-condition-truthy')
|
|
80
113
|
})
|
|
81
114
|
|
|
82
115
|
test('should not report necessary truthy conditions', () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Rule } from '../../types'
|
|
2
|
+
import { contextlessEvaluateFormula } from '../../util/contextlessEvaluateFormula'
|
|
2
3
|
|
|
3
4
|
export const noUnnecessaryConditionTruthy: Rule = {
|
|
4
5
|
code: 'no-unnecessary-condition-truthy',
|
|
@@ -10,14 +11,15 @@ export const noUnnecessaryConditionTruthy: Rule = {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
if (
|
|
13
|
-
value.arguments.some(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
value.arguments.some((arg) => {
|
|
15
|
+
// Objects and arrays, even empty ones, are always truthy
|
|
16
|
+
if (arg.formula.type === 'object' || arg.formula.type === 'array') {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { result, isStatic } = contextlessEvaluateFormula(arg.formula)
|
|
21
|
+
return isStatic && Boolean(result) === true
|
|
22
|
+
})
|
|
21
23
|
) {
|
|
22
24
|
report(path)
|
|
23
25
|
}
|
|
@@ -130,7 +130,7 @@ describe('fix noReferenceNodeRule', () => {
|
|
|
130
130
|
const fixedFiles = fixProject({
|
|
131
131
|
files,
|
|
132
132
|
rule: noReferenceNodeRule,
|
|
133
|
-
fixType: 'delete
|
|
133
|
+
fixType: 'delete-orphan-node',
|
|
134
134
|
})
|
|
135
135
|
expect(Object.keys(fixedFiles.components.test!.nodes)).toEqual([
|
|
136
136
|
'root',
|
|
@@ -6,22 +6,29 @@ export const noReferenceNodeRule: Rule<{ node: string }> = {
|
|
|
6
6
|
level: 'warning',
|
|
7
7
|
category: 'No References',
|
|
8
8
|
visit: (report, args) => {
|
|
9
|
-
if (args.nodeType !== 'component') {
|
|
9
|
+
if (args.nodeType !== 'component-node') {
|
|
10
10
|
return
|
|
11
11
|
}
|
|
12
|
-
const { path,
|
|
13
|
-
const
|
|
14
|
-
|
|
12
|
+
const { path, component } = args
|
|
13
|
+
const nodeId = path.at(-1)
|
|
14
|
+
if (typeof nodeId !== 'string') {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const referencedNodesInComponent = args.memo(
|
|
18
|
+
`node-references-${component.name}`,
|
|
19
|
+
() =>
|
|
20
|
+
new Set(
|
|
21
|
+
Object.values(component.nodes).flatMap((node) => node.children ?? []),
|
|
22
|
+
),
|
|
15
23
|
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
24
|
+
|
|
25
|
+
if (nodeId !== 'root' && !referencedNodesInComponent.has(nodeId)) {
|
|
26
|
+
report(path, { node: nodeId }, ['delete-orphan-node'])
|
|
20
27
|
}
|
|
21
28
|
},
|
|
22
29
|
fixes: {
|
|
23
|
-
'delete
|
|
30
|
+
'delete-orphan-node': removeFromPathFix,
|
|
24
31
|
},
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
export type NoReferenceNodeRuleFix = 'delete
|
|
34
|
+
export type NoReferenceNodeRuleFix = 'delete-orphan-node'
|
|
@@ -92,7 +92,7 @@ describe('fix invalidStyleSyntaxRule', () => {
|
|
|
92
92
|
const fixedFiles = fixProject({
|
|
93
93
|
files,
|
|
94
94
|
rule: invalidStyleSyntaxRule,
|
|
95
|
-
fixType: 'delete
|
|
95
|
+
fixType: 'delete-style-property',
|
|
96
96
|
})
|
|
97
97
|
expect((fixedFiles.components.test!.nodes.root as ElementNodeModel).style)
|
|
98
98
|
.toMatchInlineSnapshot(`
|
|
@@ -24,12 +24,12 @@ export const invalidStyleSyntaxRule: Rule<{
|
|
|
24
24
|
},
|
|
25
25
|
)
|
|
26
26
|
if (!valid) {
|
|
27
|
-
report(path, { property: value.styleProperty }, ['delete
|
|
27
|
+
report(path, { property: value.styleProperty }, ['delete-style-property'])
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
fixes: {
|
|
31
|
-
'delete
|
|
31
|
+
'delete-style-property': removeFromPathFix,
|
|
32
32
|
},
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export type InvalidStyleSyntaxRuleFix = 'delete
|
|
35
|
+
export type InvalidStyleSyntaxRuleFix = 'delete-style-property'
|
package/src/types.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { NoReferenceEventRuleFix } from './rules/events/noReferenceEventRul
|
|
|
30
30
|
import type { LegacyFormulaRuleFix } from './rules/formulas/legacyFormulaRule'
|
|
31
31
|
import type { NoReferenceComponentFormulaRuleFix } from './rules/formulas/noReferenceComponentFormulaRule'
|
|
32
32
|
import type { NoReferenceProjectFormulaRuleFix } from './rules/formulas/noReferenceProjectFormulaRule'
|
|
33
|
+
import type { NoStaticNodeConditionRuleFix } from './rules/logic/noStaticNodeConditionRule'
|
|
33
34
|
import type { NoReferenceNodeRuleFix } from './rules/noReferenceNodeRule'
|
|
34
35
|
import type { InvalidStyleSyntaxRuleFix } from './rules/style/invalidStyleSyntaxRule'
|
|
35
36
|
|
|
@@ -59,6 +60,7 @@ type Code =
|
|
|
59
60
|
| 'no-reference project action'
|
|
60
61
|
| 'no-reference project formula'
|
|
61
62
|
| 'no-reference variable'
|
|
63
|
+
| 'no-static-node-condition'
|
|
62
64
|
| 'no-unnecessary-condition-falsy'
|
|
63
65
|
| 'no-unnecessary-condition-truthy'
|
|
64
66
|
| 'non-empty void element'
|
|
@@ -343,6 +345,7 @@ type FixType =
|
|
|
343
345
|
| NoReferenceComponentFormulaRuleFix
|
|
344
346
|
| NoReferenceNodeRuleFix
|
|
345
347
|
| InvalidStyleSyntaxRuleFix
|
|
348
|
+
| NoStaticNodeConditionRuleFix
|
|
346
349
|
|
|
347
350
|
export interface Rule<T = unknown, V = NodeType> {
|
|
348
351
|
category: Category
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { contextlessEvaluateFormula } from './contextlessEvaluateFormula'
|
|
3
|
+
|
|
4
|
+
describe('contextlessEvaluateFormula', () => {
|
|
5
|
+
test('should return `true` when the formula is a simple value formula', () => {
|
|
6
|
+
expect(
|
|
7
|
+
contextlessEvaluateFormula({
|
|
8
|
+
type: 'value',
|
|
9
|
+
value: true,
|
|
10
|
+
}),
|
|
11
|
+
).toEqual({
|
|
12
|
+
isStatic: true,
|
|
13
|
+
result: true,
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('should not return a result and have `isStatic: false` when the formula uses a non-pure formula ("randomNumber", "Now")', () => {
|
|
18
|
+
expect(
|
|
19
|
+
contextlessEvaluateFormula({
|
|
20
|
+
type: 'apply',
|
|
21
|
+
name: 'randomNumber',
|
|
22
|
+
arguments: [],
|
|
23
|
+
}),
|
|
24
|
+
).toEqual({
|
|
25
|
+
isStatic: false,
|
|
26
|
+
result: undefined,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(
|
|
30
|
+
contextlessEvaluateFormula({
|
|
31
|
+
type: 'apply',
|
|
32
|
+
name: 'now',
|
|
33
|
+
arguments: [],
|
|
34
|
+
}),
|
|
35
|
+
).toEqual({
|
|
36
|
+
isStatic: false,
|
|
37
|
+
result: undefined,
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should not return a result and have `isStatic: false` when the formula depends on a variable', () => {
|
|
42
|
+
expect(
|
|
43
|
+
contextlessEvaluateFormula({
|
|
44
|
+
type: 'path',
|
|
45
|
+
path: ['Variables', 'myVariable'],
|
|
46
|
+
}),
|
|
47
|
+
).toEqual({
|
|
48
|
+
isStatic: false,
|
|
49
|
+
result: undefined,
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('should return a result and static for an array formula where all args are array or value types', () => {
|
|
54
|
+
expect(
|
|
55
|
+
contextlessEvaluateFormula({
|
|
56
|
+
type: 'array',
|
|
57
|
+
arguments: [
|
|
58
|
+
{ formula: { type: 'value', value: 1 } },
|
|
59
|
+
{
|
|
60
|
+
formula: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
arguments: [{ formula: { type: 'value', value: 2 } }],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}),
|
|
67
|
+
).toEqual({
|
|
68
|
+
isStatic: true,
|
|
69
|
+
result: [1, [2]],
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('should return `isStatic: false` for an array formula with a non-static argument', () => {
|
|
74
|
+
expect(
|
|
75
|
+
contextlessEvaluateFormula({
|
|
76
|
+
type: 'array',
|
|
77
|
+
arguments: [
|
|
78
|
+
{ formula: { type: 'value', value: 1 } },
|
|
79
|
+
{ formula: { type: 'apply', name: 'randomNumber', arguments: [] } },
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
).toEqual({
|
|
83
|
+
isStatic: false,
|
|
84
|
+
result: [
|
|
85
|
+
1,
|
|
86
|
+
undefined, // The second argument was not static
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('should be static true for `And` formulas with no arguments', () => {
|
|
92
|
+
expect(
|
|
93
|
+
contextlessEvaluateFormula({
|
|
94
|
+
type: 'and',
|
|
95
|
+
arguments: [],
|
|
96
|
+
}),
|
|
97
|
+
).toEqual({
|
|
98
|
+
isStatic: true,
|
|
99
|
+
result: true,
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('should be static false for `Or` formulas with no arguments', () => {
|
|
104
|
+
expect(
|
|
105
|
+
contextlessEvaluateFormula({
|
|
106
|
+
type: 'or',
|
|
107
|
+
arguments: [],
|
|
108
|
+
}),
|
|
109
|
+
).toEqual({
|
|
110
|
+
isStatic: true,
|
|
111
|
+
result: false,
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('should be static true for `And` formulas with all static truthy arguments', () => {
|
|
116
|
+
expect(
|
|
117
|
+
contextlessEvaluateFormula({
|
|
118
|
+
type: 'and',
|
|
119
|
+
arguments: [
|
|
120
|
+
{ formula: { type: 'value', value: true } },
|
|
121
|
+
{ formula: { type: 'value', value: true } },
|
|
122
|
+
],
|
|
123
|
+
}),
|
|
124
|
+
).toEqual({
|
|
125
|
+
isStatic: true,
|
|
126
|
+
result: true,
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('should not be static for `And` where any dynamic value exist', () => {
|
|
131
|
+
expect(
|
|
132
|
+
contextlessEvaluateFormula({
|
|
133
|
+
type: 'and',
|
|
134
|
+
arguments: [
|
|
135
|
+
{ formula: { type: 'value', value: true } },
|
|
136
|
+
{ formula: { type: 'apply', name: 'randomNumber', arguments: [] } },
|
|
137
|
+
],
|
|
138
|
+
}),
|
|
139
|
+
).toEqual({
|
|
140
|
+
isStatic: false,
|
|
141
|
+
result: undefined,
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('should be static false for `And` when any argument is static false, even when dynamic arguments exist', () => {
|
|
146
|
+
expect(
|
|
147
|
+
contextlessEvaluateFormula({
|
|
148
|
+
type: 'and',
|
|
149
|
+
arguments: [
|
|
150
|
+
{ formula: { type: 'value', value: true } },
|
|
151
|
+
{ formula: { type: 'path', path: ['Variables', 'myVariable'] } },
|
|
152
|
+
{ formula: { type: 'value', value: false } },
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
).toEqual({
|
|
156
|
+
isStatic: true,
|
|
157
|
+
result: false,
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('should not be static for `Or` when any argument is dynamic and all static are falsy', () => {
|
|
162
|
+
expect(
|
|
163
|
+
contextlessEvaluateFormula({
|
|
164
|
+
type: 'or',
|
|
165
|
+
arguments: [
|
|
166
|
+
{ formula: { type: 'value', value: false } },
|
|
167
|
+
{ formula: { type: 'apply', name: 'randomNumber', arguments: [] } },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
).toEqual({
|
|
171
|
+
isStatic: false,
|
|
172
|
+
result: undefined,
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('should be static true for `Or` when any argument is static true, even when dynamic arguments exist', () => {
|
|
177
|
+
expect(
|
|
178
|
+
contextlessEvaluateFormula({
|
|
179
|
+
type: 'or',
|
|
180
|
+
arguments: [
|
|
181
|
+
{ formula: { type: 'value', value: true } },
|
|
182
|
+
{ formula: { type: 'apply', name: 'randomNumber', arguments: [] } },
|
|
183
|
+
],
|
|
184
|
+
}),
|
|
185
|
+
).toEqual({
|
|
186
|
+
isStatic: true,
|
|
187
|
+
result: true,
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type Formula } from '@nordcraft/core/dist/formula/formula'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static evaluation of a formula.
|
|
5
|
+
*
|
|
6
|
+
* Can be used by issues to determine if a formula or sub-formula can be reduced to a static value.
|
|
7
|
+
* When sophisticated enough, it can be used during compile-time to reduce all static subgraphs of a formula to static values, greatly reducing payload and improving runtime performance.
|
|
8
|
+
*
|
|
9
|
+
* @returns {
|
|
10
|
+
* isStatic: boolean; // Whether the formula is static (i.e., does not depend on any variables, context AND only use pure formulas (no Random, Date, etc.))
|
|
11
|
+
* result: unknown; // The evaluated value of the formula
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* TODO: Make this function more capable of evaluating pure core formulas.
|
|
15
|
+
* TODO: Memoize the results (using path or a fast hash) to avoid re-evaluating any similar sub-graphs multiple times.
|
|
16
|
+
* TODO: Add a complex test-suite to ensure it works and develops as expected.
|
|
17
|
+
*/
|
|
18
|
+
export const contextlessEvaluateFormula = (
|
|
19
|
+
formula: Formula,
|
|
20
|
+
): {
|
|
21
|
+
isStatic: boolean
|
|
22
|
+
result: unknown
|
|
23
|
+
} => {
|
|
24
|
+
// Very basic implementation, just to get started.
|
|
25
|
+
switch (formula.type) {
|
|
26
|
+
case 'value': {
|
|
27
|
+
return {
|
|
28
|
+
isStatic: true,
|
|
29
|
+
result: formula.value,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case 'array': {
|
|
34
|
+
const results = formula.arguments.map((arg) =>
|
|
35
|
+
contextlessEvaluateFormula(arg.formula),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isStatic: results.every((res) => res.isStatic),
|
|
40
|
+
result: results.map((res) => res.result),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case 'record': {
|
|
45
|
+
const entries = Object.entries(formula.entries).map(
|
|
46
|
+
([key, arg]) => [key, contextlessEvaluateFormula(arg.formula)] as const,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const results = entries.map(([, res]) => res)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isStatic: results.every((res) => res.isStatic),
|
|
53
|
+
result: Object.fromEntries(
|
|
54
|
+
entries.map(([key, res]) => [key, res.result]),
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Static if:
|
|
60
|
+
// - ALL conditions are static AND truthy
|
|
61
|
+
// - ANY condition is static and falsy
|
|
62
|
+
// - EMPTY argument list is always true
|
|
63
|
+
case 'and': {
|
|
64
|
+
const results = formula.arguments.map((arg) =>
|
|
65
|
+
contextlessEvaluateFormula(arg.formula),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const alwaysTrue =
|
|
69
|
+
results.length === 0 ||
|
|
70
|
+
results.every((res) => res.isStatic && Boolean(res.result) === true)
|
|
71
|
+
const alwaysFalsy = results.some(
|
|
72
|
+
(res) => res.isStatic && Boolean(res.result) === false,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
isStatic: alwaysTrue || alwaysFalsy,
|
|
77
|
+
result: alwaysTrue ? true : alwaysFalsy ? false : undefined,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Static if:
|
|
82
|
+
// - ANY condition is static AND truthy
|
|
83
|
+
// - ALL conditions are static AND falsy
|
|
84
|
+
// - EMPTY argument list is always false
|
|
85
|
+
case 'or': {
|
|
86
|
+
const results = formula.arguments.map((arg) =>
|
|
87
|
+
contextlessEvaluateFormula(arg.formula),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const alwaysFalsy =
|
|
91
|
+
results.length === 0 ||
|
|
92
|
+
results.every((res) => res.isStatic && Boolean(res.result) === false)
|
|
93
|
+
const alwaysTrue = results.some(
|
|
94
|
+
(res) => res.isStatic && Boolean(res.result) === true,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
isStatic: alwaysTrue || alwaysFalsy,
|
|
99
|
+
result: alwaysFalsy ? false : alwaysTrue ? true : undefined,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
// For now, we assume that any other formula is not static.
|
|
105
|
+
return {
|
|
106
|
+
isStatic: false,
|
|
107
|
+
result: undefined,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { NodeModel } from '@nordcraft/core/dist/component/component.types'
|
|
2
|
+
import { get, omit, set } from '@nordcraft/core/dist/utils/collections'
|
|
2
3
|
import type { FixFunction, NodeType } from '../types'
|
|
3
4
|
|
|
4
5
|
export const removeFromPathFix: FixFunction<NodeType> = ({ path, files }) =>
|
|
5
6
|
omit(files, path)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Same as removeFromPathFix, but also removes the node from its parent's children.
|
|
10
|
+
*/
|
|
11
|
+
export const removeNodeFromPathFix: FixFunction<NodeType> = (data) => {
|
|
12
|
+
if (data.nodeType !== 'component-node') {
|
|
13
|
+
throw new Error('removeNodeFromPathFix can only be used on component nodes')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const componentNodesPath = data.path.slice(0, -1).map(String)
|
|
17
|
+
const filesWithoutNode = removeFromPathFix(data)
|
|
18
|
+
const nodes = get(filesWithoutNode, componentNodesPath) as Record<
|
|
19
|
+
string,
|
|
20
|
+
NodeModel
|
|
21
|
+
>
|
|
22
|
+
for (const key in nodes) {
|
|
23
|
+
if (
|
|
24
|
+
nodes[key].children?.includes(data.path[data.path.length - 1] as string)
|
|
25
|
+
) {
|
|
26
|
+
nodes[key].children = nodes[key].children.filter(
|
|
27
|
+
(p) => p !== data.path[data.path.length - 1],
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return set(filesWithoutNode, componentNodesPath, nodes)
|
|
33
|
+
}
|