@cdxgen/cdxgen 13.2.0 → 13.4.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/README.md +33 -14
- package/bin/cdxgen.js +310 -138
- package/bin/evinse.js +22 -0
- package/bin/repl.js +86 -0
- package/bin/sign.js +119 -15
- package/bin/tracebom.js +69 -5
- package/bin/validate.js +107 -19
- package/bin/verify.js +172 -63
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +85 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +156 -58
- package/lib/cli/jsBom.js +99 -53
- package/lib/cli/jvmBom.js +708 -147
- package/lib/cli/managedBom.js +415 -224
- package/lib/cli/nativeBom.js +264 -44
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +22 -15
- package/lib/core/fs.js +176 -7
- package/lib/core/httpClient.js +159 -24
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +274 -108
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +76 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +399 -51
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1983 -793
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +356 -37
- package/lib/ecosystems/jsEvidence.js +104 -44
- package/lib/ecosystems/kosi.js +31 -7
- package/lib/ecosystems/npmutils.js +174 -9
- package/lib/ecosystems/parsers-dotnet.js +616 -159
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +371 -70
- package/lib/ecosystems/parsers-js.js +263 -76
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +929 -216
- package/lib/ecosystems/parsers-python.js +167 -24
- package/lib/ecosystems/parsers-rust.js +28 -22
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +1492 -182
- package/lib/ecosystems/rusi.js +54 -10
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +1008 -191
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +1101 -222
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +703 -43
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +798 -57
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +90 -6
- package/lib/inventory/evidenceUtils.js +166 -113
- package/lib/inventory/fetchBatch.js +690 -55
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +45 -27
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +295 -25
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +467 -46
- package/lib/managers/oci.js +63 -3
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +466 -0
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +83 -16
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +64 -11
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +2 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +23 -23
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +9 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +70 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +57 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +54 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +16 -5
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +164 -3
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +132 -15
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +182 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +219 -5
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +121 -9
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +59 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +17 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +77 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +18 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure extractor for the Terraform / OpenTofu configuration files in one
|
|
3
|
+
* directory: `*.tf`, `*.tf.json`, `*.tofu` and `*.tofu.json`.
|
|
4
|
+
*
|
|
5
|
+
* Only three things in a configuration matter for a BOM: the `module` blocks
|
|
6
|
+
* (name, `source`, `version`), the `required_providers` entries inside the
|
|
7
|
+
* `terraform` block, and `required_version`. Full HCL is a much larger
|
|
8
|
+
* grammar, so this module scans characters directly — comments, quoted
|
|
9
|
+
* strings with `${…}`/`%{…}` template sequences, heredocs, and
|
|
10
|
+
* bracket-depth-aware attribute boundaries — and skips everything else with
|
|
11
|
+
* balanced matching. It never throws and always terminates: every loop either
|
|
12
|
+
* consumes input or stops at EOF.
|
|
13
|
+
*
|
|
14
|
+
* An expression counts as a literal only when it is exactly one quoted string
|
|
15
|
+
* without template sequences; module inputs, provider configuration and
|
|
16
|
+
* backend settings are never captured, let alone emitted.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const IDENTIFIER_START = /[A-Za-z_]/;
|
|
20
|
+
const IDENTIFIER_PART = /[A-Za-z0-9_-]/;
|
|
21
|
+
const HEREDOC_MARKER = /[A-Za-z0-9_]/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse one configuration file.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} text File contents
|
|
27
|
+
* @param {Object} [options]
|
|
28
|
+
* @param {boolean} [options.json] Parse as the JSON variant (`.tf.json`)
|
|
29
|
+
* @returns {{ moduleCalls: object[], requiredProviders: object[], requiredVersions: string[], errors: string[] }}
|
|
30
|
+
*/
|
|
31
|
+
export function parseTerraformConfig(text, { json = false } = {}) {
|
|
32
|
+
if (typeof text !== "string") {
|
|
33
|
+
return {
|
|
34
|
+
moduleCalls: [],
|
|
35
|
+
requiredProviders: [],
|
|
36
|
+
requiredVersions: [],
|
|
37
|
+
errors: ["configuration input is not text"],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return json ? parseJsonConfig(text) : parseNativeConfig(text);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse native-syntax HCL.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} text File contents
|
|
47
|
+
* @returns {{ moduleCalls: object[], requiredProviders: object[], requiredVersions: string[], errors: string[] }}
|
|
48
|
+
*/
|
|
49
|
+
function parseNativeConfig(text) {
|
|
50
|
+
const len = text.length;
|
|
51
|
+
const errors = [];
|
|
52
|
+
const moduleCalls = [];
|
|
53
|
+
const requiredProviders = [];
|
|
54
|
+
const requiredVersions = [];
|
|
55
|
+
let i = 0;
|
|
56
|
+
let line = 1;
|
|
57
|
+
|
|
58
|
+
const error = (message) => {
|
|
59
|
+
if (errors.length < 100) {
|
|
60
|
+
errors.push(`${message} (line ${line})`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const skipSpacesTabs = () => {
|
|
65
|
+
while (
|
|
66
|
+
i < len &&
|
|
67
|
+
(text[i] === " " || text[i] === "\t" || text[i] === "\r")
|
|
68
|
+
) {
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const skipLineComment = () => {
|
|
74
|
+
while (i < len && text[i] !== "\n") {
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const skipBlockComment = () => {
|
|
80
|
+
i += 2;
|
|
81
|
+
while (i < len) {
|
|
82
|
+
if (text[i] === "*" && text[i + 1] === "/") {
|
|
83
|
+
i += 2;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (text[i] === "\n") {
|
|
87
|
+
line++;
|
|
88
|
+
}
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
error("unterminated block comment");
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const skipWhitespace = () => {
|
|
95
|
+
for (;;) {
|
|
96
|
+
if (i >= len) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const c = text[i];
|
|
100
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
101
|
+
i++;
|
|
102
|
+
} else if (c === "\n") {
|
|
103
|
+
i++;
|
|
104
|
+
line++;
|
|
105
|
+
} else if (c === "#") {
|
|
106
|
+
skipLineComment();
|
|
107
|
+
} else if (c === "/" && text[i + 1] === "/") {
|
|
108
|
+
skipLineComment();
|
|
109
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
110
|
+
skipBlockComment();
|
|
111
|
+
} else {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const readIdentifier = () => {
|
|
118
|
+
if (i >= len || !IDENTIFIER_START.test(text[i])) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const start = i;
|
|
122
|
+
i++;
|
|
123
|
+
while (i < len && IDENTIFIER_PART.test(text[i])) {
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
return text.slice(start, i);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Scan a template `${…}` or `%{…}` sequence, leaving `i` after its `}`. */
|
|
130
|
+
const scanTemplate = () => {
|
|
131
|
+
i += 2;
|
|
132
|
+
let depth = 1;
|
|
133
|
+
while (i < len) {
|
|
134
|
+
const c = text[i];
|
|
135
|
+
if (c === "{") {
|
|
136
|
+
depth++;
|
|
137
|
+
i++;
|
|
138
|
+
} else if (c === "}") {
|
|
139
|
+
depth--;
|
|
140
|
+
i++;
|
|
141
|
+
if (depth === 0) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
} else if (c === '"') {
|
|
145
|
+
scanString();
|
|
146
|
+
} else {
|
|
147
|
+
if (c === "\n") {
|
|
148
|
+
line++;
|
|
149
|
+
}
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
error("unterminated template sequence");
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Scan a quoted string at `i`, leaving `i` after the closing quote. */
|
|
157
|
+
const scanString = () => {
|
|
158
|
+
i++;
|
|
159
|
+
const parts = [];
|
|
160
|
+
let literal = true;
|
|
161
|
+
while (i < len) {
|
|
162
|
+
const c = text[i];
|
|
163
|
+
if (c === '"') {
|
|
164
|
+
i++;
|
|
165
|
+
return { value: parts.join(""), literal };
|
|
166
|
+
}
|
|
167
|
+
if (c === "\n") {
|
|
168
|
+
error("unterminated quoted string");
|
|
169
|
+
return { value: parts.join(""), literal: false };
|
|
170
|
+
}
|
|
171
|
+
if (c === "\\") {
|
|
172
|
+
const next = text[i + 1];
|
|
173
|
+
if (next === undefined) {
|
|
174
|
+
i++;
|
|
175
|
+
error("unterminated quoted string");
|
|
176
|
+
return { value: parts.join(""), literal: false };
|
|
177
|
+
}
|
|
178
|
+
i = scanEscape(parts, i);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (
|
|
182
|
+
(c === "$" || c === "%") &&
|
|
183
|
+
text[i + 1] === c &&
|
|
184
|
+
text[i + 2] === "{"
|
|
185
|
+
) {
|
|
186
|
+
// `$${` and `%%{` escape the template opener.
|
|
187
|
+
parts.push(`${c}{`);
|
|
188
|
+
i += 3;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if ((c === "$" || c === "%") && text[i + 1] === "{") {
|
|
192
|
+
const start = i;
|
|
193
|
+
scanTemplate();
|
|
194
|
+
parts.push(text.slice(start, i));
|
|
195
|
+
literal = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
parts.push(c);
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
error("unterminated quoted string");
|
|
202
|
+
return { value: parts.join(""), literal: false };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/** Decode one `\x` escape into `parts`; returns the index after it. */
|
|
206
|
+
const scanEscape = (parts, at) => {
|
|
207
|
+
const next = text[at + 1];
|
|
208
|
+
if (next === "n") {
|
|
209
|
+
parts.push("\n");
|
|
210
|
+
return at + 2;
|
|
211
|
+
}
|
|
212
|
+
if (next === "r") {
|
|
213
|
+
parts.push("\r");
|
|
214
|
+
return at + 2;
|
|
215
|
+
}
|
|
216
|
+
if (next === "t") {
|
|
217
|
+
parts.push("\t");
|
|
218
|
+
return at + 2;
|
|
219
|
+
}
|
|
220
|
+
if (next === "\\" || next === '"') {
|
|
221
|
+
parts.push(next);
|
|
222
|
+
return at + 2;
|
|
223
|
+
}
|
|
224
|
+
if (next === "u" || next === "U") {
|
|
225
|
+
const digits = next === "u" ? 4 : 8;
|
|
226
|
+
let value = 0;
|
|
227
|
+
let end = at + 2;
|
|
228
|
+
let count = 0;
|
|
229
|
+
while (end < len && count < digits) {
|
|
230
|
+
const code = text.charCodeAt(end);
|
|
231
|
+
const nibble =
|
|
232
|
+
code >= 48 && code <= 57
|
|
233
|
+
? code - 48
|
|
234
|
+
: code >= 97 && code <= 102
|
|
235
|
+
? code - 87
|
|
236
|
+
: code >= 65 && code <= 70
|
|
237
|
+
? code - 55
|
|
238
|
+
: -1;
|
|
239
|
+
if (nibble < 0) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
value = value * 16 + nibble;
|
|
243
|
+
end++;
|
|
244
|
+
count++;
|
|
245
|
+
}
|
|
246
|
+
if (count === digits) {
|
|
247
|
+
// `\UFFFFFFFF` is well-formed hex but no code point; String.fromCodePoint
|
|
248
|
+
// would throw and the whole file would be lost to the outer catch.
|
|
249
|
+
if (value <= 0x10ffff) {
|
|
250
|
+
parts.push(String.fromCodePoint(value));
|
|
251
|
+
} else {
|
|
252
|
+
error("invalid unicode escape");
|
|
253
|
+
}
|
|
254
|
+
return end;
|
|
255
|
+
}
|
|
256
|
+
error("invalid unicode escape");
|
|
257
|
+
return Math.max(end, at + 2);
|
|
258
|
+
}
|
|
259
|
+
// Unknown escape: keep the character as written.
|
|
260
|
+
parts.push(next);
|
|
261
|
+
return at + 2;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/** Scan a heredoc `<<ID` / `<<-ID` including its body. */
|
|
265
|
+
const scanHeredoc = () => {
|
|
266
|
+
i += 2;
|
|
267
|
+
if (i < len && text[i] === "-") {
|
|
268
|
+
i++;
|
|
269
|
+
}
|
|
270
|
+
const start = i;
|
|
271
|
+
while (i < len && HEREDOC_MARKER.test(text[i])) {
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
274
|
+
const marker = text.slice(start, i);
|
|
275
|
+
if (!marker) {
|
|
276
|
+
error("heredoc without a marker");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
skipSpacesTabs();
|
|
280
|
+
if (i >= len) {
|
|
281
|
+
error("unterminated heredoc");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (text[i] !== "\n") {
|
|
285
|
+
// Invalid opener: stop at the end of the line and let the caller
|
|
286
|
+
// decide what the newline ends.
|
|
287
|
+
error("heredoc marker not followed by a newline");
|
|
288
|
+
skipLineComment();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
line++;
|
|
293
|
+
// The body runs to the first line that is the marker alone. The newline
|
|
294
|
+
// after that closing line is left unconsumed: it is the newline that ends
|
|
295
|
+
// the attribute the heredoc belongs to, so a caller scanning an attribute
|
|
296
|
+
// value at bracket depth zero must still see it. Consuming it made the
|
|
297
|
+
// next attribute (a module's `source` or `version` written after a
|
|
298
|
+
// `user_data = <<EOT` heredoc) part of this value, and lost it.
|
|
299
|
+
while (i < len) {
|
|
300
|
+
const lineEnd = text.indexOf("\n", i);
|
|
301
|
+
const stop = lineEnd === -1 ? len : lineEnd;
|
|
302
|
+
const body = text.slice(i, stop);
|
|
303
|
+
if (body.trim() === marker) {
|
|
304
|
+
i = stop;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
i = lineEnd === -1 ? len : lineEnd + 1;
|
|
308
|
+
if (lineEnd !== -1) {
|
|
309
|
+
line++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
error("unterminated heredoc");
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const atHeredoc = () =>
|
|
316
|
+
text[i] === "<" &&
|
|
317
|
+
text[i + 1] === "<" &&
|
|
318
|
+
(text[i + 2] === "-" || HEREDOC_MARKER.test(text[i + 2] || ""));
|
|
319
|
+
|
|
320
|
+
/** Skip a `{…}` group, aware of strings, heredocs and comments. */
|
|
321
|
+
const skipBraces = () => {
|
|
322
|
+
let depth = 0;
|
|
323
|
+
while (i < len) {
|
|
324
|
+
const c = text[i];
|
|
325
|
+
if (c === "{") {
|
|
326
|
+
depth++;
|
|
327
|
+
i++;
|
|
328
|
+
} else if (c === "}") {
|
|
329
|
+
depth--;
|
|
330
|
+
i++;
|
|
331
|
+
if (depth === 0) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
} else if (c === '"') {
|
|
335
|
+
scanString();
|
|
336
|
+
} else if (atHeredoc()) {
|
|
337
|
+
scanHeredoc();
|
|
338
|
+
} else if (c === "#") {
|
|
339
|
+
skipLineComment();
|
|
340
|
+
} else if (c === "/" && text[i + 1] === "/") {
|
|
341
|
+
skipLineComment();
|
|
342
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
343
|
+
skipBlockComment();
|
|
344
|
+
} else {
|
|
345
|
+
if (c === "\n") {
|
|
346
|
+
line++;
|
|
347
|
+
}
|
|
348
|
+
i++;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (depth > 0) {
|
|
352
|
+
error("unterminated block");
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Skip spaces and comments that cannot hide the end of the statement,
|
|
358
|
+
* starting at `at`. Returns the new index and the lines crossed.
|
|
359
|
+
*/
|
|
360
|
+
const trailingSpacesAndComments = (at) => {
|
|
361
|
+
let j = at;
|
|
362
|
+
let lines = 0;
|
|
363
|
+
for (;;) {
|
|
364
|
+
while (
|
|
365
|
+
j < len &&
|
|
366
|
+
(text[j] === " " || text[j] === "\t" || text[j] === "\r")
|
|
367
|
+
) {
|
|
368
|
+
j++;
|
|
369
|
+
}
|
|
370
|
+
if (j < len && text[j] === "#") {
|
|
371
|
+
while (j < len && text[j] !== "\n") {
|
|
372
|
+
j++;
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (j < len && text[j] === "/" && text[j + 1] === "/") {
|
|
377
|
+
while (j < len && text[j] !== "\n") {
|
|
378
|
+
j++;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
if (j < len && text[j] === "/" && text[j + 1] === "*") {
|
|
383
|
+
j += 2;
|
|
384
|
+
let closed = false;
|
|
385
|
+
while (j < len) {
|
|
386
|
+
if (text[j] === "*" && text[j + 1] === "/") {
|
|
387
|
+
j += 2;
|
|
388
|
+
closed = true;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
if (text[j] === "\n") {
|
|
392
|
+
lines++;
|
|
393
|
+
}
|
|
394
|
+
j++;
|
|
395
|
+
}
|
|
396
|
+
if (!closed) {
|
|
397
|
+
return { at: len, lines, unterminated: true };
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
return { at: j, lines, unterminated: false };
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Scan one attribute value, stopping at the newline, `}` or comma that ends
|
|
408
|
+
* it (without consuming that character).
|
|
409
|
+
*/
|
|
410
|
+
const scanExpression = () => {
|
|
411
|
+
skipWhitespace();
|
|
412
|
+
if (i >= len) {
|
|
413
|
+
error("missing expression");
|
|
414
|
+
return { kind: "other" };
|
|
415
|
+
}
|
|
416
|
+
if (text[i] === '"') {
|
|
417
|
+
const startLine = line;
|
|
418
|
+
const scanned = scanString();
|
|
419
|
+
const rest = trailingSpacesAndComments(i);
|
|
420
|
+
const j = rest.at;
|
|
421
|
+
if (
|
|
422
|
+
rest.unterminated ||
|
|
423
|
+
j >= len ||
|
|
424
|
+
text[j] === "\n" ||
|
|
425
|
+
text[j] === "}" ||
|
|
426
|
+
text[j] === ","
|
|
427
|
+
) {
|
|
428
|
+
line += rest.lines;
|
|
429
|
+
i = j;
|
|
430
|
+
return {
|
|
431
|
+
kind: "string",
|
|
432
|
+
value: scanned.value,
|
|
433
|
+
literal: scanned.literal,
|
|
434
|
+
line: startLine,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
// Content follows on the same line, so this is a larger expression;
|
|
438
|
+
// keep scanning from just after the string.
|
|
439
|
+
}
|
|
440
|
+
let depth = 0;
|
|
441
|
+
while (i < len) {
|
|
442
|
+
const c = text[i];
|
|
443
|
+
if (c === "\n") {
|
|
444
|
+
if (depth === 0) {
|
|
445
|
+
return { kind: "other" };
|
|
446
|
+
}
|
|
447
|
+
line++;
|
|
448
|
+
i++;
|
|
449
|
+
} else if (c === "(" || c === "[" || c === "{") {
|
|
450
|
+
depth++;
|
|
451
|
+
i++;
|
|
452
|
+
} else if (c === ")" || c === "]" || c === "}") {
|
|
453
|
+
if (depth === 0) {
|
|
454
|
+
return { kind: "other" };
|
|
455
|
+
}
|
|
456
|
+
depth--;
|
|
457
|
+
i++;
|
|
458
|
+
} else if (c === '"') {
|
|
459
|
+
scanString();
|
|
460
|
+
} else if (atHeredoc()) {
|
|
461
|
+
scanHeredoc();
|
|
462
|
+
} else if (c === "#") {
|
|
463
|
+
skipLineComment();
|
|
464
|
+
} else if (c === "/" && text[i + 1] === "/") {
|
|
465
|
+
skipLineComment();
|
|
466
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
467
|
+
skipBlockComment();
|
|
468
|
+
} else {
|
|
469
|
+
i++;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return { kind: "other" };
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
/** Recovery: move to the end of the current line. */
|
|
476
|
+
const skipToNewline = () => {
|
|
477
|
+
while (i < len && text[i] !== "\n") {
|
|
478
|
+
i++;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Skip the remainder of a statement whose first identifier was consumed:
|
|
484
|
+
* further labels, then either `{…}` or `= expression`.
|
|
485
|
+
*/
|
|
486
|
+
const skipRestOfStatement = () => {
|
|
487
|
+
for (;;) {
|
|
488
|
+
skipSpacesTabs();
|
|
489
|
+
if (i >= len) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const c = text[i];
|
|
493
|
+
if (c === '"') {
|
|
494
|
+
scanString();
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (c === "{") {
|
|
498
|
+
skipBraces();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (c === "=") {
|
|
502
|
+
i++;
|
|
503
|
+
scanExpression();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (IDENTIFIER_START.test(c)) {
|
|
507
|
+
readIdentifier();
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (c === "\n") {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
i++;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const parseModuleBlock = (stmtLine) => {
|
|
519
|
+
skipSpacesTabs();
|
|
520
|
+
let name = null;
|
|
521
|
+
if (i < len && text[i] === '"') {
|
|
522
|
+
const label = scanString();
|
|
523
|
+
if (label.literal) {
|
|
524
|
+
name = label.value;
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
name = readIdentifier();
|
|
528
|
+
}
|
|
529
|
+
if (!name) {
|
|
530
|
+
error("module block without a usable label");
|
|
531
|
+
skipToNewline();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
skipSpacesTabs();
|
|
535
|
+
if (text[i] !== "{") {
|
|
536
|
+
error("module block without a body");
|
|
537
|
+
skipToNewline();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
i++;
|
|
541
|
+
// `sourceLiteral`/`versionLiteral` are set only when the attribute was
|
|
542
|
+
// seen, so a merge can tell "absent" from "present but non-literal".
|
|
543
|
+
const call = { name, line: stmtLine };
|
|
544
|
+
for (;;) {
|
|
545
|
+
skipWhitespace();
|
|
546
|
+
if (i >= len) {
|
|
547
|
+
error("unterminated module block");
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
if (text[i] === "}") {
|
|
551
|
+
i++;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
const attrLine = line;
|
|
555
|
+
const attr = readIdentifier();
|
|
556
|
+
if (!attr) {
|
|
557
|
+
error("unexpected character in module block");
|
|
558
|
+
i++;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
skipSpacesTabs();
|
|
562
|
+
if (text[i] === "=") {
|
|
563
|
+
i++;
|
|
564
|
+
const expr = scanExpression();
|
|
565
|
+
if (attr === "source" || attr === "version") {
|
|
566
|
+
const key = attr;
|
|
567
|
+
if (expr.kind === "string") {
|
|
568
|
+
call[`${key}Literal`] = expr.literal;
|
|
569
|
+
call[`${key}Line`] = expr.line ?? attrLine;
|
|
570
|
+
if (expr.literal) {
|
|
571
|
+
call[key] = expr.value;
|
|
572
|
+
} else {
|
|
573
|
+
delete call[key];
|
|
574
|
+
}
|
|
575
|
+
} else if (expr.kind === "other") {
|
|
576
|
+
call[`${key}Literal`] = false;
|
|
577
|
+
call[`${key}Line`] = attrLine;
|
|
578
|
+
delete call[key];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (text[i] === "{") {
|
|
584
|
+
// Nested blocks never carry the call's own source or version.
|
|
585
|
+
skipBraces();
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
skipRestOfStatement();
|
|
589
|
+
}
|
|
590
|
+
moduleCalls.push(call);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const parseProviderObject = (entry) => {
|
|
594
|
+
for (;;) {
|
|
595
|
+
skipWhitespace();
|
|
596
|
+
if (i >= len) {
|
|
597
|
+
error("unterminated provider source object");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (text[i] === "}") {
|
|
601
|
+
i++;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
let key = null;
|
|
605
|
+
if (text[i] === '"') {
|
|
606
|
+
const label = scanString();
|
|
607
|
+
if (label.literal) {
|
|
608
|
+
key = label.value;
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
key = readIdentifier();
|
|
612
|
+
}
|
|
613
|
+
if (!key) {
|
|
614
|
+
error("provider source object entry without a key");
|
|
615
|
+
i++;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
skipSpacesTabs();
|
|
619
|
+
const c = text[i];
|
|
620
|
+
if (c !== "=" && c !== ":") {
|
|
621
|
+
error("provider source object entry without a separator");
|
|
622
|
+
skipToNewline();
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
i++;
|
|
626
|
+
skipWhitespace();
|
|
627
|
+
if (i < len && text[i] === '"') {
|
|
628
|
+
const value = scanString();
|
|
629
|
+
if (value.literal && (key === "source" || key === "version")) {
|
|
630
|
+
entry[key] = value.value;
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
skipObjectValue();
|
|
634
|
+
}
|
|
635
|
+
skipSpacesTabs();
|
|
636
|
+
if (text[i] === ",") {
|
|
637
|
+
i++;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
/** Skip an object element value up to its separating comma, newline or `}`. */
|
|
643
|
+
const skipObjectValue = () => {
|
|
644
|
+
let depth = 0;
|
|
645
|
+
while (i < len) {
|
|
646
|
+
const c = text[i];
|
|
647
|
+
if (c === ",") {
|
|
648
|
+
if (depth === 0) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
i++;
|
|
652
|
+
} else if (c === "}") {
|
|
653
|
+
if (depth === 0) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
depth--;
|
|
657
|
+
i++;
|
|
658
|
+
} else if (c === "\n") {
|
|
659
|
+
if (depth === 0) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
line++;
|
|
663
|
+
i++;
|
|
664
|
+
} else if (c === "(" || c === "[" || c === "{") {
|
|
665
|
+
depth++;
|
|
666
|
+
i++;
|
|
667
|
+
} else if (c === ")" || c === "]") {
|
|
668
|
+
if (depth > 0) {
|
|
669
|
+
depth--;
|
|
670
|
+
}
|
|
671
|
+
i++;
|
|
672
|
+
} else if (c === '"') {
|
|
673
|
+
scanString();
|
|
674
|
+
} else if (atHeredoc()) {
|
|
675
|
+
scanHeredoc();
|
|
676
|
+
} else {
|
|
677
|
+
i++;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
error("unterminated provider source object value");
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const parseRequiredProvidersBody = () => {
|
|
684
|
+
for (;;) {
|
|
685
|
+
skipWhitespace();
|
|
686
|
+
if (i >= len) {
|
|
687
|
+
error("unterminated required_providers block");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (text[i] === "}") {
|
|
691
|
+
i++;
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const attrLine = line;
|
|
695
|
+
let localName = null;
|
|
696
|
+
if (text[i] === '"') {
|
|
697
|
+
const label = scanString();
|
|
698
|
+
if (label.literal) {
|
|
699
|
+
localName = label.value;
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
localName = readIdentifier();
|
|
703
|
+
}
|
|
704
|
+
if (!localName) {
|
|
705
|
+
error("required_providers entry without a name");
|
|
706
|
+
i++;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
skipSpacesTabs();
|
|
710
|
+
if (text[i] !== "=") {
|
|
711
|
+
error("required_providers entry without a value");
|
|
712
|
+
skipToNewline();
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
i++;
|
|
716
|
+
skipWhitespace();
|
|
717
|
+
const entry = { localName, line: attrLine };
|
|
718
|
+
if (i < len && text[i] === "{") {
|
|
719
|
+
i++;
|
|
720
|
+
parseProviderObject(entry);
|
|
721
|
+
} else if (i < len && text[i] === '"') {
|
|
722
|
+
// Legacy form: the string is the version constraint.
|
|
723
|
+
const constraint = scanString();
|
|
724
|
+
if (constraint.literal) {
|
|
725
|
+
entry.version = constraint.value;
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
scanExpression();
|
|
729
|
+
}
|
|
730
|
+
requiredProviders.push(entry);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const parseTerraformBlock = () => {
|
|
735
|
+
skipSpacesTabs();
|
|
736
|
+
if (text[i] !== "{") {
|
|
737
|
+
error("terraform block without a body");
|
|
738
|
+
skipToNewline();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
i++;
|
|
742
|
+
for (;;) {
|
|
743
|
+
skipWhitespace();
|
|
744
|
+
if (i >= len) {
|
|
745
|
+
error("unterminated terraform block");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (text[i] === "}") {
|
|
749
|
+
i++;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const ident = readIdentifier();
|
|
753
|
+
if (!ident) {
|
|
754
|
+
error("unexpected character in terraform block");
|
|
755
|
+
i++;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
skipSpacesTabs();
|
|
759
|
+
if (ident === "required_version" && text[i] === "=") {
|
|
760
|
+
i++;
|
|
761
|
+
const expr = scanExpression();
|
|
762
|
+
if (expr.kind === "string" && expr.literal) {
|
|
763
|
+
requiredVersions.push(expr.value);
|
|
764
|
+
}
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (ident === "required_providers" && text[i] === "{") {
|
|
768
|
+
i++;
|
|
769
|
+
parseRequiredProvidersBody();
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (text[i] === "=") {
|
|
773
|
+
i++;
|
|
774
|
+
scanExpression();
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (text[i] === "{") {
|
|
778
|
+
skipBraces();
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
skipRestOfStatement();
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
for (;;) {
|
|
787
|
+
skipWhitespace();
|
|
788
|
+
if (i >= len) {
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
const stmtLine = line;
|
|
792
|
+
const ident = readIdentifier();
|
|
793
|
+
if (!ident) {
|
|
794
|
+
error("unexpected character at top level");
|
|
795
|
+
i++;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (ident === "module") {
|
|
799
|
+
parseModuleBlock(stmtLine);
|
|
800
|
+
} else if (ident === "terraform") {
|
|
801
|
+
parseTerraformBlock();
|
|
802
|
+
} else {
|
|
803
|
+
skipRestOfStatement();
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
errors.push("configuration scanning failed");
|
|
808
|
+
}
|
|
809
|
+
return { moduleCalls, requiredProviders, requiredVersions, errors };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Whether a JSON configuration string carries an unescaped template opener.
|
|
814
|
+
*
|
|
815
|
+
* @param {string} value Candidate literal
|
|
816
|
+
* @returns {boolean}
|
|
817
|
+
*/
|
|
818
|
+
function hasTemplate(value) {
|
|
819
|
+
return value.includes("${") || value.includes("%{");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Parse the JSON configuration variant.
|
|
824
|
+
*
|
|
825
|
+
* @param {string} text File contents
|
|
826
|
+
* @returns {{ moduleCalls: object[], requiredProviders: object[], requiredVersions: string[], errors: string[] }}
|
|
827
|
+
*/
|
|
828
|
+
function parseJsonConfig(text) {
|
|
829
|
+
const result = {
|
|
830
|
+
moduleCalls: [],
|
|
831
|
+
requiredProviders: [],
|
|
832
|
+
requiredVersions: [],
|
|
833
|
+
errors: [],
|
|
834
|
+
};
|
|
835
|
+
let doc;
|
|
836
|
+
try {
|
|
837
|
+
doc = JSON.parse(text);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
result.errors.push(`invalid JSON: ${err.message}`);
|
|
840
|
+
return result;
|
|
841
|
+
}
|
|
842
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
843
|
+
return result;
|
|
844
|
+
}
|
|
845
|
+
const moduleMaps = Array.isArray(doc.module) ? doc.module : [doc.module];
|
|
846
|
+
for (const map of moduleMaps) {
|
|
847
|
+
if (!map || typeof map !== "object") {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
for (const [name, def] of Object.entries(map)) {
|
|
851
|
+
if (name === "//" || !def || typeof def !== "object") {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const call = { name };
|
|
855
|
+
if (typeof def.source === "string") {
|
|
856
|
+
call.sourceLiteral = !hasTemplate(def.source);
|
|
857
|
+
if (call.sourceLiteral) {
|
|
858
|
+
call.source = def.source;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (typeof def.version === "string") {
|
|
862
|
+
call.versionLiteral = !hasTemplate(def.version);
|
|
863
|
+
if (call.versionLiteral) {
|
|
864
|
+
call.version = def.version;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
result.moduleCalls.push(call);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const terraformBlocks = Array.isArray(doc.terraform)
|
|
871
|
+
? doc.terraform
|
|
872
|
+
: [doc.terraform];
|
|
873
|
+
for (const block of terraformBlocks) {
|
|
874
|
+
if (!block || typeof block !== "object") {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (typeof block.required_version === "string") {
|
|
878
|
+
if (!hasTemplate(block.required_version)) {
|
|
879
|
+
result.requiredVersions.push(block.required_version);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const providers = block.required_providers;
|
|
883
|
+
if (!providers || typeof providers !== "object") {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
for (const [localName, def] of Object.entries(providers)) {
|
|
887
|
+
if (localName === "//") {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const entry = { localName };
|
|
891
|
+
if (typeof def === "string") {
|
|
892
|
+
if (!hasTemplate(def)) {
|
|
893
|
+
entry.version = def;
|
|
894
|
+
}
|
|
895
|
+
} else if (def && typeof def === "object") {
|
|
896
|
+
if (typeof def.source === "string" && !hasTemplate(def.source)) {
|
|
897
|
+
entry.source = def.source;
|
|
898
|
+
}
|
|
899
|
+
if (typeof def.version === "string" && !hasTemplate(def.version)) {
|
|
900
|
+
entry.version = def.version;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
result.requiredProviders.push(entry);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return result;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const CONFIG_EXTENSIONS = [".tf.json", ".tofu.json", ".tf", ".tofu"];
|
|
910
|
+
|
|
911
|
+
const configExtension = (baseName) => {
|
|
912
|
+
for (const ext of CONFIG_EXTENSIONS) {
|
|
913
|
+
if (baseName.endsWith(ext)) {
|
|
914
|
+
return ext;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Whether a file is a Terraform override file: base name `override` or ending
|
|
922
|
+
* `_override`, with a `.tf`/`.tf.json`/`.tofu`/`.tofu.json` extension.
|
|
923
|
+
*
|
|
924
|
+
* @param {string} fileName File name (a bare name or a path)
|
|
925
|
+
* @returns {boolean}
|
|
926
|
+
*/
|
|
927
|
+
export function isTerraformOverrideFile(fileName) {
|
|
928
|
+
if (typeof fileName !== "string") {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
const baseName = fileName.split(/[\\/]/).pop() || "";
|
|
932
|
+
const ext = configExtension(baseName);
|
|
933
|
+
if (!ext) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const stem = baseName.slice(0, -ext.length);
|
|
937
|
+
return stem === "override" || stem.endsWith("_override");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Split one directory's configuration file names into primary and override
|
|
942
|
+
* files, applying OpenTofu's `.tofu` precedence when any `.tofu` file exists.
|
|
943
|
+
*
|
|
944
|
+
* @param {string[]} fileNames File names within a single directory
|
|
945
|
+
* @returns {{ primary: string[], overrides: string[] }} Sorted lists
|
|
946
|
+
*/
|
|
947
|
+
export function terraformConfigFileSet(fileNames) {
|
|
948
|
+
const names = (fileNames || []).filter(
|
|
949
|
+
(name) =>
|
|
950
|
+
typeof name === "string" &&
|
|
951
|
+
configExtension(name.split(/[\\/]/).pop() || ""),
|
|
952
|
+
);
|
|
953
|
+
const hasTofu = names.some(
|
|
954
|
+
(name) => name.endsWith(".tofu") || name.endsWith(".tofu.json"),
|
|
955
|
+
);
|
|
956
|
+
// Stems that a `.tofu`/`.tofu.json` file replaces (`main.tofu` shadows
|
|
957
|
+
// `main.tf`; `main.tofu.json` shadows `main.tf.json`).
|
|
958
|
+
const shadowed = new Set();
|
|
959
|
+
if (hasTofu) {
|
|
960
|
+
for (const name of names) {
|
|
961
|
+
const ext = configExtension(name.split(/[\\/]/).pop() || "");
|
|
962
|
+
const stem = (name.split(/[\\/]/).pop() || "").slice(0, -ext.length);
|
|
963
|
+
if (ext === ".tofu") {
|
|
964
|
+
shadowed.add(`${stem}|.tf`);
|
|
965
|
+
} else if (ext === ".tofu.json") {
|
|
966
|
+
shadowed.add(`${stem}|.tf.json`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const keep = (name) => {
|
|
971
|
+
const baseName = name.split(/[\\/]/).pop() || "";
|
|
972
|
+
const ext = configExtension(baseName);
|
|
973
|
+
if (ext !== ".tf" && ext !== ".tf.json") {
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
return !shadowed.has(`${baseName.slice(0, -ext.length)}|${ext}`);
|
|
977
|
+
};
|
|
978
|
+
const configNames = names.filter(keep);
|
|
979
|
+
return {
|
|
980
|
+
primary: configNames
|
|
981
|
+
.filter(
|
|
982
|
+
(name) => !isTerraformOverrideFile(name.split(/[\\/]/).pop() || ""),
|
|
983
|
+
)
|
|
984
|
+
.sort(),
|
|
985
|
+
overrides: configNames
|
|
986
|
+
.filter((name) =>
|
|
987
|
+
isTerraformOverrideFile(name.split(/[\\/]/).pop() || ""),
|
|
988
|
+
)
|
|
989
|
+
.sort(),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Merge parsed configuration files (primary files first, then overrides, each
|
|
995
|
+
* group in the given order).
|
|
996
|
+
*
|
|
997
|
+
* @param {{ file: string, override: boolean, result: object }[]} parsedFiles
|
|
998
|
+
* @returns {{ moduleCalls: object[], requiredProviders: object[], requiredVersions: string[], errors: string[] }}
|
|
999
|
+
*/
|
|
1000
|
+
export function mergeTerraformConfigs(parsedFiles) {
|
|
1001
|
+
const merged = {
|
|
1002
|
+
moduleCalls: [],
|
|
1003
|
+
requiredProviders: [],
|
|
1004
|
+
requiredVersions: [],
|
|
1005
|
+
errors: [],
|
|
1006
|
+
};
|
|
1007
|
+
const moduleByName = new Map();
|
|
1008
|
+
const providerByName = new Map();
|
|
1009
|
+
const entries = Array.isArray(parsedFiles) ? parsedFiles : [];
|
|
1010
|
+
const ordered = [
|
|
1011
|
+
...entries.filter((entry) => !entry.override),
|
|
1012
|
+
...entries.filter((entry) => entry.override),
|
|
1013
|
+
];
|
|
1014
|
+
const primaryVersions = [];
|
|
1015
|
+
const overrideVersions = [];
|
|
1016
|
+
for (const { file, override, result } of ordered) {
|
|
1017
|
+
if (!result) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
merged.errors.push(...result.errors);
|
|
1021
|
+
for (const version of result.requiredVersions || []) {
|
|
1022
|
+
(override ? overrideVersions : primaryVersions).push(version);
|
|
1023
|
+
}
|
|
1024
|
+
for (const call of result.moduleCalls || []) {
|
|
1025
|
+
const existing = moduleByName.get(call.name);
|
|
1026
|
+
if (!existing) {
|
|
1027
|
+
moduleByName.set(call.name, { ...call, file });
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (override) {
|
|
1031
|
+
// Replace only the attributes the override sets, with their literal
|
|
1032
|
+
// flags and their provenance.
|
|
1033
|
+
if ("sourceLiteral" in call) {
|
|
1034
|
+
if (call.sourceLiteral) {
|
|
1035
|
+
existing.source = call.source;
|
|
1036
|
+
} else {
|
|
1037
|
+
delete existing.source;
|
|
1038
|
+
}
|
|
1039
|
+
existing.sourceLiteral = call.sourceLiteral;
|
|
1040
|
+
existing.sourceFile = file;
|
|
1041
|
+
existing.sourceLine = call.sourceLine;
|
|
1042
|
+
}
|
|
1043
|
+
if ("versionLiteral" in call) {
|
|
1044
|
+
if (call.versionLiteral) {
|
|
1045
|
+
existing.version = call.version;
|
|
1046
|
+
} else {
|
|
1047
|
+
delete existing.version;
|
|
1048
|
+
}
|
|
1049
|
+
existing.versionLiteral = call.versionLiteral;
|
|
1050
|
+
existing.versionFile = file;
|
|
1051
|
+
existing.versionLine = call.versionLine;
|
|
1052
|
+
}
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
merged.errors.push(`duplicate module call '${call.name}'`);
|
|
1056
|
+
}
|
|
1057
|
+
for (const entry of result.requiredProviders || []) {
|
|
1058
|
+
const existing = providerByName.get(entry.localName);
|
|
1059
|
+
if (!existing) {
|
|
1060
|
+
providerByName.set(entry.localName, { ...entry, file });
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (override) {
|
|
1064
|
+
providerByName.set(entry.localName, { ...entry, file });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
merged.moduleCalls = [...moduleByName.values()];
|
|
1069
|
+
merged.requiredProviders = [...providerByName.values()];
|
|
1070
|
+
const versions = overrideVersions.length ? overrideVersions : primaryVersions;
|
|
1071
|
+
merged.requiredVersions = [...new Set(versions)];
|
|
1072
|
+
return merged;
|
|
1073
|
+
}
|