parse-hcl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +749 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +74 -0
- package/dist/parsers/genericParser.d.ts +167 -0
- package/dist/parsers/genericParser.js +268 -0
- package/dist/parsers/localsParser.d.ts +30 -0
- package/dist/parsers/localsParser.js +43 -0
- package/dist/parsers/outputParser.d.ts +25 -0
- package/dist/parsers/outputParser.js +44 -0
- package/dist/parsers/variableParser.d.ts +62 -0
- package/dist/parsers/variableParser.js +249 -0
- package/dist/services/artifactParsers.d.ts +12 -0
- package/dist/services/artifactParsers.js +157 -0
- package/dist/services/terraformJsonParser.d.ts +16 -0
- package/dist/services/terraformJsonParser.js +212 -0
- package/dist/services/terraformParser.d.ts +91 -0
- package/dist/services/terraformParser.js +191 -0
- package/dist/types/artifacts.d.ts +210 -0
- package/dist/types/artifacts.js +5 -0
- package/dist/types/blocks.d.ts +419 -0
- package/dist/types/blocks.js +28 -0
- package/dist/utils/common/errors.d.ts +46 -0
- package/dist/utils/common/errors.js +54 -0
- package/dist/utils/common/fs.d.ts +5 -0
- package/dist/utils/common/fs.js +48 -0
- package/dist/utils/common/logger.d.ts +5 -0
- package/dist/utils/common/logger.js +17 -0
- package/dist/utils/common/valueHelpers.d.ts +4 -0
- package/dist/utils/common/valueHelpers.js +23 -0
- package/dist/utils/graph/graphBuilder.d.ts +33 -0
- package/dist/utils/graph/graphBuilder.js +373 -0
- package/dist/utils/lexer/blockScanner.d.ts +36 -0
- package/dist/utils/lexer/blockScanner.js +143 -0
- package/dist/utils/lexer/hclLexer.d.ts +119 -0
- package/dist/utils/lexer/hclLexer.js +525 -0
- package/dist/utils/parser/bodyParser.d.ts +26 -0
- package/dist/utils/parser/bodyParser.js +81 -0
- package/dist/utils/parser/valueClassifier.d.ts +21 -0
- package/dist/utils/parser/valueClassifier.js +434 -0
- package/dist/utils/serialization/serializer.d.ts +9 -0
- package/dist/utils/serialization/serializer.js +63 -0
- package/dist/utils/serialization/yaml.d.ts +1 -0
- package/dist/utils/serialization/yaml.js +81 -0
- package/package.json +66 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dependency graph builder for Terraform documents.
|
|
4
|
+
* Creates a directed graph of dependencies between Terraform elements.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.buildDependencyGraph = buildDependencyGraph;
|
|
8
|
+
exports.createExport = createExport;
|
|
9
|
+
/** Current graph export version */
|
|
10
|
+
const GRAPH_VERSION = '1.0.0';
|
|
11
|
+
/**
|
|
12
|
+
* Builds a dependency graph from a parsed Terraform document.
|
|
13
|
+
* Analyzes all blocks and their references to construct nodes and edges.
|
|
14
|
+
*
|
|
15
|
+
* @param document - The parsed Terraform document
|
|
16
|
+
* @returns A complete dependency graph with nodes, edges, and orphan references
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const parser = new TerraformParser();
|
|
21
|
+
* const doc = parser.parseFile('main.tf');
|
|
22
|
+
* const graph = buildDependencyGraph(doc);
|
|
23
|
+
*
|
|
24
|
+
* // Visualize dependencies
|
|
25
|
+
* for (const edge of graph.edges) {
|
|
26
|
+
* console.log(`${edge.from} -> ${edge.to}`);
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function buildDependencyGraph(document) {
|
|
31
|
+
const nodes = new Map();
|
|
32
|
+
const edges = [];
|
|
33
|
+
const orphanReferences = [];
|
|
34
|
+
const edgeKeys = new Set();
|
|
35
|
+
// First pass: populate all declared nodes
|
|
36
|
+
populateNodes(document, nodes);
|
|
37
|
+
/**
|
|
38
|
+
* Adds edges from a source node to all referenced targets.
|
|
39
|
+
*/
|
|
40
|
+
const addEdges = (fromNode, refs, source) => {
|
|
41
|
+
if (!fromNode || refs.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
const target = ensureTargetNode(ref, nodes);
|
|
46
|
+
if (!target) {
|
|
47
|
+
orphanReferences.push(ref);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Deduplicate edges
|
|
51
|
+
const key = `${fromNode.id}->${target.id}:${JSON.stringify(ref)}`;
|
|
52
|
+
if (edgeKeys.has(key)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
edgeKeys.add(key);
|
|
56
|
+
edges.push({
|
|
57
|
+
from: fromNode.id,
|
|
58
|
+
to: target.id,
|
|
59
|
+
reference: ref,
|
|
60
|
+
source
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
// Process terraform settings
|
|
65
|
+
for (const block of document.terraform) {
|
|
66
|
+
const node = nodes.get(nodeId('terraform', 'settings'));
|
|
67
|
+
addEdges(node, referencesFromAttributes(block.properties), block.source);
|
|
68
|
+
}
|
|
69
|
+
// Process providers
|
|
70
|
+
for (const provider of document.provider) {
|
|
71
|
+
const node = nodes.get(nodeId('provider', provider.name, provider.alias));
|
|
72
|
+
addEdges(node, referencesFromAttributes(provider.properties), provider.source);
|
|
73
|
+
}
|
|
74
|
+
// Process variables (references in default values)
|
|
75
|
+
for (const variable of document.variable) {
|
|
76
|
+
const node = nodes.get(nodeId('variable', variable.name));
|
|
77
|
+
addEdges(node, referencesFromValue(variable.default), variable.source);
|
|
78
|
+
}
|
|
79
|
+
// Process outputs
|
|
80
|
+
for (const output of document.output) {
|
|
81
|
+
const node = nodes.get(nodeId('output', output.name));
|
|
82
|
+
addEdges(node, referencesFromValue(output.value), output.source);
|
|
83
|
+
}
|
|
84
|
+
// Process modules
|
|
85
|
+
for (const module of document.module) {
|
|
86
|
+
const node = nodes.get(nodeId('module', module.name));
|
|
87
|
+
addEdges(node, referencesFromAttributes(module.properties), module.source);
|
|
88
|
+
}
|
|
89
|
+
// Process resources
|
|
90
|
+
for (const resource of document.resource) {
|
|
91
|
+
const node = nodes.get(nodeId('resource', resource.type, resource.name));
|
|
92
|
+
addEdges(node, referencesFromAttributes(resource.properties), resource.source);
|
|
93
|
+
addEdges(node, referencesFromAttributes(resource.meta), resource.source);
|
|
94
|
+
// Process dynamic blocks
|
|
95
|
+
for (const dyn of resource.dynamic_blocks) {
|
|
96
|
+
addEdges(node, referencesFromValue(dyn.for_each), resource.source);
|
|
97
|
+
addEdges(node, referencesFromAttributes(dyn.content), resource.source);
|
|
98
|
+
}
|
|
99
|
+
// Process nested blocks
|
|
100
|
+
addEdges(node, referencesFromNestedBlocks(resource.blocks), resource.source);
|
|
101
|
+
}
|
|
102
|
+
// Process data sources
|
|
103
|
+
for (const data of document.data) {
|
|
104
|
+
const node = nodes.get(nodeId('data', data.dataType, data.name));
|
|
105
|
+
addEdges(node, referencesFromAttributes(data.properties), data.source);
|
|
106
|
+
addEdges(node, referencesFromNestedBlocks(data.blocks), data.source);
|
|
107
|
+
}
|
|
108
|
+
// Process locals
|
|
109
|
+
for (const local of document.locals) {
|
|
110
|
+
const node = nodes.get(nodeId('locals', local.name));
|
|
111
|
+
addEdges(node, referencesFromValue(local.value), local.source);
|
|
112
|
+
}
|
|
113
|
+
// Process other blocks (moved/import/check/terraform_data/unknown)
|
|
114
|
+
const otherBlocks = [
|
|
115
|
+
...document.moved,
|
|
116
|
+
...document.import,
|
|
117
|
+
...document.check,
|
|
118
|
+
...document.terraform_data,
|
|
119
|
+
...document.unknown
|
|
120
|
+
];
|
|
121
|
+
for (const block of otherBlocks) {
|
|
122
|
+
// These blocks don't have nodes, but we track their references
|
|
123
|
+
const allRefs = [
|
|
124
|
+
...referencesFromAttributes(block.properties),
|
|
125
|
+
...referencesFromNestedBlocks(block.blocks)
|
|
126
|
+
];
|
|
127
|
+
// Add edges from the block type as a pseudo-node
|
|
128
|
+
const blockNode = nodes.get(nodeId(block.type, block.labels[0] || 'default'));
|
|
129
|
+
if (!blockNode) {
|
|
130
|
+
// Create a node for this block
|
|
131
|
+
const newNode = {
|
|
132
|
+
id: nodeId(block.type, block.labels[0] || 'default'),
|
|
133
|
+
kind: block.type,
|
|
134
|
+
name: block.labels[0] || 'default',
|
|
135
|
+
source: block.source
|
|
136
|
+
};
|
|
137
|
+
nodes.set(newNode.id, newNode);
|
|
138
|
+
addEdges(newNode, allRefs, block.source);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
addEdges(blockNode, allRefs, block.source);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
nodes: Array.from(nodes.values()),
|
|
146
|
+
edges,
|
|
147
|
+
orphanReferences
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Creates a complete export containing the document and its dependency graph.
|
|
152
|
+
*
|
|
153
|
+
* @param document - The parsed Terraform document
|
|
154
|
+
* @returns TerraformExport with version, document, and graph
|
|
155
|
+
*/
|
|
156
|
+
function createExport(document) {
|
|
157
|
+
return {
|
|
158
|
+
version: GRAPH_VERSION,
|
|
159
|
+
document,
|
|
160
|
+
graph: buildDependencyGraph(document)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Populates the node map with all declared elements from the document.
|
|
165
|
+
*/
|
|
166
|
+
function populateNodes(document, nodes) {
|
|
167
|
+
const addNode = (node) => {
|
|
168
|
+
if (!nodes.has(node.id)) {
|
|
169
|
+
nodes.set(node.id, node);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
// Always add terraform settings node
|
|
173
|
+
addNode({
|
|
174
|
+
id: nodeId('terraform', 'settings'),
|
|
175
|
+
kind: 'terraform',
|
|
176
|
+
name: 'settings'
|
|
177
|
+
});
|
|
178
|
+
// Add provider nodes
|
|
179
|
+
for (const provider of document.provider) {
|
|
180
|
+
addNode({
|
|
181
|
+
id: nodeId('provider', provider.name, provider.alias),
|
|
182
|
+
kind: 'provider',
|
|
183
|
+
name: provider.alias || provider.name,
|
|
184
|
+
type: provider.name,
|
|
185
|
+
source: provider.source
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Add variable nodes
|
|
189
|
+
for (const variable of document.variable) {
|
|
190
|
+
addNode({
|
|
191
|
+
id: nodeId('variable', variable.name),
|
|
192
|
+
kind: 'variable',
|
|
193
|
+
name: variable.name,
|
|
194
|
+
source: variable.source
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Add output nodes
|
|
198
|
+
for (const output of document.output) {
|
|
199
|
+
addNode({
|
|
200
|
+
id: nodeId('output', output.name),
|
|
201
|
+
kind: 'output',
|
|
202
|
+
name: output.name,
|
|
203
|
+
source: output.source
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Add module nodes
|
|
207
|
+
for (const module of document.module) {
|
|
208
|
+
addNode({
|
|
209
|
+
id: nodeId('module', module.name),
|
|
210
|
+
kind: 'module',
|
|
211
|
+
name: module.name,
|
|
212
|
+
source: module.source
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// Add resource nodes
|
|
216
|
+
for (const resource of document.resource) {
|
|
217
|
+
addNode({
|
|
218
|
+
id: nodeId('resource', resource.type, resource.name),
|
|
219
|
+
kind: 'resource',
|
|
220
|
+
name: resource.name,
|
|
221
|
+
type: resource.type,
|
|
222
|
+
source: resource.source
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Add data source nodes
|
|
226
|
+
for (const data of document.data) {
|
|
227
|
+
addNode({
|
|
228
|
+
id: nodeId('data', data.dataType, data.name),
|
|
229
|
+
kind: 'data',
|
|
230
|
+
name: data.name,
|
|
231
|
+
type: data.dataType,
|
|
232
|
+
source: data.source
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Add local value nodes
|
|
236
|
+
for (const local of document.locals) {
|
|
237
|
+
addNode({
|
|
238
|
+
id: nodeId('locals', local.name),
|
|
239
|
+
kind: 'locals',
|
|
240
|
+
name: local.name,
|
|
241
|
+
source: local.source
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Extracts all references from a record of attributes.
|
|
247
|
+
*/
|
|
248
|
+
function referencesFromAttributes(attributes) {
|
|
249
|
+
if (!attributes) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
return Object.values(attributes).flatMap((value) => referencesFromValue(value));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Recursively extracts references from nested blocks.
|
|
256
|
+
*/
|
|
257
|
+
function referencesFromNestedBlocks(blocks) {
|
|
258
|
+
const refs = [];
|
|
259
|
+
for (const block of blocks) {
|
|
260
|
+
refs.push(...referencesFromAttributes(block.attributes));
|
|
261
|
+
refs.push(...referencesFromNestedBlocks(block.blocks));
|
|
262
|
+
}
|
|
263
|
+
return refs;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Extracts all references from a value (recursively for arrays and objects).
|
|
267
|
+
*/
|
|
268
|
+
function referencesFromValue(value) {
|
|
269
|
+
if (!value) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const direct = value.references ?? [];
|
|
273
|
+
if (value.type === 'object' && value.value) {
|
|
274
|
+
return [...direct, ...referencesFromAttributes(value.value)];
|
|
275
|
+
}
|
|
276
|
+
if (value.type === 'array' && Array.isArray(value.value)) {
|
|
277
|
+
return [
|
|
278
|
+
...direct,
|
|
279
|
+
...value.value.flatMap((item) => referencesFromValue(item))
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
return direct;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Creates a node ID from kind and name components.
|
|
286
|
+
*/
|
|
287
|
+
function nodeId(kind, primary, secondary) {
|
|
288
|
+
return [kind, primary, secondary].filter(Boolean).join('.');
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Ensures a target node exists for a reference, creating a placeholder if needed.
|
|
292
|
+
*/
|
|
293
|
+
function ensureTargetNode(ref, nodes) {
|
|
294
|
+
const existing = nodes.get(referenceToId(ref));
|
|
295
|
+
if (existing) {
|
|
296
|
+
return existing;
|
|
297
|
+
}
|
|
298
|
+
const placeholder = referenceToNode(ref);
|
|
299
|
+
if (!placeholder) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
nodes.set(placeholder.id, placeholder);
|
|
303
|
+
return placeholder;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Converts a reference to its corresponding node ID.
|
|
307
|
+
*/
|
|
308
|
+
function referenceToId(ref) {
|
|
309
|
+
switch (ref.kind) {
|
|
310
|
+
case 'variable':
|
|
311
|
+
return nodeId('variable', ref.name);
|
|
312
|
+
case 'local':
|
|
313
|
+
return nodeId('locals', ref.name);
|
|
314
|
+
case 'module_output':
|
|
315
|
+
return nodeId('module_output', ref.module, ref.name);
|
|
316
|
+
case 'data':
|
|
317
|
+
return nodeId('data', ref.data_type, ref.name);
|
|
318
|
+
case 'resource':
|
|
319
|
+
return nodeId('resource', ref.resource_type, ref.name);
|
|
320
|
+
case 'path':
|
|
321
|
+
return nodeId('path', ref.name);
|
|
322
|
+
case 'each':
|
|
323
|
+
return nodeId('each', ref.property);
|
|
324
|
+
case 'count':
|
|
325
|
+
return nodeId('count', ref.property);
|
|
326
|
+
case 'self':
|
|
327
|
+
return nodeId('self', ref.attribute);
|
|
328
|
+
default:
|
|
329
|
+
return '';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Converts a reference to a placeholder node.
|
|
334
|
+
*/
|
|
335
|
+
function referenceToNode(ref) {
|
|
336
|
+
switch (ref.kind) {
|
|
337
|
+
case 'variable':
|
|
338
|
+
return { id: referenceToId(ref), kind: 'variable', name: ref.name };
|
|
339
|
+
case 'local':
|
|
340
|
+
return { id: referenceToId(ref), kind: 'locals', name: ref.name };
|
|
341
|
+
case 'module_output':
|
|
342
|
+
return {
|
|
343
|
+
id: referenceToId(ref),
|
|
344
|
+
kind: 'module_output',
|
|
345
|
+
name: ref.name,
|
|
346
|
+
type: ref.module
|
|
347
|
+
};
|
|
348
|
+
case 'data':
|
|
349
|
+
return {
|
|
350
|
+
id: referenceToId(ref),
|
|
351
|
+
kind: 'data',
|
|
352
|
+
name: ref.name,
|
|
353
|
+
type: ref.data_type
|
|
354
|
+
};
|
|
355
|
+
case 'resource':
|
|
356
|
+
return {
|
|
357
|
+
id: referenceToId(ref),
|
|
358
|
+
kind: 'resource',
|
|
359
|
+
name: ref.name,
|
|
360
|
+
type: ref.resource_type
|
|
361
|
+
};
|
|
362
|
+
case 'path':
|
|
363
|
+
return { id: referenceToId(ref), kind: 'path', name: ref.name };
|
|
364
|
+
case 'each':
|
|
365
|
+
return { id: referenceToId(ref), kind: 'each', name: ref.property };
|
|
366
|
+
case 'count':
|
|
367
|
+
return { id: referenceToId(ref), kind: 'count', name: ref.property };
|
|
368
|
+
case 'self':
|
|
369
|
+
return { id: referenceToId(ref), kind: 'self', name: ref.attribute };
|
|
370
|
+
default:
|
|
371
|
+
return { id: `external.${JSON.stringify(ref)}`, kind: 'external', name: 'external' };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner for identifying top-level HCL blocks in Terraform configuration files.
|
|
3
|
+
* Handles block detection, label extraction, and body isolation.
|
|
4
|
+
*/
|
|
5
|
+
import { HclBlock } from '../../types/blocks';
|
|
6
|
+
/**
|
|
7
|
+
* Options for block scanning.
|
|
8
|
+
*/
|
|
9
|
+
export interface ScanOptions {
|
|
10
|
+
/** Whether to throw ParseError on syntax errors (default: false, logs warning instead) */
|
|
11
|
+
strict?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scanner for extracting top-level HCL blocks from Terraform files.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const scanner = new BlockScanner();
|
|
19
|
+
* const blocks = scanner.scan(hclContent, 'main.tf');
|
|
20
|
+
* for (const block of blocks) {
|
|
21
|
+
* console.log(`Found ${block.kind} block: ${block.labels.join('.')}`);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class BlockScanner {
|
|
26
|
+
/**
|
|
27
|
+
* Scans HCL content and extracts all top-level blocks.
|
|
28
|
+
*
|
|
29
|
+
* @param content - The HCL source content to scan
|
|
30
|
+
* @param source - The source file path (for error reporting)
|
|
31
|
+
* @param options - Scanning options
|
|
32
|
+
* @returns Array of parsed HCL blocks
|
|
33
|
+
* @throws {ParseError} If strict mode is enabled and syntax errors are found
|
|
34
|
+
*/
|
|
35
|
+
scan(content: string, source: string, options?: ScanOptions): HclBlock[];
|
|
36
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for identifying top-level HCL blocks in Terraform configuration files.
|
|
4
|
+
* Handles block detection, label extraction, and body isolation.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.BlockScanner = void 0;
|
|
8
|
+
const errors_1 = require("../common/errors");
|
|
9
|
+
const hclLexer_1 = require("./hclLexer");
|
|
10
|
+
const logger_1 = require("../common/logger");
|
|
11
|
+
/**
|
|
12
|
+
* Set of known Terraform block types.
|
|
13
|
+
* Unknown block types are categorized as 'unknown'.
|
|
14
|
+
*/
|
|
15
|
+
const KNOWN_BLOCKS = new Set([
|
|
16
|
+
'terraform',
|
|
17
|
+
'locals',
|
|
18
|
+
'provider',
|
|
19
|
+
'variable',
|
|
20
|
+
'output',
|
|
21
|
+
'module',
|
|
22
|
+
'resource',
|
|
23
|
+
'data',
|
|
24
|
+
'moved',
|
|
25
|
+
'import',
|
|
26
|
+
'check',
|
|
27
|
+
'terraform_data'
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Scanner for extracting top-level HCL blocks from Terraform files.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const scanner = new BlockScanner();
|
|
35
|
+
* const blocks = scanner.scan(hclContent, 'main.tf');
|
|
36
|
+
* for (const block of blocks) {
|
|
37
|
+
* console.log(`Found ${block.kind} block: ${block.labels.join('.')}`);
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
class BlockScanner {
|
|
42
|
+
/**
|
|
43
|
+
* Scans HCL content and extracts all top-level blocks.
|
|
44
|
+
*
|
|
45
|
+
* @param content - The HCL source content to scan
|
|
46
|
+
* @param source - The source file path (for error reporting)
|
|
47
|
+
* @param options - Scanning options
|
|
48
|
+
* @returns Array of parsed HCL blocks
|
|
49
|
+
* @throws {ParseError} If strict mode is enabled and syntax errors are found
|
|
50
|
+
*/
|
|
51
|
+
scan(content, source, options) {
|
|
52
|
+
const blocks = [];
|
|
53
|
+
const length = content.length;
|
|
54
|
+
let index = 0;
|
|
55
|
+
const strict = options?.strict ?? false;
|
|
56
|
+
while (index < length) {
|
|
57
|
+
index = (0, hclLexer_1.skipWhitespaceAndComments)(content, index);
|
|
58
|
+
// Skip standalone strings (not part of block headers)
|
|
59
|
+
if ((0, hclLexer_1.isQuote)(content[index])) {
|
|
60
|
+
index = (0, hclLexer_1.skipString)(content, index);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const identifierStart = index;
|
|
64
|
+
const keyword = (0, hclLexer_1.readIdentifier)(content, index);
|
|
65
|
+
if (!keyword) {
|
|
66
|
+
index++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
index += keyword.length;
|
|
70
|
+
index = (0, hclLexer_1.skipWhitespaceAndComments)(content, index);
|
|
71
|
+
// Read block labels (quoted strings)
|
|
72
|
+
const labels = [];
|
|
73
|
+
while ((0, hclLexer_1.isQuote)(content[index])) {
|
|
74
|
+
const { text, end } = (0, hclLexer_1.readQuotedString)(content, index);
|
|
75
|
+
labels.push(text);
|
|
76
|
+
index = (0, hclLexer_1.skipWhitespaceAndComments)(content, end);
|
|
77
|
+
}
|
|
78
|
+
// Check for opening brace
|
|
79
|
+
if (content[index] !== '{') {
|
|
80
|
+
// Not a block header, continue searching
|
|
81
|
+
index = identifierStart + keyword.length;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const braceIndex = index;
|
|
85
|
+
const endIndex = (0, hclLexer_1.findMatchingBrace)(content, braceIndex);
|
|
86
|
+
if (endIndex === -1) {
|
|
87
|
+
const location = (0, errors_1.offsetToLocation)(content, braceIndex);
|
|
88
|
+
const message = `Unclosed block '${keyword}': missing closing '}'`;
|
|
89
|
+
if (strict) {
|
|
90
|
+
throw new errors_1.ParseError(message, source, location);
|
|
91
|
+
}
|
|
92
|
+
logger_1.logger.warn(`${message} in ${source}:${location.line}:${location.column}`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const raw = normalizeRaw(content.slice(identifierStart, endIndex + 1));
|
|
96
|
+
const body = content.slice(braceIndex + 1, endIndex);
|
|
97
|
+
const kind = (KNOWN_BLOCKS.has(keyword) ? keyword : 'unknown');
|
|
98
|
+
blocks.push({
|
|
99
|
+
kind,
|
|
100
|
+
keyword,
|
|
101
|
+
labels,
|
|
102
|
+
body: body.trim(),
|
|
103
|
+
raw,
|
|
104
|
+
source
|
|
105
|
+
});
|
|
106
|
+
index = endIndex + 1;
|
|
107
|
+
}
|
|
108
|
+
return blocks;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.BlockScanner = BlockScanner;
|
|
112
|
+
/**
|
|
113
|
+
* Normalizes raw block content for consistent formatting.
|
|
114
|
+
* - Removes common leading indentation
|
|
115
|
+
* - Normalizes whitespace around '=' operators
|
|
116
|
+
* - Trims trailing whitespace from lines
|
|
117
|
+
*
|
|
118
|
+
* @param raw - The raw block content
|
|
119
|
+
* @returns Normalized block content
|
|
120
|
+
*/
|
|
121
|
+
function normalizeRaw(raw) {
|
|
122
|
+
const trimmed = raw.trim();
|
|
123
|
+
const lines = trimmed.split(/\r?\n/);
|
|
124
|
+
if (lines.length === 1) {
|
|
125
|
+
return lines[0];
|
|
126
|
+
}
|
|
127
|
+
// Calculate minimum indentation (excluding first line and empty lines)
|
|
128
|
+
const indents = lines
|
|
129
|
+
.slice(1)
|
|
130
|
+
.filter((line) => line.trim().length > 0)
|
|
131
|
+
.map((line) => (line.match(/^(\s*)/)?.[1].length ?? 0));
|
|
132
|
+
const minIndent = indents.length ? Math.min(...indents) : 0;
|
|
133
|
+
// Normalize alignment around '=' operators
|
|
134
|
+
const normalizeAlignment = (line) => line
|
|
135
|
+
.replace(/\s{2,}=\s*/g, ' = ')
|
|
136
|
+
.replace(/\s*=\s{2,}/g, ' = ')
|
|
137
|
+
.trimEnd();
|
|
138
|
+
const normalized = lines.map((line, index) => {
|
|
139
|
+
const withoutIndent = index === 0 ? line.trimStart() : line.slice(Math.min(minIndent, line.length));
|
|
140
|
+
return normalizeAlignment(withoutIndent);
|
|
141
|
+
});
|
|
142
|
+
return normalized.join('\n');
|
|
143
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared lexer utilities for HCL parsing.
|
|
3
|
+
* Provides common functions for tokenization and string handling.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Result of reading a quoted string from source.
|
|
7
|
+
*/
|
|
8
|
+
export interface QuotedStringResult {
|
|
9
|
+
/** The unquoted string content */
|
|
10
|
+
text: string;
|
|
11
|
+
/** The index after the closing quote */
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Result of reading a raw value from source.
|
|
16
|
+
*/
|
|
17
|
+
export interface ValueReadResult {
|
|
18
|
+
/** The raw value text (trimmed) */
|
|
19
|
+
raw: string;
|
|
20
|
+
/** The index after the value */
|
|
21
|
+
end: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Checks if a character is a quote character (single or double).
|
|
25
|
+
* @param char - The character to check
|
|
26
|
+
* @returns True if the character is a quote
|
|
27
|
+
*/
|
|
28
|
+
export declare function isQuote(char: string | undefined): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a character at a given position is escaped by counting preceding backslashes.
|
|
31
|
+
* Handles consecutive backslashes correctly (e.g., \\\\ is two escaped backslashes).
|
|
32
|
+
* @param text - The source text
|
|
33
|
+
* @param index - The index of the character to check
|
|
34
|
+
* @returns True if the character is escaped
|
|
35
|
+
*/
|
|
36
|
+
export declare function isEscaped(text: string, index: number): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Skips whitespace and comments (line and block comments).
|
|
39
|
+
* Handles "//", block comments, and "#" style comments.
|
|
40
|
+
* @param text - The source text
|
|
41
|
+
* @param start - The starting index
|
|
42
|
+
* @returns The index of the next non-whitespace, non-comment character
|
|
43
|
+
*/
|
|
44
|
+
export declare function skipWhitespaceAndComments(text: string, start: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* Skips a quoted string, handling escape sequences correctly.
|
|
47
|
+
* @param text - The source text
|
|
48
|
+
* @param start - The index of the opening quote
|
|
49
|
+
* @returns The index after the closing quote
|
|
50
|
+
*/
|
|
51
|
+
export declare function skipString(text: string, start: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* Skips a heredoc string (<<EOF or <<-EOF style).
|
|
54
|
+
* @param text - The source text
|
|
55
|
+
* @param start - The index of the first '<'
|
|
56
|
+
* @returns The index after the heredoc terminator
|
|
57
|
+
*/
|
|
58
|
+
export declare function skipHeredoc(text: string, start: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* Finds the matching closing brace for an opening brace.
|
|
61
|
+
* Handles nested braces, strings, comments, and heredocs.
|
|
62
|
+
* @param content - The source text
|
|
63
|
+
* @param startIndex - The index of the opening brace
|
|
64
|
+
* @returns The index of the matching closing brace, or -1 if not found
|
|
65
|
+
*/
|
|
66
|
+
export declare function findMatchingBrace(content: string, startIndex: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* Finds the matching closing bracket for an opening bracket.
|
|
69
|
+
* Handles nested brackets of all types [], {}, ().
|
|
70
|
+
* @param content - The source text
|
|
71
|
+
* @param startIndex - The index of the opening bracket
|
|
72
|
+
* @param openChar - The opening bracket character
|
|
73
|
+
* @param closeChar - The closing bracket character
|
|
74
|
+
* @returns The index of the matching closing bracket, or -1 if not found
|
|
75
|
+
*/
|
|
76
|
+
export declare function findMatchingBracket(content: string, startIndex: number, openChar: string, closeChar: string): number;
|
|
77
|
+
/**
|
|
78
|
+
* Reads an identifier from the source text.
|
|
79
|
+
* Identifiers start with a letter or underscore, followed by letters, digits, underscores, or hyphens.
|
|
80
|
+
* @param text - The source text
|
|
81
|
+
* @param start - The starting index
|
|
82
|
+
* @returns The identifier string, or empty string if no identifier found
|
|
83
|
+
*/
|
|
84
|
+
export declare function readIdentifier(text: string, start: number): string;
|
|
85
|
+
/**
|
|
86
|
+
* Reads an identifier that may contain dots (for attribute access).
|
|
87
|
+
* @param text - The source text
|
|
88
|
+
* @param start - The starting index
|
|
89
|
+
* @returns The identifier string, or empty string if no identifier found
|
|
90
|
+
*/
|
|
91
|
+
export declare function readDottedIdentifier(text: string, start: number): string;
|
|
92
|
+
/**
|
|
93
|
+
* Reads a quoted string and returns its unescaped content.
|
|
94
|
+
* @param text - The source text
|
|
95
|
+
* @param start - The index of the opening quote
|
|
96
|
+
* @returns The unquoted string and the index after the closing quote
|
|
97
|
+
*/
|
|
98
|
+
export declare function readQuotedString(text: string, start: number): QuotedStringResult;
|
|
99
|
+
/**
|
|
100
|
+
* Reads a value from HCL source (handles multiline values in brackets).
|
|
101
|
+
* @param text - The source text
|
|
102
|
+
* @param start - The starting index (after the '=' sign)
|
|
103
|
+
* @returns The raw value text and the index after the value
|
|
104
|
+
*/
|
|
105
|
+
export declare function readValue(text: string, start: number): ValueReadResult;
|
|
106
|
+
/**
|
|
107
|
+
* Splits an array literal into its elements.
|
|
108
|
+
* Handles nested arrays, objects, and strings correctly.
|
|
109
|
+
* @param raw - The raw array string including brackets
|
|
110
|
+
* @returns Array of raw element strings
|
|
111
|
+
*/
|
|
112
|
+
export declare function splitArrayElements(raw: string): string[];
|
|
113
|
+
/**
|
|
114
|
+
* Splits an object literal into key-value pairs.
|
|
115
|
+
* Handles nested objects, arrays, and strings correctly.
|
|
116
|
+
* @param raw - The raw object string including braces
|
|
117
|
+
* @returns Array of [key, value] tuples
|
|
118
|
+
*/
|
|
119
|
+
export declare function splitObjectEntries(raw: string): Array<[string, string]>;
|