typekro 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/core/composition/imperative.d.ts.map +1 -1
- package/dist/core/composition/imperative.js +15 -2
- package/dist/core/composition/imperative.js.map +1 -1
- package/dist/core/composition/typekro-runtime/typekro-runtime.d.ts.map +1 -1
- package/dist/core/composition/typekro-runtime/typekro-runtime.js +24 -25
- package/dist/core/composition/typekro-runtime/typekro-runtime.js.map +1 -1
- package/dist/core/dependencies/type-guards.d.ts.map +1 -1
- package/dist/core/dependencies/type-guards.js +7 -2
- package/dist/core/dependencies/type-guards.js.map +1 -1
- package/dist/core/deployment/engine.d.ts +13 -1
- package/dist/core/deployment/engine.d.ts.map +1 -1
- package/dist/core/deployment/engine.js +48 -3
- package/dist/core/deployment/engine.js.map +1 -1
- package/dist/core/errors.d.ts +85 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +135 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/evaluation/cel-optimizer.d.ts.map +1 -1
- package/dist/core/evaluation/cel-optimizer.js +7 -13
- package/dist/core/evaluation/cel-optimizer.js.map +1 -1
- package/dist/core/expressions/analyzer.d.ts +584 -0
- package/dist/core/expressions/analyzer.d.ts.map +1 -0
- package/dist/core/expressions/analyzer.js +2956 -0
- package/dist/core/expressions/analyzer.js.map +1 -0
- package/dist/core/expressions/cache.d.ts +136 -0
- package/dist/core/expressions/cache.d.ts.map +1 -0
- package/dist/core/expressions/cache.js +347 -0
- package/dist/core/expressions/cache.js.map +1 -0
- package/dist/core/expressions/cel-conversion-engine.d.ts +126 -0
- package/dist/core/expressions/cel-conversion-engine.d.ts.map +1 -0
- package/dist/core/expressions/cel-conversion-engine.js +293 -0
- package/dist/core/expressions/cel-conversion-engine.js.map +1 -0
- package/dist/core/expressions/compile-time-validation.d.ts +270 -0
- package/dist/core/expressions/compile-time-validation.d.ts.map +1 -0
- package/dist/core/expressions/compile-time-validation.js +506 -0
- package/dist/core/expressions/compile-time-validation.js.map +1 -0
- package/dist/core/expressions/composition-integration.d.ts +315 -0
- package/dist/core/expressions/composition-integration.d.ts.map +1 -0
- package/dist/core/expressions/composition-integration.js +936 -0
- package/dist/core/expressions/composition-integration.js.map +1 -0
- package/dist/core/expressions/conditional-expression-processor.d.ts +154 -0
- package/dist/core/expressions/conditional-expression-processor.d.ts.map +1 -0
- package/dist/core/expressions/conditional-expression-processor.js +479 -0
- package/dist/core/expressions/conditional-expression-processor.js.map +1 -0
- package/dist/core/expressions/conditional-integration.d.ts +133 -0
- package/dist/core/expressions/conditional-integration.d.ts.map +1 -0
- package/dist/core/expressions/conditional-integration.js +293 -0
- package/dist/core/expressions/conditional-integration.js.map +1 -0
- package/dist/core/expressions/conditional-validation.d.ts +181 -0
- package/dist/core/expressions/conditional-validation.d.ts.map +1 -0
- package/dist/core/expressions/conditional-validation.js +460 -0
- package/dist/core/expressions/conditional-validation.js.map +1 -0
- package/dist/core/expressions/context-aware-generator.d.ts +127 -0
- package/dist/core/expressions/context-aware-generator.d.ts.map +1 -0
- package/dist/core/expressions/context-aware-generator.js +500 -0
- package/dist/core/expressions/context-aware-generator.js.map +1 -0
- package/dist/core/expressions/context-detector.d.ts +148 -0
- package/dist/core/expressions/context-detector.d.ts.map +1 -0
- package/dist/core/expressions/context-detector.js +546 -0
- package/dist/core/expressions/context-detector.js.map +1 -0
- package/dist/core/expressions/context-switcher.d.ts +185 -0
- package/dist/core/expressions/context-switcher.d.ts.map +1 -0
- package/dist/core/expressions/context-switcher.js +515 -0
- package/dist/core/expressions/context-switcher.js.map +1 -0
- package/dist/core/expressions/context-validator.d.ts +176 -0
- package/dist/core/expressions/context-validator.d.ts.map +1 -0
- package/dist/core/expressions/context-validator.js +452 -0
- package/dist/core/expressions/context-validator.js.map +1 -0
- package/dist/core/expressions/custom-context-manager.d.ts +194 -0
- package/dist/core/expressions/custom-context-manager.d.ts.map +1 -0
- package/dist/core/expressions/custom-context-manager.js +390 -0
- package/dist/core/expressions/custom-context-manager.js.map +1 -0
- package/dist/core/expressions/expression-proxy.d.ts +80 -0
- package/dist/core/expressions/expression-proxy.d.ts.map +1 -0
- package/dist/core/expressions/expression-proxy.js +227 -0
- package/dist/core/expressions/expression-proxy.js.map +1 -0
- package/dist/core/expressions/factory-integration.d.ts +132 -0
- package/dist/core/expressions/factory-integration.d.ts.map +1 -0
- package/dist/core/expressions/factory-integration.js +327 -0
- package/dist/core/expressions/factory-integration.js.map +1 -0
- package/dist/core/expressions/factory-pattern-handler.d.ts +88 -0
- package/dist/core/expressions/factory-pattern-handler.d.ts.map +1 -0
- package/dist/core/expressions/factory-pattern-handler.js +336 -0
- package/dist/core/expressions/factory-pattern-handler.js.map +1 -0
- package/dist/core/expressions/field-hydration-processor.d.ts +188 -0
- package/dist/core/expressions/field-hydration-processor.d.ts.map +1 -0
- package/dist/core/expressions/field-hydration-processor.js +562 -0
- package/dist/core/expressions/field-hydration-processor.js.map +1 -0
- package/dist/core/expressions/imperative-analyzer.d.ts +21 -0
- package/dist/core/expressions/imperative-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/imperative-analyzer.js +457 -0
- package/dist/core/expressions/imperative-analyzer.js.map +1 -0
- package/dist/core/expressions/index.d.ts +54 -0
- package/dist/core/expressions/index.d.ts.map +1 -0
- package/dist/core/expressions/index.js +50 -0
- package/dist/core/expressions/index.js.map +1 -0
- package/dist/core/expressions/lazy-analysis.d.ts +1128 -0
- package/dist/core/expressions/lazy-analysis.d.ts.map +1 -0
- package/dist/core/expressions/lazy-analysis.js +2443 -0
- package/dist/core/expressions/lazy-analysis.js.map +1 -0
- package/dist/core/expressions/magic-assignable-analyzer.d.ts +123 -0
- package/dist/core/expressions/magic-assignable-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/magic-assignable-analyzer.js +352 -0
- package/dist/core/expressions/magic-assignable-analyzer.js.map +1 -0
- package/dist/core/expressions/magic-proxy-analyzer.d.ts +206 -0
- package/dist/core/expressions/magic-proxy-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/magic-proxy-analyzer.js +639 -0
- package/dist/core/expressions/magic-proxy-analyzer.js.map +1 -0
- package/dist/core/expressions/magic-proxy-detector.d.ts +154 -0
- package/dist/core/expressions/magic-proxy-detector.d.ts.map +1 -0
- package/dist/core/expressions/magic-proxy-detector.js +242 -0
- package/dist/core/expressions/magic-proxy-detector.js.map +1 -0
- package/dist/core/expressions/migration-helpers.d.ts +133 -0
- package/dist/core/expressions/migration-helpers.d.ts.map +1 -0
- package/dist/core/expressions/migration-helpers.js +443 -0
- package/dist/core/expressions/migration-helpers.js.map +1 -0
- package/dist/core/expressions/optionality-handler.d.ts +503 -0
- package/dist/core/expressions/optionality-handler.d.ts.map +1 -0
- package/dist/core/expressions/optionality-handler.js +1306 -0
- package/dist/core/expressions/optionality-handler.js.map +1 -0
- package/dist/core/expressions/readiness-integration.d.ts +119 -0
- package/dist/core/expressions/readiness-integration.d.ts.map +1 -0
- package/dist/core/expressions/readiness-integration.js +386 -0
- package/dist/core/expressions/readiness-integration.js.map +1 -0
- package/dist/core/expressions/resource-analyzer.d.ts +486 -0
- package/dist/core/expressions/resource-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/resource-analyzer.js +1086 -0
- package/dist/core/expressions/resource-analyzer.js.map +1 -0
- package/dist/core/expressions/resource-validation.d.ts +187 -0
- package/dist/core/expressions/resource-validation.d.ts.map +1 -0
- package/dist/core/expressions/resource-validation.js +552 -0
- package/dist/core/expressions/resource-validation.js.map +1 -0
- package/dist/core/expressions/runtime-error-mapper.d.ts +138 -0
- package/dist/core/expressions/runtime-error-mapper.d.ts.map +1 -0
- package/dist/core/expressions/runtime-error-mapper.js +412 -0
- package/dist/core/expressions/runtime-error-mapper.js.map +1 -0
- package/dist/core/expressions/source-map.d.ts +168 -0
- package/dist/core/expressions/source-map.d.ts.map +1 -0
- package/dist/core/expressions/source-map.js +350 -0
- package/dist/core/expressions/source-map.js.map +1 -0
- package/dist/core/expressions/status-builder-analyzer.d.ts +353 -0
- package/dist/core/expressions/status-builder-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/status-builder-analyzer.js +1311 -0
- package/dist/core/expressions/status-builder-analyzer.js.map +1 -0
- package/dist/core/expressions/type-inference.d.ts +184 -0
- package/dist/core/expressions/type-inference.d.ts.map +1 -0
- package/dist/core/expressions/type-inference.js +838 -0
- package/dist/core/expressions/type-inference.js.map +1 -0
- package/dist/core/expressions/type-safety.d.ts +203 -0
- package/dist/core/expressions/type-safety.d.ts.map +1 -0
- package/dist/core/expressions/type-safety.js +442 -0
- package/dist/core/expressions/type-safety.js.map +1 -0
- package/dist/core/expressions/types.d.ts +282 -0
- package/dist/core/expressions/types.d.ts.map +1 -0
- package/dist/core/expressions/types.js +8 -0
- package/dist/core/expressions/types.js.map +1 -0
- package/dist/core/kubernetes/client-provider.js +2 -2
- package/dist/core/kubernetes/client-provider.js.map +1 -1
- package/dist/core/references/cel.d.ts +13 -1
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +50 -0
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/references/schema-proxy.d.ts.map +1 -1
- package/dist/core/references/schema-proxy.js +13 -8
- package/dist/core/references/schema-proxy.js.map +1 -1
- package/dist/core/serialization/core.d.ts.map +1 -1
- package/dist/core/serialization/core.js +573 -9
- package/dist/core/serialization/core.js.map +1 -1
- package/dist/core/types/deployment.d.ts +7 -0
- package/dist/core/types/deployment.d.ts.map +1 -1
- package/dist/core/types/deployment.js +7 -0
- package/dist/core/types/deployment.js.map +1 -1
- package/dist/core/types/index.d.ts +1 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js.map +1 -1
- package/dist/core/validation/cel-validator.d.ts.map +1 -1
- package/dist/core/validation/cel-validator.js +4 -8
- package/dist/core/validation/cel-validator.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/factories/helm/helm-release.d.ts.map +1 -1
- package/dist/factories/helm/helm-release.js +0 -5
- package/dist/factories/helm/helm-release.js.map +1 -1
- package/dist/factories/helm/types.d.ts +1 -1
- package/dist/factories/helm/types.d.ts.map +1 -1
- package/dist/factories/index.js +1 -0
- package/dist/factories/index.js.map +1 -1
- package/dist/factories/shared.d.ts.map +1 -1
- package/dist/factories/shared.js +21 -1
- package/dist/factories/shared.js.map +1 -1
- package/dist/factories/simple/index.d.ts +2 -2
- package/dist/factories/simple/index.d.ts.map +1 -1
- package/dist/factories/simple/workloads/deployment.d.ts +3 -3
- package/dist/factories/simple/workloads/deployment.d.ts.map +1 -1
- package/dist/factories/simple/workloads/deployment.js +37 -11
- package/dist/factories/simple/workloads/deployment.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/type-guards.d.ts +6 -0
- package/dist/utils/type-guards.d.ts.map +1 -1
- package/dist/utils/type-guards.js +25 -2
- package/dist/utils/type-guards.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,2956 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript to CEL Expression Analyzer
|
|
3
|
+
*
|
|
4
|
+
* This module provides the core functionality for detecting KubernetesRef objects
|
|
5
|
+
* in JavaScript expressions and converting them to appropriate CEL expressions.
|
|
6
|
+
*
|
|
7
|
+
* The analyzer works with TypeKro's magic proxy system where schema.spec.name and
|
|
8
|
+
* resources.database.status.podIP return KubernetesRef objects at runtime.
|
|
9
|
+
*/
|
|
10
|
+
import * as esprima from 'esprima';
|
|
11
|
+
import * as estraverse from 'estraverse';
|
|
12
|
+
import { containsKubernetesRefs, extractResourceReferences, isKubernetesRef } from '../../utils/type-guards.js';
|
|
13
|
+
import { CEL_EXPRESSION_BRAND, KUBERNETES_REF_BRAND } from '../constants/brands.js';
|
|
14
|
+
import { ConversionError } from '../errors.js';
|
|
15
|
+
import { SourceMapUtils } from './source-map.js';
|
|
16
|
+
import { ExpressionCache } from './cache.js';
|
|
17
|
+
import { handleExpressionWithFactoryPattern } from './factory-pattern-handler.js';
|
|
18
|
+
import { ExpressionTypeValidator, TypeRegistry, TypeSafetyUtils } from './type-safety.js';
|
|
19
|
+
import { CelTypeInferenceEngine } from './type-inference.js';
|
|
20
|
+
import { ResourceReferenceValidator } from './resource-validation.js';
|
|
21
|
+
import { CompileTimeTypeChecker } from './compile-time-validation.js';
|
|
22
|
+
/**
|
|
23
|
+
* Main analyzer class for JavaScript to CEL expression conversion
|
|
24
|
+
*/
|
|
25
|
+
export class JavaScriptToCelAnalyzer {
|
|
26
|
+
cache;
|
|
27
|
+
typeValidator = new ExpressionTypeValidator();
|
|
28
|
+
enableMetrics;
|
|
29
|
+
constructor(cacheOptions) {
|
|
30
|
+
this.cache = new ExpressionCache(cacheOptions);
|
|
31
|
+
this.enableMetrics = cacheOptions?.enableMetrics ?? true;
|
|
32
|
+
}
|
|
33
|
+
typeInferenceEngine = new CelTypeInferenceEngine();
|
|
34
|
+
resourceValidator = new ResourceReferenceValidator();
|
|
35
|
+
compileTimeChecker = new CompileTimeTypeChecker();
|
|
36
|
+
/**
|
|
37
|
+
* Analyze any expression type and convert to CEL if needed
|
|
38
|
+
*/
|
|
39
|
+
analyzeExpression(expression, context) {
|
|
40
|
+
// Handle different expression types
|
|
41
|
+
if (typeof expression === 'string') {
|
|
42
|
+
return this.analyzeStringExpression(expression, context);
|
|
43
|
+
}
|
|
44
|
+
// Handle KubernetesRef objects directly
|
|
45
|
+
if (expression && typeof expression === 'object' && expression[KUBERNETES_REF_BRAND]) {
|
|
46
|
+
return this.analyzeKubernetesRefObject(expression, context);
|
|
47
|
+
}
|
|
48
|
+
// Handle other objects
|
|
49
|
+
if (typeof expression === 'object' && expression !== null) {
|
|
50
|
+
return this.analyzeObjectExpression(expression, context);
|
|
51
|
+
}
|
|
52
|
+
// Handle primitives
|
|
53
|
+
return this.analyzePrimitiveExpression(expression, context);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Analyze a JavaScript string expression and convert to CEL if it contains KubernetesRef objects
|
|
57
|
+
*/
|
|
58
|
+
analyzeStringExpression(expression, context) {
|
|
59
|
+
// Check cache first
|
|
60
|
+
const cached = this.cache.get(expression, context);
|
|
61
|
+
if (cached)
|
|
62
|
+
return cached;
|
|
63
|
+
try {
|
|
64
|
+
// Handle modern JavaScript syntax that esprima 4.0.1 doesn't support
|
|
65
|
+
const preprocessedExpression = this.preprocessModernSyntax(expression);
|
|
66
|
+
// Parse JavaScript expression to AST with location tracking
|
|
67
|
+
// Try parsing as expression first, then fall back to wrapping in parentheses
|
|
68
|
+
let ast;
|
|
69
|
+
let exprNode;
|
|
70
|
+
try {
|
|
71
|
+
// Try parsing as a standalone expression first
|
|
72
|
+
ast = esprima.parseScript(preprocessedExpression, {
|
|
73
|
+
ecmaVersion: 2020,
|
|
74
|
+
sourceType: 'script',
|
|
75
|
+
loc: true,
|
|
76
|
+
range: true
|
|
77
|
+
});
|
|
78
|
+
exprNode = ast.body[0];
|
|
79
|
+
// If it's not an expression statement, we need to wrap it
|
|
80
|
+
if (exprNode.type !== 'ExpressionStatement') {
|
|
81
|
+
throw new Error('Not an expression statement');
|
|
82
|
+
}
|
|
83
|
+
exprNode = exprNode.expression;
|
|
84
|
+
}
|
|
85
|
+
catch (firstError) {
|
|
86
|
+
// Fall back to wrapping in parentheses
|
|
87
|
+
try {
|
|
88
|
+
ast = esprima.parseScript(`(${preprocessedExpression})`, {
|
|
89
|
+
ecmaVersion: 2020,
|
|
90
|
+
sourceType: 'script',
|
|
91
|
+
loc: true,
|
|
92
|
+
range: true
|
|
93
|
+
});
|
|
94
|
+
exprNode = ast.body[0].expression;
|
|
95
|
+
}
|
|
96
|
+
catch (_secondError) {
|
|
97
|
+
// Use the original error message from the first attempt
|
|
98
|
+
throw firstError;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Create source location from AST
|
|
102
|
+
const sourceLocation = SourceMapUtils.createSourceLocation(exprNode.loc, expression);
|
|
103
|
+
// Initialize dependencies array if not provided
|
|
104
|
+
if (!context.dependencies) {
|
|
105
|
+
context.dependencies = [];
|
|
106
|
+
}
|
|
107
|
+
// Convert to CEL with source tracking (this will extract dependencies through AST analysis)
|
|
108
|
+
const celExpression = this.convertASTNodeWithSourceTracking(exprNode, context, expression, sourceLocation);
|
|
109
|
+
// Add source mapping entry
|
|
110
|
+
const sourceMapEntries = [];
|
|
111
|
+
if (context.sourceMap) {
|
|
112
|
+
const _mappingId = context.sourceMap.addMapping(expression, celExpression.expression, sourceLocation, context.type, {
|
|
113
|
+
expressionType: SourceMapUtils.determineExpressionType(exprNode.type),
|
|
114
|
+
kubernetesRefs: SourceMapUtils.extractKubernetesRefPaths(celExpression.expression),
|
|
115
|
+
dependencies: context.dependencies?.map(dep => `${dep.resourceId}.${dep.fieldPath}`) || [],
|
|
116
|
+
conversionNotes: [`Converted from ${exprNode.type} AST node`]
|
|
117
|
+
});
|
|
118
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
119
|
+
}
|
|
120
|
+
// Perform type validation and inference if enabled
|
|
121
|
+
let typeValidation;
|
|
122
|
+
let inferredType;
|
|
123
|
+
let resourceValidation;
|
|
124
|
+
if (context.strictTypeChecking !== false && context.typeRegistry) {
|
|
125
|
+
const availableTypes = context.typeRegistry.getAvailableTypes();
|
|
126
|
+
typeValidation = this.typeValidator.validateExpression(expression, availableTypes, context.expectedType);
|
|
127
|
+
inferredType = typeValidation.resultType;
|
|
128
|
+
// Also perform CEL type inference
|
|
129
|
+
const celTypeInference = this.inferCelExpressionType(celExpression, context);
|
|
130
|
+
if (celTypeInference.success && !inferredType) {
|
|
131
|
+
inferredType = celTypeInference.resultType;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Validate resource references if enabled
|
|
135
|
+
if (context.validateResourceReferences !== false && context.dependencies) {
|
|
136
|
+
resourceValidation = this.validateResourceReferences(context.dependencies, context.availableReferences, context.schemaProxy, context.validationContext);
|
|
137
|
+
}
|
|
138
|
+
// Perform compile-time type checking if enabled
|
|
139
|
+
let compileTimeValidation;
|
|
140
|
+
if (context.compileTimeTypeChecking !== false && context.compileTimeContext) {
|
|
141
|
+
compileTimeValidation = this.performCompileTimeValidation(expression, context.compileTimeContext);
|
|
142
|
+
}
|
|
143
|
+
// Only treat compile-time and type validation errors as critical
|
|
144
|
+
// Resource validation errors should be warnings, not critical errors
|
|
145
|
+
const hasCompileTimeErrors = compileTimeValidation && !compileTimeValidation.valid;
|
|
146
|
+
const hasTypeValidationErrors = typeValidation && !typeValidation.valid;
|
|
147
|
+
// Collect only critical validation errors that should affect validity
|
|
148
|
+
const criticalErrors = [];
|
|
149
|
+
if (compileTimeValidation?.errors) {
|
|
150
|
+
criticalErrors.push(...compileTimeValidation.errors);
|
|
151
|
+
}
|
|
152
|
+
if (typeValidation?.errors) {
|
|
153
|
+
criticalErrors.push(...typeValidation.errors);
|
|
154
|
+
}
|
|
155
|
+
// Resource validation errors are treated as warnings, not critical errors
|
|
156
|
+
// Aggregate warnings from all validation results
|
|
157
|
+
const aggregatedWarnings = [];
|
|
158
|
+
// Add resource validation warnings and errors (treat errors as warnings)
|
|
159
|
+
if (resourceValidation) {
|
|
160
|
+
for (const rv of resourceValidation) {
|
|
161
|
+
// Add warnings
|
|
162
|
+
for (const warning of rv.warnings) {
|
|
163
|
+
const warningObj = {
|
|
164
|
+
message: warning.message,
|
|
165
|
+
type: warning.warningType
|
|
166
|
+
};
|
|
167
|
+
if (rv.suggestions.length > 0) {
|
|
168
|
+
warningObj.suggestion = rv.suggestions.join('; ');
|
|
169
|
+
}
|
|
170
|
+
aggregatedWarnings.push(warningObj);
|
|
171
|
+
}
|
|
172
|
+
// Add errors as warnings (resource validation errors shouldn't fail the entire expression)
|
|
173
|
+
if (rv.errors) {
|
|
174
|
+
for (const error of rv.errors) {
|
|
175
|
+
const warningObj = {
|
|
176
|
+
message: error instanceof Error ? error.message : String(error),
|
|
177
|
+
type: 'resource_validation'
|
|
178
|
+
};
|
|
179
|
+
if (rv.suggestions.length > 0) {
|
|
180
|
+
warningObj.suggestion = rv.suggestions.join('; ');
|
|
181
|
+
}
|
|
182
|
+
aggregatedWarnings.push(warningObj);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Add type validation warnings (if any)
|
|
188
|
+
if (typeValidation?.warnings) {
|
|
189
|
+
for (const warning of typeValidation.warnings) {
|
|
190
|
+
aggregatedWarnings.push({
|
|
191
|
+
message: warning.message,
|
|
192
|
+
type: 'type_validation'
|
|
193
|
+
// No suggestion property for TypeValidationWarning
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Add compile-time validation warnings (if any)
|
|
198
|
+
if (compileTimeValidation?.warnings) {
|
|
199
|
+
for (const warning of compileTimeValidation.warnings) {
|
|
200
|
+
aggregatedWarnings.push({
|
|
201
|
+
message: warning.message,
|
|
202
|
+
type: 'compile_time'
|
|
203
|
+
// No suggestion property for CompileTimeWarning
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const result = {
|
|
208
|
+
valid: celExpression !== null && !hasCompileTimeErrors && !hasTypeValidationErrors,
|
|
209
|
+
celExpression,
|
|
210
|
+
dependencies: context.dependencies || [],
|
|
211
|
+
sourceMap: sourceMapEntries,
|
|
212
|
+
errors: criticalErrors,
|
|
213
|
+
requiresConversion: (context.dependencies || []).length > 0, // Only requires conversion if there are KubernetesRef dependencies
|
|
214
|
+
typeValidation,
|
|
215
|
+
inferredType,
|
|
216
|
+
resourceValidation,
|
|
217
|
+
compileTimeValidation,
|
|
218
|
+
warnings: aggregatedWarnings
|
|
219
|
+
};
|
|
220
|
+
// Cache the result
|
|
221
|
+
this.cache.set(expression, context, result);
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// If parsing fails, try to handle it as a special case
|
|
226
|
+
const specialCaseResult = this.handleSpecialCases(expression, context);
|
|
227
|
+
if (specialCaseResult) {
|
|
228
|
+
// Only cache successful special case results
|
|
229
|
+
if (specialCaseResult.valid) {
|
|
230
|
+
this.cache.set(expression, context, specialCaseResult);
|
|
231
|
+
}
|
|
232
|
+
return specialCaseResult;
|
|
233
|
+
}
|
|
234
|
+
// Create detailed error with source location
|
|
235
|
+
const sourceLocation = { line: 1, column: 1, length: expression.length };
|
|
236
|
+
const conversionError = ConversionError.forParsingFailure(expression, error instanceof Error ? error.message : String(error), sourceLocation, error instanceof Error ? error : undefined);
|
|
237
|
+
const errorResult = {
|
|
238
|
+
valid: false,
|
|
239
|
+
celExpression: null,
|
|
240
|
+
dependencies: [],
|
|
241
|
+
sourceMap: [],
|
|
242
|
+
errors: [conversionError],
|
|
243
|
+
requiresConversion: false,
|
|
244
|
+
warnings: []
|
|
245
|
+
};
|
|
246
|
+
// Don't cache error results to allow retry
|
|
247
|
+
return errorResult;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Analyze a KubernetesRef object directly
|
|
252
|
+
*/
|
|
253
|
+
analyzeKubernetesRefObject(ref, context) {
|
|
254
|
+
// Use the proper CEL path format
|
|
255
|
+
const resourceId = ref.resourceId === '__schema__' ? 'schema' : ref.resourceId;
|
|
256
|
+
const celPath = `${resourceId}.${ref.fieldPath}`;
|
|
257
|
+
// Add to dependencies
|
|
258
|
+
if (!context.dependencies) {
|
|
259
|
+
context.dependencies = [];
|
|
260
|
+
}
|
|
261
|
+
context.dependencies.push(ref);
|
|
262
|
+
return {
|
|
263
|
+
valid: true,
|
|
264
|
+
celExpression: {
|
|
265
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
266
|
+
expression: celPath,
|
|
267
|
+
_type: ref._type
|
|
268
|
+
},
|
|
269
|
+
dependencies: [ref],
|
|
270
|
+
sourceMap: [],
|
|
271
|
+
errors: [],
|
|
272
|
+
warnings: [],
|
|
273
|
+
requiresConversion: true
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Analyze object expression by examining its structure
|
|
278
|
+
*/
|
|
279
|
+
analyzeObjectExpression(obj, context) {
|
|
280
|
+
const kubernetesRefs = [];
|
|
281
|
+
// Recursively examine object properties for KubernetesRef objects
|
|
282
|
+
this.extractKubernetesRefsFromObject(obj, kubernetesRefs, '');
|
|
283
|
+
// Add to dependencies
|
|
284
|
+
if (!context.dependencies) {
|
|
285
|
+
context.dependencies = [];
|
|
286
|
+
}
|
|
287
|
+
context.dependencies.push(...kubernetesRefs);
|
|
288
|
+
return {
|
|
289
|
+
valid: true,
|
|
290
|
+
celExpression: null, // Objects don't convert to single CEL expressions
|
|
291
|
+
dependencies: kubernetesRefs,
|
|
292
|
+
sourceMap: [],
|
|
293
|
+
errors: [],
|
|
294
|
+
warnings: [],
|
|
295
|
+
requiresConversion: kubernetesRefs.length > 0
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Analyze primitive expression (no KubernetesRef objects)
|
|
300
|
+
*/
|
|
301
|
+
analyzePrimitiveExpression(_value, _context) {
|
|
302
|
+
return {
|
|
303
|
+
valid: true,
|
|
304
|
+
celExpression: null,
|
|
305
|
+
dependencies: [],
|
|
306
|
+
sourceMap: [],
|
|
307
|
+
errors: [],
|
|
308
|
+
warnings: [],
|
|
309
|
+
requiresConversion: false
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Extract KubernetesRef objects from object structure
|
|
314
|
+
*/
|
|
315
|
+
extractKubernetesRefsFromObject(obj, refs, path) {
|
|
316
|
+
if (!obj || typeof obj !== 'object')
|
|
317
|
+
return;
|
|
318
|
+
// Check if this object is a KubernetesRef
|
|
319
|
+
if (obj[KUBERNETES_REF_BRAND]) {
|
|
320
|
+
refs.push(obj);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Recursively check properties
|
|
324
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
325
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
326
|
+
this.extractKubernetesRefsFromObject(value, refs, newPath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* NEW: Analyze expressions that may contain KubernetesRef objects from magic proxy system
|
|
331
|
+
* This is the key method that detects when JavaScript expressions contain KubernetesRef objects
|
|
332
|
+
*/
|
|
333
|
+
analyzeExpressionWithRefs(expression, // Could be a JavaScript expression or contain KubernetesRef objects
|
|
334
|
+
context) {
|
|
335
|
+
try {
|
|
336
|
+
// First check if this is a static value (no KubernetesRef objects)
|
|
337
|
+
if (this.isStaticValue(expression)) {
|
|
338
|
+
// Static values don't need conversion - preserve them as-is for performance
|
|
339
|
+
return this.createStaticValueResult(expression);
|
|
340
|
+
}
|
|
341
|
+
// Check if the expression contains KubernetesRef objects
|
|
342
|
+
if (!containsKubernetesRefs(expression)) {
|
|
343
|
+
// No KubernetesRef objects found - return as-is (no conversion needed)
|
|
344
|
+
return this.createStaticValueResult(expression);
|
|
345
|
+
}
|
|
346
|
+
// Expression contains KubernetesRef objects - needs conversion
|
|
347
|
+
if (typeof expression === 'string') {
|
|
348
|
+
// String expression - parse and convert
|
|
349
|
+
return this.analyzeExpression(expression, context);
|
|
350
|
+
}
|
|
351
|
+
if (typeof expression === 'function') {
|
|
352
|
+
// Function expression - analyze function body
|
|
353
|
+
return this.analyzeFunction(expression, context);
|
|
354
|
+
}
|
|
355
|
+
// Direct KubernetesRef object
|
|
356
|
+
if (isKubernetesRef(expression)) {
|
|
357
|
+
return this.convertKubernetesRefToResult(expression, context);
|
|
358
|
+
}
|
|
359
|
+
// Template literal or complex expression
|
|
360
|
+
if (this.isTemplateLiteral(expression)) {
|
|
361
|
+
return this.analyzeTemplateLiteral(expression, context);
|
|
362
|
+
}
|
|
363
|
+
// Complex object/array containing KubernetesRef objects
|
|
364
|
+
return this.analyzeComplexValue(expression, context);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
valid: false,
|
|
369
|
+
celExpression: null,
|
|
370
|
+
dependencies: [],
|
|
371
|
+
sourceMap: [],
|
|
372
|
+
errors: [new ConversionError(`Failed to analyze expression with refs: ${error instanceof Error ? error.message : String(error)}`, String(expression), 'javascript')],
|
|
373
|
+
requiresConversion: false,
|
|
374
|
+
warnings: []
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Convert an AST node to CEL expression
|
|
380
|
+
*/
|
|
381
|
+
convertASTNode(node, context) {
|
|
382
|
+
switch (node.type) {
|
|
383
|
+
case 'BinaryExpression':
|
|
384
|
+
return this.convertBinaryExpression(node, context);
|
|
385
|
+
case 'MemberExpression':
|
|
386
|
+
return this.convertMemberExpression(node, context);
|
|
387
|
+
case 'ConditionalExpression':
|
|
388
|
+
return this.convertConditionalExpression(node, context);
|
|
389
|
+
case 'LogicalExpression':
|
|
390
|
+
return this.convertLogicalExpression(node, context);
|
|
391
|
+
case 'ChainExpression':
|
|
392
|
+
return this.convertOptionalChaining(node, context);
|
|
393
|
+
case 'TemplateLiteral':
|
|
394
|
+
return this.convertTemplateLiteral(node, context);
|
|
395
|
+
case 'Literal':
|
|
396
|
+
return this.convertLiteral(node, context);
|
|
397
|
+
case 'CallExpression':
|
|
398
|
+
return this.convertCallExpression(node, context);
|
|
399
|
+
case 'ArrayExpression':
|
|
400
|
+
return this.convertArrayExpression(node, context);
|
|
401
|
+
case 'Identifier':
|
|
402
|
+
return this.convertIdentifier(node, context);
|
|
403
|
+
case 'UnaryExpression':
|
|
404
|
+
return this.convertUnaryExpression(node, context);
|
|
405
|
+
default:
|
|
406
|
+
throw new Error(`Unsupported expression type: ${node.type}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Convert an AST node to CEL expression with source location tracking
|
|
411
|
+
*/
|
|
412
|
+
convertASTNodeWithSourceTracking(node, context, originalExpression, sourceLocation) {
|
|
413
|
+
try {
|
|
414
|
+
const celExpression = this.convertASTNode(node, context);
|
|
415
|
+
// Add source mapping if builder is available
|
|
416
|
+
if (context.sourceMap) {
|
|
417
|
+
context.sourceMap.addMapping(originalExpression, celExpression.expression, sourceLocation, context.type, {
|
|
418
|
+
expressionType: SourceMapUtils.determineExpressionType(node.type),
|
|
419
|
+
kubernetesRefs: SourceMapUtils.extractKubernetesRefPaths(celExpression.expression),
|
|
420
|
+
dependencies: context.dependencies?.map(dep => `${dep.resourceId}.${dep.fieldPath}`) || [],
|
|
421
|
+
conversionNotes: [`Converted ${node.type} at line ${sourceLocation.line}, column ${sourceLocation.column}`]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return celExpression;
|
|
425
|
+
}
|
|
426
|
+
catch (_error) {
|
|
427
|
+
// Create detailed conversion error with source location
|
|
428
|
+
const conversionError = ConversionError.forUnsupportedSyntax(originalExpression, node.type, sourceLocation, [`The ${node.type} syntax is not supported in this context`]);
|
|
429
|
+
throw conversionError;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Analyze a function for JavaScript expressions containing KubernetesRef objects
|
|
434
|
+
*/
|
|
435
|
+
analyzeFunction(fn, _context) {
|
|
436
|
+
try {
|
|
437
|
+
// Parse function to AST
|
|
438
|
+
const ast = esprima.parseScript(fn.toString());
|
|
439
|
+
// Find return statement
|
|
440
|
+
const returnStatement = this.findReturnStatement(ast);
|
|
441
|
+
if (!returnStatement) {
|
|
442
|
+
throw new Error('Function must have a return statement for analysis');
|
|
443
|
+
}
|
|
444
|
+
// For now, return a placeholder result
|
|
445
|
+
return {
|
|
446
|
+
valid: false,
|
|
447
|
+
celExpression: {
|
|
448
|
+
expression: '/* TODO: Analyze function body */',
|
|
449
|
+
_type: undefined
|
|
450
|
+
},
|
|
451
|
+
dependencies: [],
|
|
452
|
+
sourceMap: [],
|
|
453
|
+
errors: [],
|
|
454
|
+
warnings: [],
|
|
455
|
+
requiresConversion: true
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
return {
|
|
460
|
+
valid: false,
|
|
461
|
+
celExpression: null,
|
|
462
|
+
dependencies: [],
|
|
463
|
+
sourceMap: [],
|
|
464
|
+
errors: [new ConversionError(error instanceof Error ? error.message : String(error), fn.toString(), 'function-call')],
|
|
465
|
+
warnings: [],
|
|
466
|
+
requiresConversion: false
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Convert a single KubernetesRef to a conversion result
|
|
472
|
+
*/
|
|
473
|
+
convertKubernetesRefToResult(ref, context) {
|
|
474
|
+
try {
|
|
475
|
+
// Use the dedicated KubernetesRef to CEL conversion method
|
|
476
|
+
const celExpression = this.convertKubernetesRefToCel(ref, context);
|
|
477
|
+
const originalExpression = `${ref.resourceId}.${ref.fieldPath}`;
|
|
478
|
+
// Create source location for the KubernetesRef
|
|
479
|
+
const sourceLocation = {
|
|
480
|
+
line: 1,
|
|
481
|
+
column: 1,
|
|
482
|
+
length: originalExpression.length
|
|
483
|
+
};
|
|
484
|
+
// Add source mapping
|
|
485
|
+
const sourceMapEntries = [];
|
|
486
|
+
if (context.sourceMap) {
|
|
487
|
+
context.sourceMap.addMapping(originalExpression, celExpression.expression, sourceLocation, context.type, {
|
|
488
|
+
expressionType: 'member-access',
|
|
489
|
+
kubernetesRefs: [originalExpression],
|
|
490
|
+
dependencies: [`${ref.resourceId}.${ref.fieldPath}`],
|
|
491
|
+
conversionNotes: ['Direct KubernetesRef to CEL conversion']
|
|
492
|
+
});
|
|
493
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
valid: true,
|
|
497
|
+
celExpression,
|
|
498
|
+
dependencies: [ref],
|
|
499
|
+
sourceMap: sourceMapEntries,
|
|
500
|
+
errors: [],
|
|
501
|
+
warnings: [],
|
|
502
|
+
requiresConversion: true
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
catch (_error) {
|
|
506
|
+
const originalExpression = `${ref.resourceId}.${ref.fieldPath}`;
|
|
507
|
+
const sourceLocation = { line: 1, column: 1, length: originalExpression.length };
|
|
508
|
+
const conversionError = ConversionError.forKubernetesRefResolution(originalExpression, originalExpression, Object.keys(context.availableReferences || {}), sourceLocation);
|
|
509
|
+
return {
|
|
510
|
+
valid: false,
|
|
511
|
+
celExpression: null,
|
|
512
|
+
dependencies: [ref],
|
|
513
|
+
sourceMap: [],
|
|
514
|
+
errors: [conversionError],
|
|
515
|
+
warnings: [],
|
|
516
|
+
requiresConversion: true
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Analyze complex values (objects/arrays) that may contain KubernetesRef objects
|
|
522
|
+
*/
|
|
523
|
+
analyzeComplexValue(value, context) {
|
|
524
|
+
const dependencies = [];
|
|
525
|
+
const errors = [];
|
|
526
|
+
try {
|
|
527
|
+
// Recursively find all KubernetesRef objects
|
|
528
|
+
this.extractKubernetesRefs(value, dependencies);
|
|
529
|
+
if (dependencies.length === 0) {
|
|
530
|
+
return {
|
|
531
|
+
valid: false,
|
|
532
|
+
celExpression: null,
|
|
533
|
+
dependencies: [],
|
|
534
|
+
sourceMap: [],
|
|
535
|
+
errors: [],
|
|
536
|
+
warnings: [],
|
|
537
|
+
requiresConversion: false
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
// For complex values, we'll need to analyze the structure
|
|
541
|
+
// This is a placeholder implementation - will be expanded in later tasks
|
|
542
|
+
const originalExpression = JSON.stringify(value, null, 2);
|
|
543
|
+
const celExpression = {
|
|
544
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
545
|
+
expression: `/* TODO: Convert complex value with ${dependencies.length} references */`,
|
|
546
|
+
_type: undefined
|
|
547
|
+
};
|
|
548
|
+
// Create source location for the complex value
|
|
549
|
+
const sourceLocation = {
|
|
550
|
+
line: 1,
|
|
551
|
+
column: 1,
|
|
552
|
+
length: originalExpression.length
|
|
553
|
+
};
|
|
554
|
+
// Add source mapping
|
|
555
|
+
const sourceMapEntries = [];
|
|
556
|
+
if (context.sourceMap) {
|
|
557
|
+
context.sourceMap.addMapping(originalExpression, celExpression.expression, sourceLocation, context.type, {
|
|
558
|
+
expressionType: 'javascript',
|
|
559
|
+
kubernetesRefs: dependencies.map(dep => `${dep.resourceId}.${dep.fieldPath}`),
|
|
560
|
+
dependencies: dependencies.map(dep => `${dep.resourceId}.${dep.fieldPath}`),
|
|
561
|
+
conversionNotes: [`Complex value with ${dependencies.length} KubernetesRef objects`]
|
|
562
|
+
});
|
|
563
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
valid: true,
|
|
567
|
+
celExpression,
|
|
568
|
+
dependencies,
|
|
569
|
+
sourceMap: sourceMapEntries,
|
|
570
|
+
errors,
|
|
571
|
+
warnings: [],
|
|
572
|
+
requiresConversion: true
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
const originalExpression = JSON.stringify(value);
|
|
577
|
+
const sourceLocation = { line: 1, column: 1, length: originalExpression.length };
|
|
578
|
+
const conversionError = ConversionError.forParsingFailure(originalExpression, error instanceof Error ? error.message : String(error), sourceLocation, error instanceof Error ? error : undefined);
|
|
579
|
+
errors.push(conversionError);
|
|
580
|
+
return {
|
|
581
|
+
valid: false,
|
|
582
|
+
celExpression: null,
|
|
583
|
+
dependencies,
|
|
584
|
+
sourceMap: [],
|
|
585
|
+
errors,
|
|
586
|
+
warnings: [],
|
|
587
|
+
requiresConversion: true
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Generate CEL expression from KubernetesRef based on context
|
|
593
|
+
* This handles the core KubernetesRef to CEL field path conversion (resourceId.fieldPath)
|
|
594
|
+
*/
|
|
595
|
+
generateCelFromKubernetesRef(ref, context) {
|
|
596
|
+
// Validate the KubernetesRef
|
|
597
|
+
if (!ref.resourceId || !ref.fieldPath) {
|
|
598
|
+
throw new Error(`Invalid KubernetesRef: missing resourceId or fieldPath`);
|
|
599
|
+
}
|
|
600
|
+
// Generate appropriate CEL expression based on factory type and resource type
|
|
601
|
+
if (context.factoryType === 'kro') {
|
|
602
|
+
// For Kro factory, generate CEL expressions for runtime evaluation by Kro controller
|
|
603
|
+
if (ref.resourceId === '__schema__') {
|
|
604
|
+
// Schema references: schema.spec.name, schema.status.ready
|
|
605
|
+
return `schema.${ref.fieldPath}`;
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
// Resource references: resources.database.status.podIP
|
|
609
|
+
return `resources.${ref.resourceId}.${ref.fieldPath}`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// For direct factory, generate CEL expressions that will be resolved at deployment time
|
|
614
|
+
// The direct factory will resolve these before deployment
|
|
615
|
+
if (ref.resourceId === '__schema__') {
|
|
616
|
+
// Schema references are resolved from the schema proxy
|
|
617
|
+
return `schema.${ref.fieldPath}`;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Resource references are resolved from the available resources
|
|
621
|
+
return `resources.${ref.resourceId}.${ref.fieldPath}`;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Convert a KubernetesRef directly to a CEL expression
|
|
627
|
+
* This is the main method for KubernetesRef to CEL field path conversion
|
|
628
|
+
*/
|
|
629
|
+
convertKubernetesRefToCel(ref, context) {
|
|
630
|
+
try {
|
|
631
|
+
// Validate KubernetesRef types if type checking is enabled
|
|
632
|
+
if (context.strictTypeChecking !== false && context.typeRegistry) {
|
|
633
|
+
const validation = this.typeValidator.validateKubernetesRef(ref, context.availableReferences, context.schemaProxy);
|
|
634
|
+
if (!validation.valid) {
|
|
635
|
+
throw new ConversionError(`KubernetesRef type validation failed: ${validation.errors.map(e => e.message).join(', ')}`, `${ref.resourceId}.${ref.fieldPath}`, 'member-access');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const expression = this.generateCelFromKubernetesRef(ref, context);
|
|
639
|
+
// Track this KubernetesRef as a dependency
|
|
640
|
+
if (context.dependencies) {
|
|
641
|
+
context.dependencies.push(ref);
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
645
|
+
expression,
|
|
646
|
+
_type: ref._type
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
throw new ConversionError(`Failed to convert KubernetesRef to CEL: ${error instanceof Error ? error.message : String(error)}`, `${ref.resourceId}.${ref.fieldPath}`, 'member-access');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Find return statement in AST
|
|
655
|
+
*/
|
|
656
|
+
findReturnStatement(ast) {
|
|
657
|
+
let returnStatement = null;
|
|
658
|
+
estraverse.traverse(ast, {
|
|
659
|
+
enter: (node) => {
|
|
660
|
+
if (node.type === 'ReturnStatement') {
|
|
661
|
+
returnStatement = node;
|
|
662
|
+
return estraverse.VisitorOption.Break;
|
|
663
|
+
}
|
|
664
|
+
return undefined; // Continue traversal
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
return returnStatement;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Check if a value is a template literal expression
|
|
671
|
+
* This checks for JavaScript template literal syntax in runtime values
|
|
672
|
+
*/
|
|
673
|
+
isTemplateLiteral(value) {
|
|
674
|
+
// Check if it's a string that looks like a template literal
|
|
675
|
+
if (typeof value === 'string') {
|
|
676
|
+
// Look for template literal patterns like `text ${expression} more text`
|
|
677
|
+
return value.includes('${') && value.includes('}');
|
|
678
|
+
}
|
|
679
|
+
// Check if it's an object that represents a template literal structure
|
|
680
|
+
if (value && typeof value === 'object' && value.type === 'TemplateLiteral') {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Analyze template literal expressions containing KubernetesRef objects
|
|
687
|
+
* This handles runtime template literal values that contain KubernetesRef interpolations
|
|
688
|
+
*/
|
|
689
|
+
analyzeTemplateLiteral(expression, context) {
|
|
690
|
+
try {
|
|
691
|
+
const dependencies = extractResourceReferences(expression);
|
|
692
|
+
const originalExpression = String(expression);
|
|
693
|
+
let celExpression;
|
|
694
|
+
if (typeof expression === 'string') {
|
|
695
|
+
// Handle string-based template literals
|
|
696
|
+
// For now, preserve the template literal structure
|
|
697
|
+
celExpression = {
|
|
698
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
699
|
+
expression: expression, // Keep the ${} syntax for CEL
|
|
700
|
+
_type: 'string'
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
// Handle structured template literal objects
|
|
705
|
+
// This would be used when we have parsed template literal AST nodes
|
|
706
|
+
celExpression = {
|
|
707
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
708
|
+
expression: '/* Complex template literal */',
|
|
709
|
+
_type: 'string'
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
// Create source location for the template literal
|
|
713
|
+
const sourceLocation = {
|
|
714
|
+
line: 1,
|
|
715
|
+
column: 1,
|
|
716
|
+
length: originalExpression.length
|
|
717
|
+
};
|
|
718
|
+
// Add source mapping
|
|
719
|
+
const sourceMapEntries = [];
|
|
720
|
+
if (context.sourceMap) {
|
|
721
|
+
context.sourceMap.addMapping(originalExpression, celExpression.expression, sourceLocation, context.type, {
|
|
722
|
+
expressionType: 'template-literal',
|
|
723
|
+
kubernetesRefs: dependencies.map(dep => `${dep.resourceId}.${dep.fieldPath}`),
|
|
724
|
+
dependencies: dependencies.map(dep => `${dep.resourceId}.${dep.fieldPath}`),
|
|
725
|
+
conversionNotes: ['Template literal with KubernetesRef interpolations']
|
|
726
|
+
});
|
|
727
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
valid: true,
|
|
731
|
+
celExpression,
|
|
732
|
+
dependencies,
|
|
733
|
+
sourceMap: sourceMapEntries,
|
|
734
|
+
errors: [],
|
|
735
|
+
warnings: [],
|
|
736
|
+
requiresConversion: true
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
const originalExpression = String(expression);
|
|
741
|
+
const sourceLocation = { line: 1, column: 1, length: originalExpression.length };
|
|
742
|
+
const conversionError = ConversionError.forTemplateLiteral(originalExpression, [originalExpression], 0, sourceLocation, error instanceof Error ? error : undefined);
|
|
743
|
+
return {
|
|
744
|
+
valid: false,
|
|
745
|
+
celExpression: null,
|
|
746
|
+
dependencies: [],
|
|
747
|
+
sourceMap: [],
|
|
748
|
+
errors: [conversionError],
|
|
749
|
+
warnings: [],
|
|
750
|
+
requiresConversion: true
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Extract all KubernetesRef objects from a complex value
|
|
756
|
+
*/
|
|
757
|
+
extractKubernetesRefs(value, refs) {
|
|
758
|
+
const extractedRefs = extractResourceReferences(value);
|
|
759
|
+
refs.push(...extractedRefs);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Handle special cases for expressions that can't be parsed normally
|
|
763
|
+
*/
|
|
764
|
+
handleSpecialCases(expression, context) {
|
|
765
|
+
// Handle expressions with both optional chaining and nullish coalescing
|
|
766
|
+
if (expression.includes('?.') && expression.includes('??')) {
|
|
767
|
+
return this.handleMixedOptionalAndNullishExpression(expression, context);
|
|
768
|
+
}
|
|
769
|
+
// Handle optional chaining expressions
|
|
770
|
+
if (expression.includes('?.')) {
|
|
771
|
+
return this.handleOptionalChainingExpression(expression, context);
|
|
772
|
+
}
|
|
773
|
+
// Handle nullish coalescing expressions
|
|
774
|
+
if (expression.includes('??')) {
|
|
775
|
+
return this.handleNullishCoalescingExpression(expression, context);
|
|
776
|
+
}
|
|
777
|
+
// Handle simple resource references that might not parse as valid JavaScript
|
|
778
|
+
if (expression.match(/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
|
|
779
|
+
// This looks like a simple property access path
|
|
780
|
+
try {
|
|
781
|
+
// Initialize dependencies array if not provided
|
|
782
|
+
if (!context.dependencies) {
|
|
783
|
+
context.dependencies = [];
|
|
784
|
+
}
|
|
785
|
+
// Extract dependencies from the expression
|
|
786
|
+
this.extractDependenciesFromExpression(expression, context);
|
|
787
|
+
const celExpression = {
|
|
788
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
789
|
+
expression: expression,
|
|
790
|
+
_type: undefined
|
|
791
|
+
};
|
|
792
|
+
const sourceLocation = { line: 1, column: 1, length: expression.length };
|
|
793
|
+
const sourceMapEntries = [];
|
|
794
|
+
if (context.sourceMap) {
|
|
795
|
+
context.sourceMap.addMapping(expression, expression, sourceLocation, context.type, {
|
|
796
|
+
expressionType: 'member-access',
|
|
797
|
+
kubernetesRefs: this.extractResourceReferencesFromExpression(expression),
|
|
798
|
+
dependencies: context.dependencies?.map(dep => `${dep.resourceId}.${dep.fieldPath}`) || [],
|
|
799
|
+
conversionNotes: ['Simple property access path']
|
|
800
|
+
});
|
|
801
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
802
|
+
}
|
|
803
|
+
// Perform resource validation if enabled
|
|
804
|
+
let resourceValidation;
|
|
805
|
+
if (context.validateResourceReferences !== false && context.dependencies && context.dependencies.length > 0) {
|
|
806
|
+
resourceValidation = this.validateResourceReferences(context.dependencies, context.availableReferences, context.schemaProxy, context.validationContext);
|
|
807
|
+
}
|
|
808
|
+
// Extract errors from resource validation
|
|
809
|
+
const errors = [];
|
|
810
|
+
if (resourceValidation) {
|
|
811
|
+
for (const validation of resourceValidation) {
|
|
812
|
+
for (const error of validation.errors) {
|
|
813
|
+
errors.push(new ConversionError(error.message, expression, 'member-access'));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
valid: errors.length === 0,
|
|
819
|
+
celExpression,
|
|
820
|
+
dependencies: context.dependencies || [],
|
|
821
|
+
sourceMap: sourceMapEntries,
|
|
822
|
+
errors,
|
|
823
|
+
warnings: [],
|
|
824
|
+
requiresConversion: true,
|
|
825
|
+
resourceValidation
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
catch (_error) {
|
|
829
|
+
// Fall through to return null
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Handle optional chaining expressions that can't be parsed by esprima
|
|
836
|
+
*/
|
|
837
|
+
handleOptionalChainingExpression(expression, context) {
|
|
838
|
+
try {
|
|
839
|
+
// First, validate that the expression is syntactically valid JavaScript
|
|
840
|
+
// even if esprima doesn't support optional chaining
|
|
841
|
+
// We'll do a basic syntax check by removing the optional chaining and parsing
|
|
842
|
+
const withoutOptionalChaining = expression
|
|
843
|
+
.replace(/\?\.\[/g, '[') // Replace ?.[ with [
|
|
844
|
+
.replace(/\?\.\(/g, '(') // Replace ?.( with (
|
|
845
|
+
.replace(/\?\./g, '.'); // Replace ?. with .
|
|
846
|
+
try {
|
|
847
|
+
// Try to parse the expression without optional chaining to validate basic syntax
|
|
848
|
+
esprima.parseScript(`(${withoutOptionalChaining})`);
|
|
849
|
+
}
|
|
850
|
+
catch (syntaxError) {
|
|
851
|
+
// If it fails to parse even without optional chaining, it has syntax errors
|
|
852
|
+
throw new ConversionError(`Invalid JavaScript syntax in optional chaining expression: ${syntaxError instanceof Error ? syntaxError.message : String(syntaxError)}`, expression, 'optional-chaining');
|
|
853
|
+
}
|
|
854
|
+
// Convert optional chaining to CEL-compatible syntax
|
|
855
|
+
// deployment?.status?.readyReplicas -> deployment?.status?.readyReplicas
|
|
856
|
+
const celExpression = {
|
|
857
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
858
|
+
expression: expression, // Keep the ?. syntax as CEL supports it
|
|
859
|
+
_type: undefined
|
|
860
|
+
};
|
|
861
|
+
const sourceLocation = { line: 1, column: 1, length: expression.length };
|
|
862
|
+
const sourceMapEntries = [];
|
|
863
|
+
// Extract dependencies from the optional chaining expression
|
|
864
|
+
const dependencies = this.extractDependenciesFromExpressionString(expression, context);
|
|
865
|
+
if (context.sourceMap) {
|
|
866
|
+
context.sourceMap.addMapping(expression, expression, sourceLocation, context.type, {
|
|
867
|
+
expressionType: 'optional-chaining',
|
|
868
|
+
kubernetesRefs: this.extractResourceReferencesFromExpression(expression),
|
|
869
|
+
dependencies: dependencies.map(dep => `${dep.resourceId}.${dep.fieldPath}`),
|
|
870
|
+
conversionNotes: ['Optional chaining expression']
|
|
871
|
+
});
|
|
872
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
valid: true,
|
|
876
|
+
celExpression,
|
|
877
|
+
dependencies,
|
|
878
|
+
sourceMap: sourceMapEntries,
|
|
879
|
+
errors: [],
|
|
880
|
+
warnings: [],
|
|
881
|
+
requiresConversion: true
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
return {
|
|
886
|
+
valid: false,
|
|
887
|
+
celExpression: null,
|
|
888
|
+
dependencies: [],
|
|
889
|
+
sourceMap: [],
|
|
890
|
+
errors: [new ConversionError(`Failed to handle optional chaining: ${error instanceof Error ? error.message : String(error)}`, expression, 'optional-chaining')],
|
|
891
|
+
warnings: [],
|
|
892
|
+
requiresConversion: true
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Handle expressions with both optional chaining and nullish coalescing
|
|
898
|
+
*/
|
|
899
|
+
handleMixedOptionalAndNullishExpression(expression, context) {
|
|
900
|
+
try {
|
|
901
|
+
// For mixed expressions, we'll convert them to a CEL expression that handles both
|
|
902
|
+
// Optional chaining and nullish coalescing together
|
|
903
|
+
// Example: deployment.status?.readyReplicas ?? deployment.spec?.replicas ?? 1
|
|
904
|
+
// Becomes: deployment.status?.readyReplicas != null ? deployment.status?.readyReplicas : (deployment.spec?.replicas != null ? deployment.spec?.replicas : 1)
|
|
905
|
+
// Split by nullish coalescing operator
|
|
906
|
+
const parts = expression.split('??').map(part => part.trim());
|
|
907
|
+
if (parts.length < 2) {
|
|
908
|
+
throw new Error('Invalid mixed expression');
|
|
909
|
+
}
|
|
910
|
+
// Build nested conditional expression from right to left
|
|
911
|
+
let celExpression = parts[parts.length - 1] || ''; // Start with the last part (fallback)
|
|
912
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
913
|
+
const part = parts[i];
|
|
914
|
+
celExpression = `${part} != null ? ${part} : ${celExpression}`;
|
|
915
|
+
}
|
|
916
|
+
const result = {
|
|
917
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
918
|
+
expression: celExpression,
|
|
919
|
+
_type: undefined
|
|
920
|
+
};
|
|
921
|
+
// Extract dependencies from the mixed expression
|
|
922
|
+
const dependencies = this.extractDependenciesFromExpressionString(expression, context);
|
|
923
|
+
const sourceLocation = { line: 1, column: 1, length: expression.length };
|
|
924
|
+
const sourceMapEntries = [];
|
|
925
|
+
if (context.sourceMap) {
|
|
926
|
+
context.sourceMap.addMapping(expression, result.expression, sourceLocation, context.type, {
|
|
927
|
+
expressionType: 'optional-chaining',
|
|
928
|
+
kubernetesRefs: this.extractResourceReferencesFromExpression(expression),
|
|
929
|
+
dependencies: dependencies.map(dep => `${dep.resourceId}.${dep.fieldPath}`),
|
|
930
|
+
conversionNotes: ['Mixed optional chaining and nullish coalescing converted to nested conditionals']
|
|
931
|
+
});
|
|
932
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
valid: true,
|
|
936
|
+
celExpression: result,
|
|
937
|
+
dependencies,
|
|
938
|
+
sourceMap: sourceMapEntries,
|
|
939
|
+
errors: [],
|
|
940
|
+
warnings: [],
|
|
941
|
+
requiresConversion: true
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
return {
|
|
946
|
+
valid: false,
|
|
947
|
+
celExpression: null,
|
|
948
|
+
dependencies: [],
|
|
949
|
+
sourceMap: [],
|
|
950
|
+
errors: [new ConversionError(`Failed to handle mixed optional chaining and nullish coalescing: ${error instanceof Error ? error.message : String(error)}`, expression, 'optional-chaining')],
|
|
951
|
+
warnings: [],
|
|
952
|
+
requiresConversion: true
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Handle nullish coalescing expressions that can't be parsed by esprima
|
|
958
|
+
*/
|
|
959
|
+
handleNullishCoalescingExpression(expression, context) {
|
|
960
|
+
try {
|
|
961
|
+
// Convert nullish coalescing to CEL-compatible syntax
|
|
962
|
+
// deployment.status.readyReplicas ?? 0 -> deployment.status.readyReplicas != null ? deployment.status.readyReplicas : 0
|
|
963
|
+
const parts = expression.split('??').map(part => part.trim());
|
|
964
|
+
if (parts.length !== 2) {
|
|
965
|
+
throw new Error('Invalid nullish coalescing expression');
|
|
966
|
+
}
|
|
967
|
+
const [left, right] = parts;
|
|
968
|
+
const celExpression = {
|
|
969
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
970
|
+
expression: `${left} != null ? ${left} : ${right}`,
|
|
971
|
+
_type: undefined
|
|
972
|
+
};
|
|
973
|
+
const sourceLocation = { line: 1, column: 1, length: expression.length };
|
|
974
|
+
const sourceMapEntries = [];
|
|
975
|
+
if (context.sourceMap) {
|
|
976
|
+
context.sourceMap.addMapping(expression, celExpression.expression, sourceLocation, context.type, {
|
|
977
|
+
expressionType: 'nullish-coalescing',
|
|
978
|
+
kubernetesRefs: this.extractResourceReferencesFromExpression(expression),
|
|
979
|
+
dependencies: this.extractResourceReferencesFromExpression(expression),
|
|
980
|
+
conversionNotes: ['Nullish coalescing converted to conditional']
|
|
981
|
+
});
|
|
982
|
+
sourceMapEntries.push(...context.sourceMap.getEntries());
|
|
983
|
+
}
|
|
984
|
+
// Extract dependencies from the nullish coalescing expression
|
|
985
|
+
const dependencies = this.extractDependenciesFromExpressionString(expression, context);
|
|
986
|
+
return {
|
|
987
|
+
valid: true,
|
|
988
|
+
celExpression,
|
|
989
|
+
dependencies,
|
|
990
|
+
sourceMap: sourceMapEntries,
|
|
991
|
+
errors: [],
|
|
992
|
+
warnings: [],
|
|
993
|
+
requiresConversion: true
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
return {
|
|
998
|
+
valid: false,
|
|
999
|
+
celExpression: null,
|
|
1000
|
+
dependencies: [],
|
|
1001
|
+
sourceMap: [],
|
|
1002
|
+
errors: [new ConversionError(`Failed to handle nullish coalescing: ${error instanceof Error ? error.message : String(error)}`, expression, 'nullish-coalescing')],
|
|
1003
|
+
warnings: [],
|
|
1004
|
+
requiresConversion: true
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Extract resource references from expression string
|
|
1010
|
+
*/
|
|
1011
|
+
extractResourceReferencesFromExpression(expression) {
|
|
1012
|
+
const refs = [];
|
|
1013
|
+
// Look for patterns like deployment.status.readyReplicas or service?.status?.loadBalancer
|
|
1014
|
+
const resourcePattern = /([a-zA-Z_][a-zA-Z0-9_]*)\??\.([a-zA-Z_][a-zA-Z0-9_]*(?:\??\.?[a-zA-Z_][a-zA-Z0-9_]*)*)/g;
|
|
1015
|
+
let match = resourcePattern.exec(expression);
|
|
1016
|
+
while (match !== null) {
|
|
1017
|
+
refs.push(match[0].replace(/\?/g, '')); // Remove optional chaining operators for reference tracking
|
|
1018
|
+
match = resourcePattern.exec(expression);
|
|
1019
|
+
}
|
|
1020
|
+
return refs;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Check if an expression is a resource reference
|
|
1024
|
+
*/
|
|
1025
|
+
isResourceReference(expression) {
|
|
1026
|
+
// Check for explicit resource/schema prefixes
|
|
1027
|
+
if (expression.includes('resources.') || expression.includes('schema.')) {
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
// Check if it starts with a known resource name (for direct references like deployment.status.field)
|
|
1031
|
+
const parts = expression.split('.');
|
|
1032
|
+
if (parts.length >= 2) {
|
|
1033
|
+
const resourceName = parts[0];
|
|
1034
|
+
// This is a heuristic - if it looks like a resource reference pattern
|
|
1035
|
+
return !!(resourceName && /^[a-zA-Z][a-zA-Z0-9-]*$/.test(resourceName) &&
|
|
1036
|
+
(parts[1] === 'status' || parts[1] === 'spec' || parts[1] === 'metadata'));
|
|
1037
|
+
}
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Setup type registry from analysis context
|
|
1042
|
+
*/
|
|
1043
|
+
setupTypeRegistry(context) {
|
|
1044
|
+
const registry = new TypeRegistry();
|
|
1045
|
+
// Register resource types
|
|
1046
|
+
for (const [resourceId, resource] of Object.entries(context.availableReferences)) {
|
|
1047
|
+
const resourceType = TypeSafetyUtils.fromEnhancedType(resource);
|
|
1048
|
+
registry.registerResourceType(resourceId, resourceType);
|
|
1049
|
+
}
|
|
1050
|
+
// Register basic types
|
|
1051
|
+
registry.registerType('string', { typeName: 'string', optional: false, nullable: false });
|
|
1052
|
+
registry.registerType('number', { typeName: 'number', optional: false, nullable: false });
|
|
1053
|
+
registry.registerType('boolean', { typeName: 'boolean', optional: false, nullable: false });
|
|
1054
|
+
registry.registerType('null', { typeName: 'null', optional: false, nullable: true });
|
|
1055
|
+
registry.registerType('undefined', { typeName: 'undefined', optional: true, nullable: false });
|
|
1056
|
+
return registry;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Validate expression compatibility with target context
|
|
1060
|
+
*/
|
|
1061
|
+
validateExpressionCompatibility(expression, context) {
|
|
1062
|
+
const registry = context.typeRegistry || this.setupTypeRegistry(context);
|
|
1063
|
+
const availableTypes = registry.getAvailableTypes();
|
|
1064
|
+
return this.typeValidator.validateExpression(expression, availableTypes, context.expectedType);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Infer the type of a CEL expression
|
|
1068
|
+
*/
|
|
1069
|
+
inferCelExpressionType(celExpression, context) {
|
|
1070
|
+
const inferenceContext = {
|
|
1071
|
+
availableResources: context.availableReferences,
|
|
1072
|
+
...(context.schemaProxy && { schemaProxy: context.schemaProxy }),
|
|
1073
|
+
factoryType: context.factoryType
|
|
1074
|
+
};
|
|
1075
|
+
return this.typeInferenceEngine.inferType(celExpression, inferenceContext);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Infer types for multiple CEL expressions
|
|
1079
|
+
*/
|
|
1080
|
+
inferCelExpressionTypes(celExpressions, context) {
|
|
1081
|
+
const inferenceContext = {
|
|
1082
|
+
availableResources: context.availableReferences,
|
|
1083
|
+
...(context.schemaProxy && { schemaProxy: context.schemaProxy }),
|
|
1084
|
+
factoryType: context.factoryType
|
|
1085
|
+
};
|
|
1086
|
+
return this.typeInferenceEngine.inferTypes(celExpressions, inferenceContext);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Validate type compatibility between JavaScript and CEL expressions
|
|
1090
|
+
*/
|
|
1091
|
+
validateJavaScriptToCelTypeCompatibility(jsExpression, celExpression, context) {
|
|
1092
|
+
// Get JavaScript expression type
|
|
1093
|
+
const jsValidation = this.validateExpressionCompatibility(jsExpression, context);
|
|
1094
|
+
if (!jsValidation.resultType) {
|
|
1095
|
+
return jsValidation;
|
|
1096
|
+
}
|
|
1097
|
+
// Get CEL expression type
|
|
1098
|
+
const celInference = this.inferCelExpressionType(celExpression, context);
|
|
1099
|
+
if (!celInference.success) {
|
|
1100
|
+
return {
|
|
1101
|
+
valid: false,
|
|
1102
|
+
errors: celInference.errors.map(e => ({
|
|
1103
|
+
message: e.message,
|
|
1104
|
+
expression: e.celExpression,
|
|
1105
|
+
expectedType: { typeName: 'unknown', optional: false, nullable: false },
|
|
1106
|
+
actualType: { typeName: 'unknown', optional: false, nullable: false }
|
|
1107
|
+
})),
|
|
1108
|
+
warnings: [],
|
|
1109
|
+
suggestions: []
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
// Validate compatibility
|
|
1113
|
+
return this.typeInferenceEngine.validateTypeCompatibility(jsValidation.resultType, celInference.resultType);
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Validate resource references in KubernetesRef objects
|
|
1117
|
+
*/
|
|
1118
|
+
validateResourceReferences(refs, availableResources, schemaProxy, validationContext) {
|
|
1119
|
+
return this.resourceValidator.validateKubernetesRefs(refs, availableResources, schemaProxy, validationContext);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Validate a single resource reference
|
|
1123
|
+
*/
|
|
1124
|
+
validateResourceReference(ref, availableResources, schemaProxy, validationContext) {
|
|
1125
|
+
return this.resourceValidator.validateKubernetesRef(ref, availableResources, schemaProxy, validationContext);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Validate a reference chain for type safety and circular dependencies
|
|
1129
|
+
*/
|
|
1130
|
+
validateReferenceChain(refs, availableResources, schemaProxy) {
|
|
1131
|
+
return this.resourceValidator.validateReferenceChain(refs, availableResources, schemaProxy);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Get comprehensive validation report for an expression
|
|
1135
|
+
*/
|
|
1136
|
+
getValidationReport(expression, context) {
|
|
1137
|
+
const conversionResult = this.analyzeExpression(expression, context);
|
|
1138
|
+
return {
|
|
1139
|
+
expression,
|
|
1140
|
+
conversionResult,
|
|
1141
|
+
...(conversionResult.typeValidation && { typeValidation: conversionResult.typeValidation }),
|
|
1142
|
+
...(conversionResult.resourceValidation && { resourceValidation: conversionResult.resourceValidation }),
|
|
1143
|
+
summary: this.createValidationSummary(conversionResult)
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Perform compile-time type checking
|
|
1148
|
+
*/
|
|
1149
|
+
performCompileTimeValidation(expression, context) {
|
|
1150
|
+
return this.compileTimeChecker.validateExpressionCompatibility(expression, context);
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Validate compile-time compatibility for multiple expressions
|
|
1154
|
+
*/
|
|
1155
|
+
performCompileTimeValidationBatch(expressions, context) {
|
|
1156
|
+
return this.compileTimeChecker.validateExpressionsCompatibility(expressions, context);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Validate KubernetesRef compile-time compatibility
|
|
1160
|
+
*/
|
|
1161
|
+
validateKubernetesRefCompileTimeCompatibility(ref, context) {
|
|
1162
|
+
if (!context.compileTimeContext) {
|
|
1163
|
+
throw new Error('Compile-time context required for KubernetesRef validation');
|
|
1164
|
+
}
|
|
1165
|
+
const usageContext = {
|
|
1166
|
+
availableResources: context.availableReferences,
|
|
1167
|
+
...(context.schemaProxy && { schemaProxy: context.schemaProxy }),
|
|
1168
|
+
usageType: 'property-access',
|
|
1169
|
+
...(context.expectedType && {
|
|
1170
|
+
expectedResultType: {
|
|
1171
|
+
typeName: context.expectedType.typeName,
|
|
1172
|
+
isUnion: false,
|
|
1173
|
+
isGeneric: false,
|
|
1174
|
+
optional: context.expectedType.optional,
|
|
1175
|
+
nullable: context.expectedType.nullable,
|
|
1176
|
+
undefinable: context.expectedType.optional
|
|
1177
|
+
}
|
|
1178
|
+
})
|
|
1179
|
+
};
|
|
1180
|
+
return this.compileTimeChecker.validateKubernetesRefCompatibility(ref, usageContext, context.compileTimeContext);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Get comprehensive compile-time validation report
|
|
1184
|
+
*/
|
|
1185
|
+
getCompileTimeValidationReport(expression, context) {
|
|
1186
|
+
if (!context.compileTimeContext) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
return this.performCompileTimeValidation(expression, context.compileTimeContext);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Create a validation summary from conversion results
|
|
1193
|
+
*/
|
|
1194
|
+
createValidationSummary(result) {
|
|
1195
|
+
const totalErrors = result.errors.length +
|
|
1196
|
+
(result.typeValidation?.errors.length || 0) +
|
|
1197
|
+
(result.resourceValidation?.reduce((sum, rv) => sum + rv.errors.length, 0) || 0) +
|
|
1198
|
+
(result.compileTimeValidation?.errors.length || 0);
|
|
1199
|
+
const totalWarnings = (result.typeValidation?.warnings.length || 0) +
|
|
1200
|
+
(result.resourceValidation?.reduce((sum, rv) => sum + rv.warnings.length, 0) || 0) +
|
|
1201
|
+
(result.compileTimeValidation?.warnings.length || 0);
|
|
1202
|
+
return {
|
|
1203
|
+
valid: result.celExpression !== null && totalErrors === 0,
|
|
1204
|
+
totalErrors,
|
|
1205
|
+
totalWarnings,
|
|
1206
|
+
requiresConversion: result.requiresConversion,
|
|
1207
|
+
hasTypeIssues: (result.typeValidation?.errors.length || 0) > 0,
|
|
1208
|
+
hasResourceIssues: (result.resourceValidation?.some(rv => !rv.valid)) || false,
|
|
1209
|
+
hasCompileTimeIssues: (result.compileTimeValidation && !result.compileTimeValidation.valid) || false,
|
|
1210
|
+
confidence: this.calculateOverallConfidence(result)
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Extract dependencies from JavaScript expression string and return them
|
|
1215
|
+
*/
|
|
1216
|
+
extractDependenciesFromExpressionString(expression, context) {
|
|
1217
|
+
const dependencies = [];
|
|
1218
|
+
// Look for direct resource references (deployment.status.field)
|
|
1219
|
+
if (context.availableReferences) {
|
|
1220
|
+
for (const [resourceKey, _resource] of Object.entries(context.availableReferences)) {
|
|
1221
|
+
const resourcePattern = new RegExp(`\\b${resourceKey}\\.([a-zA-Z0-9_.?\\[\\]]+)`, 'g');
|
|
1222
|
+
const matches = expression.match(resourcePattern);
|
|
1223
|
+
if (matches) {
|
|
1224
|
+
for (const match of matches) {
|
|
1225
|
+
const fieldPath = match.substring(resourceKey.length + 1)
|
|
1226
|
+
.replace(/\?\./g, '.') // Remove optional chaining
|
|
1227
|
+
.replace(/\?\[/g, '['); // Remove optional array access
|
|
1228
|
+
const ref = {
|
|
1229
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
1230
|
+
resourceId: resourceKey,
|
|
1231
|
+
fieldPath,
|
|
1232
|
+
_type: this.inferTypeFromFieldPath(fieldPath)
|
|
1233
|
+
};
|
|
1234
|
+
// Only add if not already present
|
|
1235
|
+
if (!dependencies.some(dep => dep.resourceId === resourceKey && dep.fieldPath === fieldPath)) {
|
|
1236
|
+
dependencies.push(ref);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Look for schema references (schema.spec.field)
|
|
1243
|
+
const schemaPattern = /\bschema\.[a-zA-Z0-9_.?[\]?]+/g;
|
|
1244
|
+
const schemaMatches = expression.match(schemaPattern);
|
|
1245
|
+
if (schemaMatches) {
|
|
1246
|
+
for (const match of schemaMatches) {
|
|
1247
|
+
const fieldPath = match.replace('schema.', '')
|
|
1248
|
+
.replace(/\?\./g, '.') // Remove optional chaining
|
|
1249
|
+
.replace(/\?\[/g, '['); // Remove optional array access
|
|
1250
|
+
const ref = {
|
|
1251
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
1252
|
+
resourceId: '__schema__',
|
|
1253
|
+
fieldPath,
|
|
1254
|
+
_type: this.inferTypeFromFieldPath(fieldPath)
|
|
1255
|
+
};
|
|
1256
|
+
// Only add if not already present
|
|
1257
|
+
if (!dependencies.some(dep => dep.resourceId === '__schema__' && dep.fieldPath === fieldPath)) {
|
|
1258
|
+
dependencies.push(ref);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return dependencies;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Extract dependencies from JavaScript expression
|
|
1266
|
+
*/
|
|
1267
|
+
extractDependenciesFromExpression(expression, context) {
|
|
1268
|
+
if (!context.dependencies) {
|
|
1269
|
+
context.dependencies = [];
|
|
1270
|
+
}
|
|
1271
|
+
// Look for resource references (resources.name.field)
|
|
1272
|
+
const resourceMatches = expression.match(/resources\.(\w+)\.([a-zA-Z0-9_.]+)/g);
|
|
1273
|
+
if (resourceMatches) {
|
|
1274
|
+
for (const match of resourceMatches) {
|
|
1275
|
+
const parts = match.split('.');
|
|
1276
|
+
if (parts.length >= 3) {
|
|
1277
|
+
const resourceId = parts[1];
|
|
1278
|
+
const fieldPath = parts.slice(2).join('.');
|
|
1279
|
+
const ref = {
|
|
1280
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
1281
|
+
resourceId,
|
|
1282
|
+
fieldPath,
|
|
1283
|
+
_type: 'unknown'
|
|
1284
|
+
};
|
|
1285
|
+
// Only add if not already present
|
|
1286
|
+
if (!context.dependencies.some(dep => dep.resourceId === resourceId && dep.fieldPath === fieldPath)) {
|
|
1287
|
+
context.dependencies.push(ref);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
// Look for schema references (schema.spec.field)
|
|
1293
|
+
const schemaMatches = expression.match(/schema\.([a-zA-Z0-9_.]+)/g);
|
|
1294
|
+
if (schemaMatches) {
|
|
1295
|
+
for (const match of schemaMatches) {
|
|
1296
|
+
const fieldPath = match.replace('schema.', '');
|
|
1297
|
+
const ref = {
|
|
1298
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
1299
|
+
resourceId: '__schema__',
|
|
1300
|
+
fieldPath,
|
|
1301
|
+
_type: 'unknown'
|
|
1302
|
+
};
|
|
1303
|
+
// Only add if not already present
|
|
1304
|
+
if (!context.dependencies.some(dep => dep.resourceId === '__schema__' && dep.fieldPath === fieldPath)) {
|
|
1305
|
+
context.dependencies.push(ref);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// Look for direct resource references with various patterns
|
|
1310
|
+
// This handles patterns like:
|
|
1311
|
+
// - "deployment.status.readyReplicas"
|
|
1312
|
+
// - "deployment.status['readyReplicas']"
|
|
1313
|
+
// - "deployment.status?.readyReplicas"
|
|
1314
|
+
// - "deployment.status.conditions[0].type"
|
|
1315
|
+
const directResourcePatterns = [
|
|
1316
|
+
// Standard dot notation: deployment.status.readyReplicas
|
|
1317
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\.(status|spec|metadata)\.([a-zA-Z0-9_.[\]]+)/g,
|
|
1318
|
+
// Computed property access: deployment.status["readyReplicas"]
|
|
1319
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\.(status|spec|metadata)\["([^"]+)"\]/g,
|
|
1320
|
+
// Computed property access with single quotes: deployment.status['readyReplicas']
|
|
1321
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\.(status|spec|metadata)\['([^']+)'\]/g,
|
|
1322
|
+
// Optional chaining: deployment.status?.readyReplicas
|
|
1323
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\.(status|spec|metadata)\?\?\.([a-zA-Z0-9_.[\]?]+)/g,
|
|
1324
|
+
// Mixed patterns: deployment.status.conditions[0].type
|
|
1325
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\.(status|spec|metadata)\.([a-zA-Z0-9_.[\]?]+)/g
|
|
1326
|
+
];
|
|
1327
|
+
for (const pattern of directResourcePatterns) {
|
|
1328
|
+
let match;
|
|
1329
|
+
pattern.lastIndex = 0; // Reset regex state
|
|
1330
|
+
match = pattern.exec(expression);
|
|
1331
|
+
while (match !== null) {
|
|
1332
|
+
const fullMatch = match[0];
|
|
1333
|
+
const resourceId = match[1];
|
|
1334
|
+
const baseField = match[2]; // status, spec, or metadata
|
|
1335
|
+
const remainingPath = match[3];
|
|
1336
|
+
let fieldPath = baseField;
|
|
1337
|
+
// Handle different patterns
|
|
1338
|
+
if (remainingPath) {
|
|
1339
|
+
// For computed property access patterns, the remainingPath is the property name
|
|
1340
|
+
if (pattern.source.includes('\\["') || pattern.source.includes("\\'")) {
|
|
1341
|
+
fieldPath = `${baseField}.${remainingPath}`;
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
fieldPath = `${baseField}.${remainingPath}`;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
// For computed property access, we need to extract the property name differently
|
|
1349
|
+
const computedMatch = fullMatch.match(/\.(status|spec|metadata)\["([^"]+)"\]/) ||
|
|
1350
|
+
fullMatch.match(/\.(status|spec|metadata)\['([^']+)'\]/);
|
|
1351
|
+
if (computedMatch) {
|
|
1352
|
+
fieldPath = `${computedMatch[1]}.${computedMatch[2]}`;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
// Clean up field path
|
|
1356
|
+
fieldPath = fieldPath?.replace(/\?\?/g, '').replace(/\?/g, '') || '';
|
|
1357
|
+
fieldPath = fieldPath.replace(/\["([^"]+)"\]/g, '.$1');
|
|
1358
|
+
fieldPath = fieldPath.replace(/\['([^']+)'\]/g, '.$1');
|
|
1359
|
+
fieldPath = fieldPath.replace(/\[(\d+)\]/g, '[$1]'); // Keep array indices
|
|
1360
|
+
// Check if this resource exists in available references or add it anyway
|
|
1361
|
+
const shouldAdd = !context.availableReferences ||
|
|
1362
|
+
(resourceId ? context.availableReferences[resourceId] : null) ||
|
|
1363
|
+
true; // Add all for now, let validation handle it later
|
|
1364
|
+
if (shouldAdd) {
|
|
1365
|
+
const ref = {
|
|
1366
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
1367
|
+
resourceId,
|
|
1368
|
+
fieldPath,
|
|
1369
|
+
_type: 'unknown'
|
|
1370
|
+
};
|
|
1371
|
+
// Only add if not already present
|
|
1372
|
+
if (!context.dependencies.some(dep => dep.resourceId === resourceId && dep.fieldPath === fieldPath)) {
|
|
1373
|
+
context.dependencies.push(ref);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
// Get next match
|
|
1377
|
+
match = pattern.exec(expression);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
// Look for template literal interpolations
|
|
1381
|
+
// This handles patterns like: `http://${service.status.loadBalancer.ingress[0].ip}`
|
|
1382
|
+
const templateLiteralMatches = expression.match(/\$\{([^}]+)\}/g);
|
|
1383
|
+
if (templateLiteralMatches) {
|
|
1384
|
+
for (const match of templateLiteralMatches) {
|
|
1385
|
+
const innerExpression = match.slice(2, -1); // Remove ${ and }
|
|
1386
|
+
// Recursively extract dependencies from the inner expression
|
|
1387
|
+
this.extractDependenciesFromExpression(innerExpression, context);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Calculate overall confidence score
|
|
1393
|
+
*/
|
|
1394
|
+
calculateOverallConfidence(result) {
|
|
1395
|
+
let confidence = 1.0;
|
|
1396
|
+
// Reduce confidence for errors
|
|
1397
|
+
if (result.errors.length > 0) {
|
|
1398
|
+
confidence *= 0.1;
|
|
1399
|
+
}
|
|
1400
|
+
// Reduce confidence for type validation issues
|
|
1401
|
+
if (result.typeValidation && !result.typeValidation.valid) {
|
|
1402
|
+
confidence *= 0.5;
|
|
1403
|
+
}
|
|
1404
|
+
// Reduce confidence for resource validation issues
|
|
1405
|
+
if (result.resourceValidation) {
|
|
1406
|
+
const invalidResources = result.resourceValidation.filter(rv => !rv.valid).length;
|
|
1407
|
+
if (invalidResources > 0) {
|
|
1408
|
+
confidence *= Math.max(0.1, 1 - (invalidResources * 0.3));
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// Reduce confidence for compile-time validation issues
|
|
1412
|
+
if (result.compileTimeValidation && !result.compileTimeValidation.valid) {
|
|
1413
|
+
confidence *= 0.3;
|
|
1414
|
+
}
|
|
1415
|
+
return Math.max(0, Math.min(1, confidence));
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Convert binary expressions (>, <, ==, !=, &&, ||) with KubernetesRef operand handling
|
|
1419
|
+
*/
|
|
1420
|
+
convertBinaryExpression(node, context) {
|
|
1421
|
+
// Convert operands with proper precedence handling
|
|
1422
|
+
const left = this.handleComplexExpression(node.left, context, node.operator);
|
|
1423
|
+
const right = this.handleComplexExpression(node.right, context, node.operator);
|
|
1424
|
+
// Map JavaScript operators to CEL operators
|
|
1425
|
+
const operator = this.mapOperatorToCel(node.operator);
|
|
1426
|
+
// Generate CEL expression with proper precedence
|
|
1427
|
+
const leftExpr = this.addParenthesesIfNeeded(left.expression, node.operator, true);
|
|
1428
|
+
const rightExpr = this.addParenthesesIfNeeded(right.expression, node.operator, false);
|
|
1429
|
+
const expression = `${leftExpr} ${operator} ${rightExpr}`;
|
|
1430
|
+
return {
|
|
1431
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1432
|
+
expression,
|
|
1433
|
+
_type: undefined
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Convert member expressions (object.property, object['property']) and array access (array[0], array[index])
|
|
1438
|
+
*/
|
|
1439
|
+
convertMemberExpression(node, context) {
|
|
1440
|
+
// Handle optional member expressions (obj?.prop)
|
|
1441
|
+
if (node.optional) {
|
|
1442
|
+
return this.convertOptionalMemberExpression(node, context);
|
|
1443
|
+
}
|
|
1444
|
+
// Handle computed member access (array[index] or object['key'])
|
|
1445
|
+
if (node.computed) {
|
|
1446
|
+
return this.convertArrayAccess(node, context);
|
|
1447
|
+
}
|
|
1448
|
+
// Check if the object is a complex expression (like a method call result)
|
|
1449
|
+
if (node.object.type === 'CallExpression' || node.object.type === 'MemberExpression' && this.isComplexExpression(node.object)) {
|
|
1450
|
+
// Convert the object expression first
|
|
1451
|
+
const objectExpr = this.convertASTNode(node.object, context);
|
|
1452
|
+
const propertyName = node.property.name;
|
|
1453
|
+
// Create a member access on the result of the complex expression
|
|
1454
|
+
const expression = `${objectExpr.expression}.${propertyName}`;
|
|
1455
|
+
return {
|
|
1456
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1457
|
+
expression,
|
|
1458
|
+
_type: undefined
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
// Try to extract the full member path for simple cases
|
|
1462
|
+
let path;
|
|
1463
|
+
try {
|
|
1464
|
+
path = this.extractMemberPath(node);
|
|
1465
|
+
}
|
|
1466
|
+
catch (_error) {
|
|
1467
|
+
// If path extraction fails, fall back to converting the object and property separately
|
|
1468
|
+
const objectExpr = this.convertASTNode(node.object, context);
|
|
1469
|
+
const propertyName = node.property.name;
|
|
1470
|
+
const expression = `${objectExpr.expression}.${propertyName}`;
|
|
1471
|
+
return {
|
|
1472
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1473
|
+
expression,
|
|
1474
|
+
_type: undefined
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
// Check if this is a resource reference
|
|
1478
|
+
if (context.availableReferences) {
|
|
1479
|
+
for (const [resourceKey, resource] of Object.entries(context.availableReferences)) {
|
|
1480
|
+
if (path.startsWith(`resources.${resourceKey}.`) || path.startsWith(`${resourceKey}.`) || path === resourceKey) {
|
|
1481
|
+
let fieldPath;
|
|
1482
|
+
if (path === resourceKey) {
|
|
1483
|
+
// Direct resource reference
|
|
1484
|
+
fieldPath = '';
|
|
1485
|
+
}
|
|
1486
|
+
else if (path.startsWith('resources.')) {
|
|
1487
|
+
fieldPath = path.substring(`resources.${resourceKey}.`.length);
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
fieldPath = path.substring(`${resourceKey}.`.length);
|
|
1491
|
+
}
|
|
1492
|
+
return this.getResourceFieldReference(resource, resourceKey, fieldPath, context);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// Handle schema references
|
|
1497
|
+
if (path.startsWith('schema.')) {
|
|
1498
|
+
return this.getSchemaFieldReference(path, context);
|
|
1499
|
+
}
|
|
1500
|
+
// Handle unknown resources - this should be an error in strict mode
|
|
1501
|
+
const parts = path.split('.');
|
|
1502
|
+
if (parts.length >= 2) {
|
|
1503
|
+
let resourceName;
|
|
1504
|
+
let fieldPath;
|
|
1505
|
+
// Check if this is a resources.* prefixed expression
|
|
1506
|
+
if (parts[0] === 'resources' && parts.length >= 3) {
|
|
1507
|
+
resourceName = parts[1] || ''; // The actual resource name after "resources."
|
|
1508
|
+
fieldPath = parts.slice(2).join('.'); // The field path after the resource name
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
resourceName = parts[0] || '';
|
|
1512
|
+
fieldPath = parts.slice(1).join('.');
|
|
1513
|
+
}
|
|
1514
|
+
// For strict validation contexts, check if resource should be available
|
|
1515
|
+
// For now, we'll be lenient and allow unknown resources with warnings
|
|
1516
|
+
// Create a placeholder KubernetesRef for the unknown resource
|
|
1517
|
+
const unknownRef = {
|
|
1518
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
1519
|
+
resourceId: resourceName,
|
|
1520
|
+
fieldPath: fieldPath,
|
|
1521
|
+
_type: this.inferTypeFromFieldPath(fieldPath)
|
|
1522
|
+
};
|
|
1523
|
+
// Add to dependencies
|
|
1524
|
+
if (context.dependencies) {
|
|
1525
|
+
context.dependencies.push(unknownRef);
|
|
1526
|
+
}
|
|
1527
|
+
// Generate CEL expression for unknown resource
|
|
1528
|
+
const expression = `resources.${resourceName}.${fieldPath}`;
|
|
1529
|
+
return {
|
|
1530
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1531
|
+
expression,
|
|
1532
|
+
_type: undefined
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
throw new Error(`Unable to resolve member expression: ${path}`);
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Convert conditional expressions (condition ? true : false)
|
|
1539
|
+
* Handles ternary operators with proper CEL syntax and KubernetesRef support
|
|
1540
|
+
*/
|
|
1541
|
+
convertConditionalExpression(node, context) {
|
|
1542
|
+
const test = this.handleComplexExpression(node.test, context, '?');
|
|
1543
|
+
const consequent = this.handleComplexExpression(node.consequent, context, '?');
|
|
1544
|
+
const alternate = this.handleComplexExpression(node.alternate, context, '?');
|
|
1545
|
+
// Ensure the test condition is properly formatted for CEL
|
|
1546
|
+
let testExpression = test.expression;
|
|
1547
|
+
// If the test is a resource reference or optional chaining, ensure it's properly evaluated as boolean
|
|
1548
|
+
if (this.isResourceReference(testExpression) || testExpression.includes('?')) {
|
|
1549
|
+
// For resource references, we need to check if they exist and are truthy
|
|
1550
|
+
testExpression = this.convertToBooleanTest(testExpression);
|
|
1551
|
+
}
|
|
1552
|
+
// Add parentheses to operands if needed for precedence
|
|
1553
|
+
const testExpr = this.addParenthesesIfNeeded(testExpression, '?', true);
|
|
1554
|
+
const consequentExpr = this.addParenthesesIfNeeded(consequent.expression, '?', false);
|
|
1555
|
+
const alternateExpr = this.addParenthesesIfNeeded(alternate.expression, '?', false);
|
|
1556
|
+
const expression = `${testExpr} ? ${consequentExpr} : ${alternateExpr}`;
|
|
1557
|
+
return {
|
|
1558
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1559
|
+
expression,
|
|
1560
|
+
_type: undefined
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Convert an expression to a proper boolean test for CEL conditionals
|
|
1565
|
+
*/
|
|
1566
|
+
convertToBooleanTest(expression) {
|
|
1567
|
+
// If it's already a comparison or boolean expression, use as-is
|
|
1568
|
+
if (this.isBooleanExpression(expression)) {
|
|
1569
|
+
return expression;
|
|
1570
|
+
}
|
|
1571
|
+
// For resource references and other values, check for truthiness
|
|
1572
|
+
// This handles JavaScript's truthy/falsy semantics in CEL
|
|
1573
|
+
return `${expression} != null && ${expression} != "" && ${expression} != false && ${expression} != 0`;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Check if an expression is already a boolean expression
|
|
1577
|
+
*/
|
|
1578
|
+
isBooleanExpression(expression) {
|
|
1579
|
+
// Check for comparison operators
|
|
1580
|
+
const comparisonOperators = ['==', '!=', '>', '<', '>=', '<=', '&&', '||'];
|
|
1581
|
+
return comparisonOperators.some(op => expression.includes(` ${op} `));
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Add parentheses to expression if needed for proper precedence
|
|
1585
|
+
*/
|
|
1586
|
+
addParenthesesIfNeeded(expression, parentOperator, isLeftOperand) {
|
|
1587
|
+
// If no parent operator, no parentheses needed
|
|
1588
|
+
if (!parentOperator) {
|
|
1589
|
+
return expression;
|
|
1590
|
+
}
|
|
1591
|
+
// Get the precedence of operators in the expression
|
|
1592
|
+
const expressionOperator = this.getMainOperator(expression);
|
|
1593
|
+
if (!expressionOperator) {
|
|
1594
|
+
return expression; // No operator found, likely a simple expression
|
|
1595
|
+
}
|
|
1596
|
+
const parentPrecedence = this.getOperatorPrecedence(parentOperator);
|
|
1597
|
+
const expressionPrecedence = this.getOperatorPrecedence(expressionOperator);
|
|
1598
|
+
// Add parentheses if expression has lower precedence than parent
|
|
1599
|
+
// or if it's a right operand with equal precedence (for left-associative operators)
|
|
1600
|
+
if (expressionPrecedence < parentPrecedence ||
|
|
1601
|
+
(expressionPrecedence === parentPrecedence && !isLeftOperand && this.isLeftAssociative(parentOperator))) {
|
|
1602
|
+
return `(${expression})`;
|
|
1603
|
+
}
|
|
1604
|
+
return expression;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Get the main operator in an expression (the one with lowest precedence)
|
|
1608
|
+
*/
|
|
1609
|
+
getMainOperator(expression) {
|
|
1610
|
+
// This is a simplified implementation - in a full parser, we'd need to handle
|
|
1611
|
+
// nested expressions properly. For now, we'll look for operators outside of parentheses.
|
|
1612
|
+
const operators = ['||', '&&', '==', '!=', '<=', '>=', '<', '>', '+', '-', '*', '/', '%'];
|
|
1613
|
+
let depth = 0;
|
|
1614
|
+
let mainOperator = null;
|
|
1615
|
+
let lowestPrecedence = Infinity;
|
|
1616
|
+
for (let i = 0; i < expression.length; i++) {
|
|
1617
|
+
const char = expression[i];
|
|
1618
|
+
if (char === '(') {
|
|
1619
|
+
depth++;
|
|
1620
|
+
}
|
|
1621
|
+
else if (char === ')') {
|
|
1622
|
+
depth--;
|
|
1623
|
+
}
|
|
1624
|
+
else if (depth === 0) {
|
|
1625
|
+
// Check for operators at the top level
|
|
1626
|
+
for (const op of operators) {
|
|
1627
|
+
if (expression.substring(i, i + op.length) === op) {
|
|
1628
|
+
const precedence = this.getOperatorPrecedence(op);
|
|
1629
|
+
if (precedence <= lowestPrecedence) {
|
|
1630
|
+
lowestPrecedence = precedence;
|
|
1631
|
+
mainOperator = op;
|
|
1632
|
+
}
|
|
1633
|
+
i += op.length - 1; // Skip the operator
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return mainOperator;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Get operator precedence (lower number = lower precedence)
|
|
1643
|
+
*/
|
|
1644
|
+
getOperatorPrecedence(operator) {
|
|
1645
|
+
const precedence = {
|
|
1646
|
+
'||': 1,
|
|
1647
|
+
'&&': 2,
|
|
1648
|
+
'==': 3,
|
|
1649
|
+
'!=': 3,
|
|
1650
|
+
'<': 4,
|
|
1651
|
+
'<=': 4,
|
|
1652
|
+
'>': 4,
|
|
1653
|
+
'>=': 4,
|
|
1654
|
+
'+': 5,
|
|
1655
|
+
'-': 5,
|
|
1656
|
+
'*': 6,
|
|
1657
|
+
'/': 6,
|
|
1658
|
+
'%': 6,
|
|
1659
|
+
'??': 1, // Same as ||
|
|
1660
|
+
'?': 0 // Ternary has lowest precedence
|
|
1661
|
+
};
|
|
1662
|
+
return precedence[operator] ?? 10; // Unknown operators get high precedence
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Check if an operator is left-associative
|
|
1666
|
+
*/
|
|
1667
|
+
isLeftAssociative(operator) {
|
|
1668
|
+
// Most operators are left-associative, ternary is right-associative
|
|
1669
|
+
return operator !== '?';
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Handle complex nested expressions with proper precedence
|
|
1673
|
+
*/
|
|
1674
|
+
handleComplexExpression(node, context, parentOperator) {
|
|
1675
|
+
const result = this.convertASTNode(node, context);
|
|
1676
|
+
// Add parentheses if needed for precedence
|
|
1677
|
+
const expressionWithParens = this.addParenthesesIfNeeded(result.expression, parentOperator);
|
|
1678
|
+
return {
|
|
1679
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1680
|
+
expression: expressionWithParens,
|
|
1681
|
+
_type: result._type
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Convert logical expressions (&&, ||, ??)
|
|
1686
|
+
* Handles logical OR fallback conversion (value || default) and nullish coalescing (value ?? default)
|
|
1687
|
+
*/
|
|
1688
|
+
convertLogicalExpression(node, context) {
|
|
1689
|
+
const left = this.convertASTNode(node.left, context);
|
|
1690
|
+
const right = this.convertASTNode(node.right, context);
|
|
1691
|
+
if (node.operator === '||') {
|
|
1692
|
+
return this.convertLogicalOrFallback(left, right, context);
|
|
1693
|
+
}
|
|
1694
|
+
if (node.operator === '&&') {
|
|
1695
|
+
return this.convertLogicalAnd(left, right, context);
|
|
1696
|
+
}
|
|
1697
|
+
if (node.operator === '??') {
|
|
1698
|
+
return this.convertNullishCoalescing(left, right, context);
|
|
1699
|
+
}
|
|
1700
|
+
// For other logical operators, use direct mapping
|
|
1701
|
+
const operator = this.mapOperatorToCel(node.operator);
|
|
1702
|
+
const expression = `${left.expression} ${operator} ${right.expression}`;
|
|
1703
|
+
return {
|
|
1704
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1705
|
+
expression,
|
|
1706
|
+
_type: undefined
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Convert logical OR fallback (value || default) to appropriate CEL conditionals
|
|
1711
|
+
*/
|
|
1712
|
+
convertLogicalOrFallback(left, right, _context) {
|
|
1713
|
+
// Add parentheses to operands if they contain lower precedence operators
|
|
1714
|
+
const leftExpr = this.addParenthesesIfNeeded(left.expression, '||', true);
|
|
1715
|
+
const rightExpr = this.addParenthesesIfNeeded(right.expression, '||', false);
|
|
1716
|
+
// For resource references and optional chaining, we can use a simpler null check
|
|
1717
|
+
if (this.isResourceReference(left.expression) || left.expression.includes('?')) {
|
|
1718
|
+
// For resource references, primarily check for null/undefined
|
|
1719
|
+
const expression = `${leftExpr} != null ? ${leftExpr} : ${rightExpr}`;
|
|
1720
|
+
return {
|
|
1721
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1722
|
+
expression,
|
|
1723
|
+
_type: undefined
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
// For general expressions, check for all falsy values
|
|
1727
|
+
// This handles JavaScript's truthy/falsy semantics in CEL
|
|
1728
|
+
const expression = `${leftExpr} != null && ${leftExpr} != "" && ${leftExpr} != false && ${leftExpr} != 0 ? ${leftExpr} : ${rightExpr}`;
|
|
1729
|
+
return {
|
|
1730
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1731
|
+
expression,
|
|
1732
|
+
_type: undefined
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Convert logical AND (value && other) to CEL conditional
|
|
1737
|
+
*/
|
|
1738
|
+
convertLogicalAnd(left, right, _context) {
|
|
1739
|
+
// Add parentheses to operands if they contain lower precedence operators
|
|
1740
|
+
const leftExpr = this.addParenthesesIfNeeded(left.expression, '&&', true);
|
|
1741
|
+
const rightExpr = this.addParenthesesIfNeeded(right.expression, '&&', false);
|
|
1742
|
+
// For resource references, primarily check for null/undefined
|
|
1743
|
+
if (this.isResourceReference(left.expression) || left.expression.includes('?')) {
|
|
1744
|
+
const expression = `${leftExpr} != null ? ${rightExpr} : ${leftExpr}`;
|
|
1745
|
+
return {
|
|
1746
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1747
|
+
expression,
|
|
1748
|
+
_type: undefined
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
// For general expressions, check for all truthy values
|
|
1752
|
+
const expression = `${leftExpr} != null && ${leftExpr} != "" && ${leftExpr} != false && ${leftExpr} != 0 ? ${rightExpr} : ${leftExpr}`;
|
|
1753
|
+
return {
|
|
1754
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1755
|
+
expression,
|
|
1756
|
+
_type: undefined
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Convert nullish coalescing (value ?? default) to CEL null-checking expressions
|
|
1761
|
+
* Only checks for null and undefined, not other falsy values like || does
|
|
1762
|
+
*/
|
|
1763
|
+
convertNullishCoalescing(left, right, _context) {
|
|
1764
|
+
// Add parentheses to operands if they contain lower precedence operators
|
|
1765
|
+
const leftExpr = this.addParenthesesIfNeeded(left.expression, '??', true);
|
|
1766
|
+
const rightExpr = this.addParenthesesIfNeeded(right.expression, '??', false);
|
|
1767
|
+
// Nullish coalescing only checks for null and undefined, not other falsy values
|
|
1768
|
+
// This is more precise than || which checks for all falsy values
|
|
1769
|
+
const expression = `${leftExpr} != null ? ${leftExpr} : ${rightExpr}`;
|
|
1770
|
+
return {
|
|
1771
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1772
|
+
expression,
|
|
1773
|
+
_type: undefined
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Preprocess modern JavaScript syntax for older esprima parser
|
|
1778
|
+
*/
|
|
1779
|
+
preprocessModernSyntax(expression) {
|
|
1780
|
+
const processed = expression;
|
|
1781
|
+
// Handle optional chaining (?.) - convert to special handling
|
|
1782
|
+
if (processed.includes('?.')) {
|
|
1783
|
+
// Mark for special handling but don't throw error
|
|
1784
|
+
// Keep as-is for now, let the parser handle it
|
|
1785
|
+
}
|
|
1786
|
+
// Handle nullish coalescing (??) - convert to special handling
|
|
1787
|
+
if (processed.includes('??')) {
|
|
1788
|
+
// Mark for special handling but don't throw error
|
|
1789
|
+
// Keep as-is for now, let the parser handle it
|
|
1790
|
+
}
|
|
1791
|
+
return processed;
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Convert optional chaining expressions (obj?.prop?.field) to Kro conditional CEL
|
|
1795
|
+
* Uses Kro's ? operator for null-safe property access
|
|
1796
|
+
*/
|
|
1797
|
+
convertOptionalChaining(node, context) {
|
|
1798
|
+
// ChainExpression wraps the actual optional expression
|
|
1799
|
+
const expression = node.expression;
|
|
1800
|
+
if (expression.type === 'MemberExpression' && expression.optional) {
|
|
1801
|
+
return this.convertOptionalMemberExpression(expression, context);
|
|
1802
|
+
}
|
|
1803
|
+
if (expression.type === 'CallExpression' && expression.optional) {
|
|
1804
|
+
return this.convertOptionalCallExpression(expression, context);
|
|
1805
|
+
}
|
|
1806
|
+
// If it's not actually optional, convert the inner expression normally
|
|
1807
|
+
return this.convertASTNode(expression, context);
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Convert optional member expressions (obj?.prop, obj?.prop?.field)
|
|
1811
|
+
*/
|
|
1812
|
+
convertOptionalMemberExpression(node, context) {
|
|
1813
|
+
// Build the optional chain by recursively processing the object
|
|
1814
|
+
const objectExpr = this.convertASTNode(node.object, context);
|
|
1815
|
+
let propertyAccess;
|
|
1816
|
+
if (node.computed) {
|
|
1817
|
+
// Handle obj?.[key] syntax
|
|
1818
|
+
const property = this.convertASTNode(node.property, context);
|
|
1819
|
+
propertyAccess = `[${property.expression}]`;
|
|
1820
|
+
}
|
|
1821
|
+
else {
|
|
1822
|
+
// Handle obj?.prop syntax
|
|
1823
|
+
propertyAccess = `.${node.property.name}`;
|
|
1824
|
+
}
|
|
1825
|
+
// Use Kro's ? operator for null-safe access
|
|
1826
|
+
// The ? operator in Kro CEL provides null-safe property access
|
|
1827
|
+
const expression = `${objectExpr.expression}?${propertyAccess}`;
|
|
1828
|
+
return {
|
|
1829
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1830
|
+
expression,
|
|
1831
|
+
_type: undefined
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Convert optional call expressions (obj?.method?.())
|
|
1836
|
+
*/
|
|
1837
|
+
convertOptionalCallExpression(node, context) {
|
|
1838
|
+
// Convert the callee with optional chaining
|
|
1839
|
+
const callee = this.convertASTNode(node.callee, context);
|
|
1840
|
+
// Convert arguments
|
|
1841
|
+
const args = node.arguments.map((arg) => this.convertASTNode(arg, context).expression).join(', ');
|
|
1842
|
+
// Use Kro's ? operator for null-safe method calls
|
|
1843
|
+
const expression = `${callee.expression}?(${args})`;
|
|
1844
|
+
return {
|
|
1845
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1846
|
+
expression,
|
|
1847
|
+
_type: undefined
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Convert template literals with KubernetesRef interpolation
|
|
1852
|
+
* Handles expressions like `http://${database.status.podIP}:5432/db`
|
|
1853
|
+
*/
|
|
1854
|
+
convertTemplateLiteral(node, context) {
|
|
1855
|
+
let result = '';
|
|
1856
|
+
const _dependencies = [];
|
|
1857
|
+
// Process each part of the template literal
|
|
1858
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
1859
|
+
// Add the literal string part
|
|
1860
|
+
const literalPart = node.quasis[i].value.cooked;
|
|
1861
|
+
result += literalPart;
|
|
1862
|
+
// Add the interpolated expression if it exists
|
|
1863
|
+
if (i < node.expressions.length) {
|
|
1864
|
+
const expr = this.convertASTNode(node.expressions[i], context);
|
|
1865
|
+
// For template literals, we need to wrap expressions in ${}
|
|
1866
|
+
result += `\${${expr.expression}}`;
|
|
1867
|
+
// Track dependencies from the interpolated expression
|
|
1868
|
+
// Note: We'd need to extract dependencies from the expression
|
|
1869
|
+
// For now, we'll handle this in a future enhancement
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1874
|
+
expression: result,
|
|
1875
|
+
_type: 'string' // Template literals always produce strings
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Convert literal values (strings, numbers, booleans - no KubernetesRef objects)
|
|
1880
|
+
* This preserves literal values exactly as they are, without any KubernetesRef processing
|
|
1881
|
+
*/
|
|
1882
|
+
convertLiteral(node, _context) {
|
|
1883
|
+
let literalValue;
|
|
1884
|
+
if (typeof node.value === 'string') {
|
|
1885
|
+
// Preserve string literals with proper quoting for CEL
|
|
1886
|
+
literalValue = `"${node.value.replace(/"/g, '\\"')}"`;
|
|
1887
|
+
}
|
|
1888
|
+
else if (typeof node.value === 'number') {
|
|
1889
|
+
// Preserve numeric literals as-is
|
|
1890
|
+
literalValue = String(node.value);
|
|
1891
|
+
}
|
|
1892
|
+
else if (typeof node.value === 'boolean') {
|
|
1893
|
+
// Preserve boolean literals as-is
|
|
1894
|
+
literalValue = String(node.value);
|
|
1895
|
+
}
|
|
1896
|
+
else if (node.value === null) {
|
|
1897
|
+
// Preserve null literals
|
|
1898
|
+
literalValue = 'null';
|
|
1899
|
+
}
|
|
1900
|
+
else if (node.value === undefined) {
|
|
1901
|
+
// Handle undefined (though this shouldn't appear in valid JS literals)
|
|
1902
|
+
literalValue = 'null';
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
// For any other literal types, convert to string
|
|
1906
|
+
literalValue = `"${String(node.value).replace(/"/g, '\\"')}"`;
|
|
1907
|
+
}
|
|
1908
|
+
return {
|
|
1909
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1910
|
+
expression: literalValue,
|
|
1911
|
+
_type: typeof node.value
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Convert call expressions (method calls and global functions)
|
|
1916
|
+
*/
|
|
1917
|
+
convertCallExpression(node, context) {
|
|
1918
|
+
// Handle global functions and Math methods
|
|
1919
|
+
if (node.callee.type === 'Identifier') {
|
|
1920
|
+
const functionName = node.callee.name;
|
|
1921
|
+
return this.convertGlobalFunction(functionName, node.arguments, context);
|
|
1922
|
+
}
|
|
1923
|
+
// Handle Math.* functions
|
|
1924
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
1925
|
+
node.callee.object.type === 'Identifier' &&
|
|
1926
|
+
node.callee.object.name === 'Math') {
|
|
1927
|
+
const mathMethod = node.callee.property.name;
|
|
1928
|
+
return this.convertMathFunction(mathMethod, node.arguments, context);
|
|
1929
|
+
}
|
|
1930
|
+
// Handle common JavaScript methods that can be converted to CEL
|
|
1931
|
+
if (node.callee.type === 'MemberExpression') {
|
|
1932
|
+
const object = this.convertASTNode(node.callee.object, context);
|
|
1933
|
+
const methodName = node.callee.property.name;
|
|
1934
|
+
switch (methodName) {
|
|
1935
|
+
case 'find':
|
|
1936
|
+
return this.convertArrayFind(object, node.arguments, context);
|
|
1937
|
+
case 'filter':
|
|
1938
|
+
return this.convertArrayFilter(object, node.arguments, context);
|
|
1939
|
+
case 'map':
|
|
1940
|
+
return this.convertArrayMap(object, node.arguments, context);
|
|
1941
|
+
case 'includes':
|
|
1942
|
+
return this.convertStringIncludes(object, node.arguments, context);
|
|
1943
|
+
case 'some':
|
|
1944
|
+
return this.convertArraySome(object, node.arguments, context);
|
|
1945
|
+
case 'every':
|
|
1946
|
+
return this.convertArrayEvery(object, node.arguments, context);
|
|
1947
|
+
case 'startsWith':
|
|
1948
|
+
return this.convertStringStartsWith(object, node.arguments, context);
|
|
1949
|
+
case 'endsWith':
|
|
1950
|
+
return this.convertStringEndsWith(object, node.arguments, context);
|
|
1951
|
+
case 'toLowerCase':
|
|
1952
|
+
return this.convertStringToLowerCase(object, node.arguments, context);
|
|
1953
|
+
case 'toUpperCase':
|
|
1954
|
+
return this.convertStringToUpperCase(object, node.arguments, context);
|
|
1955
|
+
case 'trim':
|
|
1956
|
+
return this.convertStringTrim(object, node.arguments, context);
|
|
1957
|
+
case 'substring':
|
|
1958
|
+
return this.convertStringSubstring(object, node.arguments, context);
|
|
1959
|
+
case 'slice':
|
|
1960
|
+
return this.convertStringSlice(object, node.arguments, context);
|
|
1961
|
+
case 'split':
|
|
1962
|
+
return this.convertStringSplit(object, node.arguments, context);
|
|
1963
|
+
case 'join':
|
|
1964
|
+
return this.convertArrayJoin(object, node.arguments, context);
|
|
1965
|
+
case 'flatMap':
|
|
1966
|
+
return this.convertArrayFlatMap(object, node.arguments, context);
|
|
1967
|
+
case 'length':
|
|
1968
|
+
return this.convertLengthProperty(object, context);
|
|
1969
|
+
case 'padStart':
|
|
1970
|
+
return this.convertStringPadStart(object, node.arguments, context);
|
|
1971
|
+
case 'padEnd':
|
|
1972
|
+
return this.convertStringPadEnd(object, node.arguments, context);
|
|
1973
|
+
case 'repeat':
|
|
1974
|
+
return this.convertStringRepeat(object, node.arguments, context);
|
|
1975
|
+
case 'replace':
|
|
1976
|
+
return this.convertStringReplace(object, node.arguments, context);
|
|
1977
|
+
case 'indexOf':
|
|
1978
|
+
return this.convertStringIndexOf(object, node.arguments, context);
|
|
1979
|
+
case 'lastIndexOf':
|
|
1980
|
+
return this.convertStringLastIndexOf(object, node.arguments, context);
|
|
1981
|
+
default:
|
|
1982
|
+
throw new Error(`Unsupported method call: ${methodName}`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
throw new Error(`Unsupported call expression`);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Convert global functions like Number(), String(), Boolean()
|
|
1989
|
+
*/
|
|
1990
|
+
convertGlobalFunction(functionName, args, context) {
|
|
1991
|
+
const convertedArgs = args.map(arg => this.convertASTNode(arg, context));
|
|
1992
|
+
switch (functionName) {
|
|
1993
|
+
case 'Number':
|
|
1994
|
+
if (args.length === 1) {
|
|
1995
|
+
return {
|
|
1996
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
1997
|
+
expression: `double(${convertedArgs[0]?.expression || 'null'})`,
|
|
1998
|
+
_type: 'number'
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
break;
|
|
2002
|
+
case 'String':
|
|
2003
|
+
if (args.length === 1) {
|
|
2004
|
+
return {
|
|
2005
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2006
|
+
expression: `string(${convertedArgs[0]?.expression || 'null'})`,
|
|
2007
|
+
_type: 'string'
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
break;
|
|
2011
|
+
case 'Boolean':
|
|
2012
|
+
if (args.length === 1) {
|
|
2013
|
+
return {
|
|
2014
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2015
|
+
expression: `bool(${convertedArgs[0]?.expression || 'null'})`,
|
|
2016
|
+
_type: 'boolean'
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
break;
|
|
2020
|
+
case 'parseInt':
|
|
2021
|
+
if (args.length >= 1) {
|
|
2022
|
+
return {
|
|
2023
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2024
|
+
expression: `int(${convertedArgs[0]?.expression || 'null'})`,
|
|
2025
|
+
_type: 'number'
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
case 'parseFloat':
|
|
2030
|
+
if (args.length >= 1) {
|
|
2031
|
+
return {
|
|
2032
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2033
|
+
expression: `double(${convertedArgs[0]?.expression || 'null'})`,
|
|
2034
|
+
_type: 'number'
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
throw new Error(`Unsupported global function: ${functionName}`);
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Convert Math functions like Math.min(), Math.max(), Math.abs()
|
|
2043
|
+
*/
|
|
2044
|
+
convertMathFunction(mathMethod, args, context) {
|
|
2045
|
+
const convertedArgs = args.map(arg => this.convertASTNode(arg, context));
|
|
2046
|
+
switch (mathMethod) {
|
|
2047
|
+
case 'min':
|
|
2048
|
+
if (args.length >= 2) {
|
|
2049
|
+
// CEL doesn't have a direct min function, so we'll use a conditional approach
|
|
2050
|
+
// For now, we'll use a simple approach for 2 arguments
|
|
2051
|
+
if (args.length === 2) {
|
|
2052
|
+
return {
|
|
2053
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2054
|
+
expression: `${convertedArgs[0]?.expression || 'null'} < ${convertedArgs[1]?.expression || 'null'} ? ${convertedArgs[0]?.expression || 'null'} : ${convertedArgs[1]?.expression || 'null'}`,
|
|
2055
|
+
_type: 'number'
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
// For more than 2 arguments, we'll create a nested conditional
|
|
2060
|
+
let expression = convertedArgs[0]?.expression || 'null';
|
|
2061
|
+
for (let i = 1; i < convertedArgs.length; i++) {
|
|
2062
|
+
expression = `${expression} < ${convertedArgs[i]?.expression || 'null'} ? ${expression} : ${convertedArgs[i]?.expression || 'null'}`;
|
|
2063
|
+
}
|
|
2064
|
+
return {
|
|
2065
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2066
|
+
expression,
|
|
2067
|
+
_type: 'number'
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
break;
|
|
2072
|
+
case 'max':
|
|
2073
|
+
if (args.length >= 2) {
|
|
2074
|
+
if (args.length === 2) {
|
|
2075
|
+
return {
|
|
2076
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2077
|
+
expression: `${convertedArgs[0]?.expression || 'null'} > ${convertedArgs[1]?.expression || 'null'} ? ${convertedArgs[0]?.expression || 'null'} : ${convertedArgs[1]?.expression || 'null'}`,
|
|
2078
|
+
_type: 'number'
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
else {
|
|
2082
|
+
let expression = convertedArgs[0]?.expression || 'null';
|
|
2083
|
+
for (let i = 1; i < convertedArgs.length; i++) {
|
|
2084
|
+
expression = `${expression} > ${convertedArgs[i]?.expression || 'null'} ? ${expression} : ${convertedArgs[i]?.expression || 'null'}`;
|
|
2085
|
+
}
|
|
2086
|
+
return {
|
|
2087
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2088
|
+
expression,
|
|
2089
|
+
_type: 'number'
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
break;
|
|
2094
|
+
case 'abs':
|
|
2095
|
+
if (args.length === 1) {
|
|
2096
|
+
return {
|
|
2097
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2098
|
+
expression: `${convertedArgs[0]?.expression || 'null'} < 0 ? -${convertedArgs[0]?.expression || 'null'} : ${convertedArgs[0]?.expression || 'null'}`,
|
|
2099
|
+
_type: 'number'
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
break;
|
|
2103
|
+
case 'floor':
|
|
2104
|
+
if (args.length === 1) {
|
|
2105
|
+
return {
|
|
2106
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2107
|
+
expression: `int(${convertedArgs[0]?.expression || 'null'})`,
|
|
2108
|
+
_type: 'number'
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
break;
|
|
2112
|
+
case 'ceil':
|
|
2113
|
+
if (args.length === 1) {
|
|
2114
|
+
return {
|
|
2115
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2116
|
+
expression: `int(${convertedArgs[0]?.expression || 'null'} + 0.999999)`,
|
|
2117
|
+
_type: 'number'
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
break;
|
|
2121
|
+
case 'round':
|
|
2122
|
+
if (args.length === 1) {
|
|
2123
|
+
return {
|
|
2124
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2125
|
+
expression: `int(${convertedArgs[0]?.expression || 'null'} + 0.5)`,
|
|
2126
|
+
_type: 'number'
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
throw new Error(`Unsupported Math function: ${mathMethod}`);
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Convert unary expressions like !x, +x, -x, !!x
|
|
2135
|
+
*/
|
|
2136
|
+
convertUnaryExpression(node, context) {
|
|
2137
|
+
const operand = this.convertASTNode(node.argument, context);
|
|
2138
|
+
switch (node.operator) {
|
|
2139
|
+
case '!':
|
|
2140
|
+
return {
|
|
2141
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2142
|
+
expression: `!${operand.expression}`,
|
|
2143
|
+
_type: 'boolean'
|
|
2144
|
+
};
|
|
2145
|
+
case '+':
|
|
2146
|
+
return {
|
|
2147
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2148
|
+
expression: `double(${operand.expression})`,
|
|
2149
|
+
_type: 'number'
|
|
2150
|
+
};
|
|
2151
|
+
case '-':
|
|
2152
|
+
return {
|
|
2153
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2154
|
+
expression: `-${operand.expression}`,
|
|
2155
|
+
_type: 'number'
|
|
2156
|
+
};
|
|
2157
|
+
case 'typeof':
|
|
2158
|
+
return {
|
|
2159
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2160
|
+
expression: `type(${operand.expression})`,
|
|
2161
|
+
_type: 'string'
|
|
2162
|
+
};
|
|
2163
|
+
default:
|
|
2164
|
+
throw new Error(`Unsupported unary operator: ${node.operator}`);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Convert array expressions
|
|
2169
|
+
*/
|
|
2170
|
+
convertArrayExpression(node, context) {
|
|
2171
|
+
const elements = node.elements.map((element) => {
|
|
2172
|
+
if (element === null)
|
|
2173
|
+
return 'null';
|
|
2174
|
+
return this.convertASTNode(element, context).expression;
|
|
2175
|
+
});
|
|
2176
|
+
const expression = `[${elements.join(', ')}]`;
|
|
2177
|
+
return {
|
|
2178
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2179
|
+
expression,
|
|
2180
|
+
_type: undefined
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Convert identifier expressions
|
|
2185
|
+
*/
|
|
2186
|
+
convertIdentifier(node, context) {
|
|
2187
|
+
// For identifiers, we need to check if they refer to available references
|
|
2188
|
+
const name = node.name;
|
|
2189
|
+
// Check if this is a resource reference
|
|
2190
|
+
if (context.availableReferences?.[name]) {
|
|
2191
|
+
// This is a direct resource reference
|
|
2192
|
+
return {
|
|
2193
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2194
|
+
expression: `resources.${name}`,
|
|
2195
|
+
_type: undefined
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
// Check if this is a schema reference
|
|
2199
|
+
if (name === 'schema') {
|
|
2200
|
+
return {
|
|
2201
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2202
|
+
expression: 'schema',
|
|
2203
|
+
_type: undefined
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
// For other identifiers, return as-is (might be local variables in complex expressions)
|
|
2207
|
+
return {
|
|
2208
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2209
|
+
expression: name,
|
|
2210
|
+
_type: undefined
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Map JavaScript operators to CEL operators
|
|
2215
|
+
*/
|
|
2216
|
+
mapOperatorToCel(operator) {
|
|
2217
|
+
const mapping = {
|
|
2218
|
+
'===': '==',
|
|
2219
|
+
'!==': '!=',
|
|
2220
|
+
'&&': '&&',
|
|
2221
|
+
'||': '||',
|
|
2222
|
+
'>': '>',
|
|
2223
|
+
'<': '<',
|
|
2224
|
+
'>=': '>=',
|
|
2225
|
+
'<=': '<=',
|
|
2226
|
+
'==': '==',
|
|
2227
|
+
'!=': '!='
|
|
2228
|
+
};
|
|
2229
|
+
return mapping[operator] || operator;
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Extract member path from AST node
|
|
2233
|
+
*/
|
|
2234
|
+
extractMemberPath(node) {
|
|
2235
|
+
if (node.type === 'Identifier') {
|
|
2236
|
+
return node.name;
|
|
2237
|
+
}
|
|
2238
|
+
if (node.type === 'MemberExpression') {
|
|
2239
|
+
const object = this.extractMemberPath(node.object);
|
|
2240
|
+
if (node.computed) {
|
|
2241
|
+
// For computed access like array[0] or object['key'], we need special handling
|
|
2242
|
+
// This is used for path extraction, so we'll represent it differently
|
|
2243
|
+
const property = this.getSourceText(node.property);
|
|
2244
|
+
const optionalMarker = node.optional ? '?.' : '';
|
|
2245
|
+
return `${object}${optionalMarker}[${property}]`;
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
// For regular property access like object.property or object?.property
|
|
2249
|
+
const property = node.property.name;
|
|
2250
|
+
const optionalMarker = node.optional ? '?.' : '.';
|
|
2251
|
+
return `${object}${optionalMarker}${property}`;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
if (node.type === 'ChainExpression') {
|
|
2255
|
+
// Handle ChainExpression wrapper for optional chaining
|
|
2256
|
+
return this.extractMemberPath(node.expression);
|
|
2257
|
+
}
|
|
2258
|
+
throw new Error(`Cannot extract path from node type: ${node.type}`);
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Check if a node represents a complex expression that can't be handled as a simple path
|
|
2262
|
+
*/
|
|
2263
|
+
isComplexExpression(node) {
|
|
2264
|
+
if (node.type === 'CallExpression') {
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
if (node.type === 'MemberExpression') {
|
|
2268
|
+
// Recursively check if the object is complex
|
|
2269
|
+
return this.isComplexExpression(node.object);
|
|
2270
|
+
}
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Get source text from AST node (placeholder implementation)
|
|
2275
|
+
*/
|
|
2276
|
+
getSourceText(node) {
|
|
2277
|
+
// For now, return a placeholder - this would need access to original source
|
|
2278
|
+
if (node.type === 'Literal') {
|
|
2279
|
+
return String(node.value);
|
|
2280
|
+
}
|
|
2281
|
+
return '<expression>';
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Generate CEL expression for resource field reference
|
|
2285
|
+
*/
|
|
2286
|
+
getResourceFieldReference(_resource, resourceKey, fieldPath, context) {
|
|
2287
|
+
// Generate CEL expression for resource field reference using the correct format
|
|
2288
|
+
// This should match the format used by getInnerCelPath
|
|
2289
|
+
const expression = `${resourceKey}.${fieldPath}`;
|
|
2290
|
+
// Create a KubernetesRef object and add it to dependencies
|
|
2291
|
+
const ref = {
|
|
2292
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
2293
|
+
resourceId: resourceKey,
|
|
2294
|
+
fieldPath,
|
|
2295
|
+
_type: this.inferTypeFromFieldPath(fieldPath)
|
|
2296
|
+
};
|
|
2297
|
+
if (!context.dependencies) {
|
|
2298
|
+
context.dependencies = [];
|
|
2299
|
+
}
|
|
2300
|
+
context.dependencies.push(ref);
|
|
2301
|
+
return {
|
|
2302
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2303
|
+
expression,
|
|
2304
|
+
_type: undefined
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Generate CEL expression for schema field reference
|
|
2309
|
+
*/
|
|
2310
|
+
getSchemaFieldReference(path, context) {
|
|
2311
|
+
// Create a KubernetesRef object for schema reference and add it to dependencies
|
|
2312
|
+
const fieldPath = path.substring('schema.'.length);
|
|
2313
|
+
const ref = {
|
|
2314
|
+
[KUBERNETES_REF_BRAND]: true,
|
|
2315
|
+
resourceId: '__schema__',
|
|
2316
|
+
fieldPath,
|
|
2317
|
+
_type: this.inferTypeFromFieldPath(fieldPath)
|
|
2318
|
+
};
|
|
2319
|
+
if (!context.dependencies) {
|
|
2320
|
+
context.dependencies = [];
|
|
2321
|
+
}
|
|
2322
|
+
context.dependencies.push(ref);
|
|
2323
|
+
// Generate CEL expression for schema field reference
|
|
2324
|
+
return {
|
|
2325
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2326
|
+
expression: path,
|
|
2327
|
+
_type: undefined
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Convert array.find() method calls
|
|
2332
|
+
*/
|
|
2333
|
+
convertArrayFind(object, args, context) {
|
|
2334
|
+
if (args.length !== 1) {
|
|
2335
|
+
throw new Error('Array.find() requires exactly one argument');
|
|
2336
|
+
}
|
|
2337
|
+
// For simple property comparisons like c => c.type === "Available", we can convert to CEL
|
|
2338
|
+
const arg = args[0];
|
|
2339
|
+
if (arg.type === 'ArrowFunctionExpression' && arg.body.type === 'BinaryExpression') {
|
|
2340
|
+
const param = arg.params[0].name;
|
|
2341
|
+
const binaryExpr = arg.body;
|
|
2342
|
+
// Handle the left side (should be a member expression like c.type)
|
|
2343
|
+
let leftExpr;
|
|
2344
|
+
if (binaryExpr.left.type === 'MemberExpression' && binaryExpr.left.object.name === param) {
|
|
2345
|
+
// Simple case: c.type
|
|
2346
|
+
leftExpr = `${param}.${binaryExpr.left.property.name}`;
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
// More complex case - try to convert but replace parameter references
|
|
2350
|
+
try {
|
|
2351
|
+
const leftResult = this.convertASTNode(binaryExpr.left, context);
|
|
2352
|
+
leftExpr = leftResult.expression.replace(new RegExp(`\\b${param}\\b`, 'g'), param);
|
|
2353
|
+
}
|
|
2354
|
+
catch {
|
|
2355
|
+
leftExpr = `${param}.property`;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
// Handle the right side (usually a literal)
|
|
2359
|
+
let rightExpr;
|
|
2360
|
+
try {
|
|
2361
|
+
const rightResult = this.convertASTNode(binaryExpr.right, context);
|
|
2362
|
+
rightExpr = rightResult.expression;
|
|
2363
|
+
}
|
|
2364
|
+
catch {
|
|
2365
|
+
rightExpr = 'value';
|
|
2366
|
+
}
|
|
2367
|
+
const operator = this.convertBinaryOperator(binaryExpr.operator);
|
|
2368
|
+
const expression = `${object.expression}.filter(${param}, ${leftExpr} ${operator} ${rightExpr})[0]`;
|
|
2369
|
+
return {
|
|
2370
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2371
|
+
expression,
|
|
2372
|
+
_type: undefined
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
// For now, create a placeholder for complex find operations
|
|
2376
|
+
const expression = `${object.expression}.filter(/* TODO: convert find predicate */)[0]`;
|
|
2377
|
+
return {
|
|
2378
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2379
|
+
expression,
|
|
2380
|
+
_type: undefined
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Convert array.filter() method calls
|
|
2385
|
+
*/
|
|
2386
|
+
convertArrayFilter(object, args, context) {
|
|
2387
|
+
if (args.length !== 1) {
|
|
2388
|
+
throw new Error('Array.filter() requires exactly one argument');
|
|
2389
|
+
}
|
|
2390
|
+
// For simple property access like i => i.ip, we can convert to CEL
|
|
2391
|
+
const arg = args[0];
|
|
2392
|
+
if (arg.type === 'ArrowFunctionExpression') {
|
|
2393
|
+
const param = arg.params[0].name;
|
|
2394
|
+
if (arg.body.type === 'MemberExpression') {
|
|
2395
|
+
// Simple property access: i => i.ip
|
|
2396
|
+
const property = arg.body.property.name;
|
|
2397
|
+
const expression = `${object.expression}.filter(${param}, has(${param}.${property}) && ${param}.${property} != null)`;
|
|
2398
|
+
return {
|
|
2399
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2400
|
+
expression,
|
|
2401
|
+
_type: undefined
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
else if (arg.body.type === 'BinaryExpression') {
|
|
2405
|
+
// Binary comparison: i => i.type === "Available"
|
|
2406
|
+
const left = this.convertASTNode(arg.body.left, context);
|
|
2407
|
+
const operator = this.convertBinaryOperator(arg.body.operator);
|
|
2408
|
+
const right = this.convertASTNode(arg.body.right, context);
|
|
2409
|
+
// Replace parameter references with the iteration variable
|
|
2410
|
+
const leftExpr = left.expression.replace(new RegExp(`\\b${param}\\b`, 'g'), param);
|
|
2411
|
+
const rightExpr = right.expression;
|
|
2412
|
+
const expression = `${object.expression}.filter(${param}, ${leftExpr} ${operator} ${rightExpr})`;
|
|
2413
|
+
return {
|
|
2414
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2415
|
+
expression,
|
|
2416
|
+
_type: undefined
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
// For now, create a placeholder for complex filter operations
|
|
2421
|
+
const expression = `${object.expression}.filter(/* TODO: convert filter predicate */)`;
|
|
2422
|
+
return {
|
|
2423
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2424
|
+
expression,
|
|
2425
|
+
_type: undefined
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Convert string.includes() method calls
|
|
2430
|
+
*/
|
|
2431
|
+
convertStringIncludes(object, args, context) {
|
|
2432
|
+
if (args.length !== 1) {
|
|
2433
|
+
throw new Error('String.includes() requires exactly one argument');
|
|
2434
|
+
}
|
|
2435
|
+
const searchValue = this.convertASTNode(args[0], context);
|
|
2436
|
+
const expression = `${object.expression}.contains(${searchValue.expression})`;
|
|
2437
|
+
return {
|
|
2438
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2439
|
+
expression,
|
|
2440
|
+
_type: undefined
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* Convert array.map() method calls
|
|
2445
|
+
*/
|
|
2446
|
+
convertArrayMap(object, args, _context) {
|
|
2447
|
+
if (args.length !== 1) {
|
|
2448
|
+
throw new Error('Array.map() requires exactly one argument');
|
|
2449
|
+
}
|
|
2450
|
+
// For simple property access like c => c.name, we can convert to CEL
|
|
2451
|
+
const arg = args[0];
|
|
2452
|
+
if (arg.type === 'ArrowFunctionExpression' && arg.body.type === 'MemberExpression') {
|
|
2453
|
+
const param = arg.params[0].name;
|
|
2454
|
+
const property = arg.body.property.name;
|
|
2455
|
+
const expression = `${object.expression}.map(${param}, ${param}.${property})`;
|
|
2456
|
+
return {
|
|
2457
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2458
|
+
expression,
|
|
2459
|
+
_type: undefined
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
// For now, create a placeholder for complex map operations
|
|
2463
|
+
const expression = `${object.expression}.map(/* TODO: convert map predicate */)`;
|
|
2464
|
+
return {
|
|
2465
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2466
|
+
expression,
|
|
2467
|
+
_type: undefined
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Convert array.some() method calls
|
|
2472
|
+
*/
|
|
2473
|
+
convertArraySome(object, args, _context) {
|
|
2474
|
+
if (args.length !== 1) {
|
|
2475
|
+
throw new Error('Array.some() requires exactly one argument');
|
|
2476
|
+
}
|
|
2477
|
+
// For now, create a placeholder - full implementation would need lambda support
|
|
2478
|
+
const expression = `${object.expression}.exists(/* TODO: convert predicate */)`;
|
|
2479
|
+
return {
|
|
2480
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2481
|
+
expression,
|
|
2482
|
+
_type: undefined
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Convert array.every() method calls
|
|
2487
|
+
*/
|
|
2488
|
+
convertArrayEvery(object, args, _context) {
|
|
2489
|
+
if (args.length !== 1) {
|
|
2490
|
+
throw new Error('Array.every() requires exactly one argument');
|
|
2491
|
+
}
|
|
2492
|
+
// For now, create a placeholder - full implementation would need lambda support
|
|
2493
|
+
const expression = `${object.expression}.all(/* TODO: convert predicate */)`;
|
|
2494
|
+
return {
|
|
2495
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2496
|
+
expression,
|
|
2497
|
+
_type: undefined
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Convert string.startsWith() method calls
|
|
2502
|
+
*/
|
|
2503
|
+
convertStringStartsWith(object, args, context) {
|
|
2504
|
+
if (args.length !== 1) {
|
|
2505
|
+
throw new Error('String.startsWith() requires exactly one argument');
|
|
2506
|
+
}
|
|
2507
|
+
const searchValue = this.convertASTNode(args[0], context);
|
|
2508
|
+
const expression = `${object.expression}.startsWith(${searchValue.expression})`;
|
|
2509
|
+
return {
|
|
2510
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2511
|
+
expression,
|
|
2512
|
+
_type: undefined
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Convert string.endsWith() method calls
|
|
2517
|
+
*/
|
|
2518
|
+
convertStringEndsWith(object, args, context) {
|
|
2519
|
+
if (args.length !== 1) {
|
|
2520
|
+
throw new Error('String.endsWith() requires exactly one argument');
|
|
2521
|
+
}
|
|
2522
|
+
const searchValue = this.convertASTNode(args[0], context);
|
|
2523
|
+
const expression = `${object.expression}.endsWith(${searchValue.expression})`;
|
|
2524
|
+
return {
|
|
2525
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2526
|
+
expression,
|
|
2527
|
+
_type: undefined
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Convert string.toLowerCase() method calls
|
|
2532
|
+
*/
|
|
2533
|
+
convertStringToLowerCase(object, args, _context) {
|
|
2534
|
+
if (args.length !== 0) {
|
|
2535
|
+
throw new Error('String.toLowerCase() requires no arguments');
|
|
2536
|
+
}
|
|
2537
|
+
const expression = `${object.expression}.lowerAscii()`;
|
|
2538
|
+
return {
|
|
2539
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2540
|
+
expression,
|
|
2541
|
+
_type: undefined
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Convert string.toUpperCase() method calls
|
|
2546
|
+
*/
|
|
2547
|
+
convertStringToUpperCase(object, args, _context) {
|
|
2548
|
+
if (args.length !== 0) {
|
|
2549
|
+
throw new Error('String.toUpperCase() requires no arguments');
|
|
2550
|
+
}
|
|
2551
|
+
const expression = `${object.expression}.upperAscii()`;
|
|
2552
|
+
return {
|
|
2553
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2554
|
+
expression,
|
|
2555
|
+
_type: undefined
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Convert string.trim() method calls
|
|
2560
|
+
*/
|
|
2561
|
+
convertStringTrim(object, args, _context) {
|
|
2562
|
+
if (args.length !== 0) {
|
|
2563
|
+
throw new Error('String.trim() requires no arguments');
|
|
2564
|
+
}
|
|
2565
|
+
// CEL doesn't have a direct trim function, so we'll use a placeholder
|
|
2566
|
+
const expression = `${object.expression}.trim()`;
|
|
2567
|
+
return {
|
|
2568
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2569
|
+
expression,
|
|
2570
|
+
_type: undefined
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Convert string.substring() method calls
|
|
2575
|
+
*/
|
|
2576
|
+
convertStringSubstring(object, args, context) {
|
|
2577
|
+
if (args.length < 1 || args.length > 2) {
|
|
2578
|
+
throw new Error('String.substring() requires 1 or 2 arguments');
|
|
2579
|
+
}
|
|
2580
|
+
const startIndex = this.convertASTNode(args[0], context);
|
|
2581
|
+
if (args.length === 1) {
|
|
2582
|
+
const expression = `${object.expression}.substring(${startIndex.expression})`;
|
|
2583
|
+
return {
|
|
2584
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2585
|
+
expression,
|
|
2586
|
+
_type: undefined
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
else {
|
|
2590
|
+
const endIndex = this.convertASTNode(args[1], context);
|
|
2591
|
+
const expression = `${object.expression}.substring(${startIndex.expression}, ${endIndex.expression})`;
|
|
2592
|
+
return {
|
|
2593
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2594
|
+
expression,
|
|
2595
|
+
_type: undefined
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Convert string.slice() method calls
|
|
2601
|
+
*/
|
|
2602
|
+
convertStringSlice(object, args, context) {
|
|
2603
|
+
if (args.length < 1 || args.length > 2) {
|
|
2604
|
+
throw new Error('String.slice() requires 1 or 2 arguments');
|
|
2605
|
+
}
|
|
2606
|
+
const startIndex = this.convertASTNode(args[0], context);
|
|
2607
|
+
if (args.length === 1) {
|
|
2608
|
+
const expression = `${object.expression}.substring(${startIndex.expression})`;
|
|
2609
|
+
return {
|
|
2610
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2611
|
+
expression,
|
|
2612
|
+
_type: undefined
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
else {
|
|
2616
|
+
const endIndex = this.convertASTNode(args[1], context);
|
|
2617
|
+
const expression = `${object.expression}.substring(${startIndex.expression}, ${endIndex.expression})`;
|
|
2618
|
+
return {
|
|
2619
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2620
|
+
expression,
|
|
2621
|
+
_type: undefined
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Convert string.split() method calls
|
|
2627
|
+
*/
|
|
2628
|
+
convertStringSplit(object, args, context) {
|
|
2629
|
+
if (args.length !== 1) {
|
|
2630
|
+
throw new Error('String.split() requires exactly one argument');
|
|
2631
|
+
}
|
|
2632
|
+
const separator = this.convertASTNode(args[0], context);
|
|
2633
|
+
const expression = `${object.expression}.split(${separator.expression})`;
|
|
2634
|
+
return {
|
|
2635
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2636
|
+
expression,
|
|
2637
|
+
_type: undefined
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Convert array.join() method calls
|
|
2642
|
+
*/
|
|
2643
|
+
convertArrayJoin(object, args, context) {
|
|
2644
|
+
if (args.length !== 1) {
|
|
2645
|
+
throw new Error('Array.join() requires exactly one argument');
|
|
2646
|
+
}
|
|
2647
|
+
const separator = this.convertASTNode(args[0], context);
|
|
2648
|
+
const expression = `${object.expression}.join(${separator.expression})`;
|
|
2649
|
+
return {
|
|
2650
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2651
|
+
expression,
|
|
2652
|
+
_type: undefined
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Convert array.flatMap() method calls
|
|
2657
|
+
*/
|
|
2658
|
+
convertArrayFlatMap(object, args, _context) {
|
|
2659
|
+
if (args.length !== 1) {
|
|
2660
|
+
throw new Error('Array.flatMap() requires exactly one argument');
|
|
2661
|
+
}
|
|
2662
|
+
const arg = args[0];
|
|
2663
|
+
// Handle arrow function: arr.flatMap(x => x.items)
|
|
2664
|
+
if (arg.type === 'ArrowFunctionExpression') {
|
|
2665
|
+
const param = arg.params[0].name;
|
|
2666
|
+
if (arg.body.type === 'MemberExpression') {
|
|
2667
|
+
// Simple property access: x => x.items
|
|
2668
|
+
const property = arg.body.property.name;
|
|
2669
|
+
const expression = `${object.expression}.map(${param}, ${param}.${property}).flatten()`;
|
|
2670
|
+
return {
|
|
2671
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2672
|
+
expression,
|
|
2673
|
+
_type: undefined
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
throw new Error('Unsupported flatMap expression');
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Convert .length property access
|
|
2681
|
+
*/
|
|
2682
|
+
convertLengthProperty(object, _context) {
|
|
2683
|
+
const expression = `size(${object.expression})`;
|
|
2684
|
+
return {
|
|
2685
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2686
|
+
expression,
|
|
2687
|
+
_type: undefined
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Convert string.padStart() method calls
|
|
2692
|
+
*/
|
|
2693
|
+
convertStringPadStart(object, args, context) {
|
|
2694
|
+
if (args.length < 1 || args.length > 2) {
|
|
2695
|
+
throw new Error('String.padStart() requires 1 or 2 arguments');
|
|
2696
|
+
}
|
|
2697
|
+
const targetLength = this.convertASTNode(args[0], context);
|
|
2698
|
+
const padString = args.length > 1 ? this.convertASTNode(args[1], context) : { expression: '" "' };
|
|
2699
|
+
// CEL doesn't have padStart, so we'll simulate it
|
|
2700
|
+
const expression = `size(${object.expression}) >= ${targetLength.expression} ? ${object.expression} : (${padString.expression}.repeat(${targetLength.expression} - size(${object.expression})) + ${object.expression})`;
|
|
2701
|
+
return {
|
|
2702
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2703
|
+
expression,
|
|
2704
|
+
_type: 'string'
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Convert string.padEnd() method calls
|
|
2709
|
+
*/
|
|
2710
|
+
convertStringPadEnd(object, args, context) {
|
|
2711
|
+
if (args.length < 1 || args.length > 2) {
|
|
2712
|
+
throw new Error('String.padEnd() requires 1 or 2 arguments');
|
|
2713
|
+
}
|
|
2714
|
+
const targetLength = this.convertASTNode(args[0], context);
|
|
2715
|
+
const padString = args.length > 1 ? this.convertASTNode(args[1], context) : { expression: '" "' };
|
|
2716
|
+
// CEL doesn't have padEnd, so we'll simulate it
|
|
2717
|
+
const expression = `size(${object.expression}) >= ${targetLength.expression} ? ${object.expression} : (${object.expression} + ${padString.expression}.repeat(${targetLength.expression} - size(${object.expression})))`;
|
|
2718
|
+
return {
|
|
2719
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2720
|
+
expression,
|
|
2721
|
+
_type: 'string'
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Convert string.repeat() method calls
|
|
2726
|
+
*/
|
|
2727
|
+
convertStringRepeat(object, args, context) {
|
|
2728
|
+
if (args.length !== 1) {
|
|
2729
|
+
throw new Error('String.repeat() requires exactly one argument');
|
|
2730
|
+
}
|
|
2731
|
+
const count = this.convertASTNode(args[0], context);
|
|
2732
|
+
// CEL doesn't have repeat, so we'll use a simple approach for small counts
|
|
2733
|
+
const expression = `${object.expression}.repeat(${count.expression})`;
|
|
2734
|
+
return {
|
|
2735
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2736
|
+
expression,
|
|
2737
|
+
_type: 'string'
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Convert string.replace() method calls
|
|
2742
|
+
*/
|
|
2743
|
+
convertStringReplace(object, args, context) {
|
|
2744
|
+
if (args.length !== 2) {
|
|
2745
|
+
throw new Error('String.replace() requires exactly two arguments');
|
|
2746
|
+
}
|
|
2747
|
+
const searchValue = this.convertASTNode(args[0], context);
|
|
2748
|
+
const replaceValue = this.convertASTNode(args[1], context);
|
|
2749
|
+
// CEL doesn't have replace, so we'll use a simple substitution
|
|
2750
|
+
const expression = `${object.expression}.replace(${searchValue.expression}, ${replaceValue.expression})`;
|
|
2751
|
+
return {
|
|
2752
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2753
|
+
expression,
|
|
2754
|
+
_type: 'string'
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Convert string.indexOf() method calls
|
|
2759
|
+
*/
|
|
2760
|
+
convertStringIndexOf(object, args, context) {
|
|
2761
|
+
if (args.length !== 1) {
|
|
2762
|
+
throw new Error('String.indexOf() requires exactly one argument');
|
|
2763
|
+
}
|
|
2764
|
+
const searchValue = this.convertASTNode(args[0], context);
|
|
2765
|
+
// CEL doesn't have indexOf, so we'll use a conditional approach
|
|
2766
|
+
const expression = `${object.expression}.contains(${searchValue.expression}) ? 0 : -1`;
|
|
2767
|
+
return {
|
|
2768
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2769
|
+
expression,
|
|
2770
|
+
_type: 'number'
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Convert string.lastIndexOf() method calls
|
|
2775
|
+
*/
|
|
2776
|
+
convertStringLastIndexOf(object, args, context) {
|
|
2777
|
+
if (args.length !== 1) {
|
|
2778
|
+
throw new Error('String.lastIndexOf() requires exactly one argument');
|
|
2779
|
+
}
|
|
2780
|
+
const searchValue = this.convertASTNode(args[0], context);
|
|
2781
|
+
// CEL doesn't have lastIndexOf, so we'll use a conditional approach
|
|
2782
|
+
const expression = `${object.expression}.contains(${searchValue.expression}) ? size(${object.expression}) - size(${searchValue.expression}) : -1`;
|
|
2783
|
+
return {
|
|
2784
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2785
|
+
expression,
|
|
2786
|
+
_type: 'number'
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Infer type from field path based on common Kubernetes patterns
|
|
2791
|
+
*/
|
|
2792
|
+
inferTypeFromFieldPath(fieldPath) {
|
|
2793
|
+
// Common patterns for type inference
|
|
2794
|
+
if (fieldPath.includes('replicas') || fieldPath.includes('count') || fieldPath.includes('port')) {
|
|
2795
|
+
return 0; // number
|
|
2796
|
+
}
|
|
2797
|
+
if (fieldPath.includes('ready') || fieldPath.includes('available') || fieldPath.includes('enabled')) {
|
|
2798
|
+
return false; // boolean
|
|
2799
|
+
}
|
|
2800
|
+
if (fieldPath.includes('name') || fieldPath.includes('image') || fieldPath.includes('namespace')) {
|
|
2801
|
+
return ''; // string
|
|
2802
|
+
}
|
|
2803
|
+
if (fieldPath.includes('labels') || fieldPath.includes('annotations')) {
|
|
2804
|
+
return {}; // object
|
|
2805
|
+
}
|
|
2806
|
+
if (fieldPath.includes('conditions') || fieldPath.includes('ingress') || fieldPath.includes('containers')) {
|
|
2807
|
+
return []; // array
|
|
2808
|
+
}
|
|
2809
|
+
// Default to string for unknown fields
|
|
2810
|
+
return '';
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Convert binary operators to CEL equivalents
|
|
2814
|
+
*/
|
|
2815
|
+
convertBinaryOperator(operator) {
|
|
2816
|
+
const operatorMap = {
|
|
2817
|
+
'===': '==',
|
|
2818
|
+
'!==': '!=',
|
|
2819
|
+
'==': '==',
|
|
2820
|
+
'!=': '!=',
|
|
2821
|
+
'<': '<',
|
|
2822
|
+
'<=': '<=',
|
|
2823
|
+
'>': '>',
|
|
2824
|
+
'>=': '>=',
|
|
2825
|
+
'+': '+',
|
|
2826
|
+
'-': '-',
|
|
2827
|
+
'*': '*',
|
|
2828
|
+
'/': '/',
|
|
2829
|
+
'%': '%'
|
|
2830
|
+
};
|
|
2831
|
+
const celOperator = operatorMap[operator];
|
|
2832
|
+
if (!celOperator) {
|
|
2833
|
+
throw new Error(`Unsupported binary operator: ${operator}`);
|
|
2834
|
+
}
|
|
2835
|
+
return celOperator;
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Convert array access expressions with KubernetesRef support (array[0], array[index])
|
|
2839
|
+
*/
|
|
2840
|
+
convertArrayAccess(node, context) {
|
|
2841
|
+
// Convert the object being accessed (could be a KubernetesRef)
|
|
2842
|
+
const object = this.convertASTNode(node.object, context);
|
|
2843
|
+
// Convert the index/key expression
|
|
2844
|
+
const property = this.convertASTNode(node.property, context);
|
|
2845
|
+
// Generate CEL array access expression
|
|
2846
|
+
const expression = `${object.expression}[${property.expression}]`;
|
|
2847
|
+
return {
|
|
2848
|
+
[CEL_EXPRESSION_BRAND]: true,
|
|
2849
|
+
expression,
|
|
2850
|
+
_type: undefined
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Check if a value is a static literal that doesn't need conversion
|
|
2855
|
+
* Static values (no KubernetesRef objects) should be preserved as-is for performance
|
|
2856
|
+
*/
|
|
2857
|
+
isStaticValue(value) {
|
|
2858
|
+
// Primitive values are always static
|
|
2859
|
+
if (value === null || value === undefined)
|
|
2860
|
+
return true;
|
|
2861
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
2862
|
+
return true;
|
|
2863
|
+
}
|
|
2864
|
+
// Check if it's a KubernetesRef (not static)
|
|
2865
|
+
if (isKubernetesRef(value))
|
|
2866
|
+
return false;
|
|
2867
|
+
// Check if it contains KubernetesRef objects (not static)
|
|
2868
|
+
if (containsKubernetesRefs(value))
|
|
2869
|
+
return false;
|
|
2870
|
+
// Arrays and objects need recursive checking
|
|
2871
|
+
if (Array.isArray(value)) {
|
|
2872
|
+
return value.every(item => this.isStaticValue(item));
|
|
2873
|
+
}
|
|
2874
|
+
if (value && typeof value === 'object') {
|
|
2875
|
+
return Object.values(value).every(val => this.isStaticValue(val));
|
|
2876
|
+
}
|
|
2877
|
+
// Default to static for other types
|
|
2878
|
+
return true;
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Create a result for static values that don't require conversion
|
|
2882
|
+
*/
|
|
2883
|
+
createStaticValueResult(_value) {
|
|
2884
|
+
return {
|
|
2885
|
+
valid: true,
|
|
2886
|
+
celExpression: null, // No CEL expression needed for static values
|
|
2887
|
+
dependencies: [],
|
|
2888
|
+
sourceMap: [],
|
|
2889
|
+
errors: [],
|
|
2890
|
+
warnings: [],
|
|
2891
|
+
requiresConversion: false // Key: static values don't need conversion
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Analyze expression using factory pattern aware handling
|
|
2896
|
+
*
|
|
2897
|
+
* This method integrates with the factory pattern handler to provide
|
|
2898
|
+
* appropriate expression processing based on the deployment strategy.
|
|
2899
|
+
*/
|
|
2900
|
+
analyzeExpressionWithFactoryPattern(expression, context) {
|
|
2901
|
+
try {
|
|
2902
|
+
// Use the factory pattern handler for initial processing
|
|
2903
|
+
const factoryResult = handleExpressionWithFactoryPattern(expression, context);
|
|
2904
|
+
// If the factory handler processed it successfully, return the result
|
|
2905
|
+
if (factoryResult.valid && factoryResult.celExpression) {
|
|
2906
|
+
return factoryResult;
|
|
2907
|
+
}
|
|
2908
|
+
// If the factory handler determined no conversion is needed, return as-is
|
|
2909
|
+
if (!factoryResult.requiresConversion) {
|
|
2910
|
+
return factoryResult;
|
|
2911
|
+
}
|
|
2912
|
+
// If the factory handler couldn't process it, fall back to the main analyzer
|
|
2913
|
+
if (typeof expression === 'string') {
|
|
2914
|
+
return this.analyzeExpression(expression, context);
|
|
2915
|
+
}
|
|
2916
|
+
// For non-string expressions that need conversion, return the factory result
|
|
2917
|
+
return factoryResult;
|
|
2918
|
+
}
|
|
2919
|
+
catch (error) {
|
|
2920
|
+
return {
|
|
2921
|
+
valid: false,
|
|
2922
|
+
celExpression: null,
|
|
2923
|
+
dependencies: [],
|
|
2924
|
+
sourceMap: [],
|
|
2925
|
+
errors: [new ConversionError(`Factory pattern expression analysis failed: ${error instanceof Error ? error.message : String(error)}`, String(expression), 'javascript')],
|
|
2926
|
+
warnings: [],
|
|
2927
|
+
requiresConversion: true
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Get cache statistics for performance monitoring
|
|
2933
|
+
*/
|
|
2934
|
+
getCacheStats() {
|
|
2935
|
+
return this.cache.getStats();
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Clear all caches
|
|
2939
|
+
*/
|
|
2940
|
+
clearCache() {
|
|
2941
|
+
this.cache.clear();
|
|
2942
|
+
}
|
|
2943
|
+
/**
|
|
2944
|
+
* Force cleanup of expired cache entries
|
|
2945
|
+
*/
|
|
2946
|
+
cleanupCache() {
|
|
2947
|
+
return this.cache.cleanup();
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Destroy analyzer and cleanup resources
|
|
2951
|
+
*/
|
|
2952
|
+
destroy() {
|
|
2953
|
+
this.cache.destroy();
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
//# sourceMappingURL=analyzer.js.map
|