codeterrain-terraform 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,470 @@
1
+ """Terraform for codeterrain: the definitions and references of ``.tf`` files.
2
+
3
+ Definitions are named the way Terraform code refers to them:
4
+
5
+ - ``resource "aws_s3_bucket" "logs"`` is ``aws_s3_bucket.logs``;
6
+ - ``data "aws_ami" "base"`` is ``data.aws_ami.base``;
7
+ - ``module "vpc"`` is ``module.vpc``, ``variable "region"`` is ``var.region``, an entry of a
8
+ ``locals`` block is ``local.name`` and ``output "url"`` is ``output.url``;
9
+ - an entry of ``required_providers`` is ``provider.aws``, and ``provider "aws"`` is
10
+ ``provider.aws`` too, or ``provider.aws.east`` with ``alias = "east"``.
11
+
12
+ A module call's doc is its ``source`` and ``version``, a required provider's the same; a
13
+ resource's ``provider = aws.east`` and a module's ``providers = { aws = aws.east }`` are
14
+ references to ``provider.aws.east``.
15
+
16
+ References are the expressions that name one of those (``var.region``, ``aws_s3_bucket.logs.arn``,
17
+ ``module.vpc.id``), in attribute values and inside string and heredoc templates, with the
18
+ definition they sit in as their scope. The reader is a tokenizer, not an evaluator: it follows no
19
+ module source, expands no ``count`` or ``for_each`` and reads no ``.tfvars``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ import string
26
+ import sys
27
+ import textwrap
28
+ from dataclasses import dataclass
29
+ from importlib.metadata import PackageNotFoundError, version
30
+ from typing import Any
31
+
32
+ try:
33
+ __version__ = version("codeterrain-terraform")
34
+ except PackageNotFoundError: # run from a source tree
35
+ __version__ = "0"
36
+
37
+ #: Labels each top-level block takes, and how its definition is named.
38
+ _BLOCKS = {"resource": 2, "data": 2, "module": 1, "variable": 1, "output": 1, "provider": 1}
39
+ _PREFIX = {"resource": "", "data": "data.", "module": "module.", "variable": "var.",
40
+ "output": "output.", "provider": "provider."}
41
+ #: Objects whose attributes are never a definition of this configuration.
42
+ _BUILTIN = frozenset({"each", "count", "path", "terraform", "self"})
43
+ #: A resource type: a provider name, ``_``, then the type (``aws_s3_bucket``, ``random_id``).
44
+ _RESOURCE_TYPE = re.compile(r"[a-z][a-z0-9]*_[a-z0-9_]+\Z")
45
+ #: A Terraform identifier: Unicode letters, digits, ``_`` and ``-``, not starting with a digit.
46
+ _IDENT = re.compile(r"[^\W\d][\w-]*")
47
+ #: Operators of more than one character, kept as one token so ``==`` is never read as ``=``.
48
+ _OPERATOR = re.compile(r"==|!=|<=|>=|=>|&&|\|\||\.\.\.|::")
49
+ _HEREDOC = re.compile(r"<<-?([^\W\d][\w-]*)[ \t]*\r?\n")
50
+ #: What a quoted string's single-character escapes stand for.
51
+ _ESCAPES = {"n": "\n", "r": "\r", "t": "\t", '"': '"', "\\": "\\"}
52
+ #: Deepest nesting of templates inside templates read before the file is given up.
53
+ MAX_NESTING = 64
54
+ MAX_DOC = 200
55
+
56
+
57
+ class TerraformError(ValueError):
58
+ pass
59
+
60
+
61
+ @dataclass
62
+ class Tok:
63
+ #: "ident", "str" (a literal string: its text), "punct", "mark" (a directive's ``%{`` or end)
64
+ kind: str
65
+ value: str
66
+ line: int
67
+
68
+
69
+ class _Lexer:
70
+ def __init__(self, src: str) -> None:
71
+ self.src = src
72
+ self.i = 0
73
+ self.line = 1
74
+ self.out: list[Tok] = []
75
+
76
+ def _advance(self, to: int) -> None:
77
+ self.line += self.src.count("\n", self.i, to)
78
+ self.i = to
79
+
80
+ def run(self) -> list[Tok]:
81
+ self._code(stop=None, depth=0)
82
+ return self.out
83
+
84
+ def _code(self, stop: str | None, depth: int) -> None:
85
+ """Tokens up to the ``}`` that closes an interpolation (``stop``), or the end."""
86
+ if depth > MAX_NESTING:
87
+ raise TerraformError("templates nested too deeply")
88
+ src, braces = self.src, 0
89
+ while self.i < len(src):
90
+ c = src[self.i]
91
+ if c == "\n" or c in " \t\r":
92
+ self._advance(self.i + 1)
93
+ elif c == "#" or src.startswith("//", self.i):
94
+ end = src.find("\n", self.i)
95
+ self._advance(len(src) if end < 0 else end)
96
+ elif src.startswith("/*", self.i):
97
+ end = src.find("*/", self.i + 2)
98
+ self._advance(len(src) if end < 0 else end + 2)
99
+ elif c == '"':
100
+ self._string(depth)
101
+ elif src.startswith("<<", self.i) and (m := _HEREDOC.match(src, self.i)):
102
+ self._heredoc(m, depth)
103
+ elif m := _IDENT.match(src, self.i):
104
+ self.out.append(Tok("ident", m.group(), self.line))
105
+ self._advance(m.end())
106
+ elif m := _OPERATOR.match(src, self.i):
107
+ self.out.append(Tok("punct", m.group(), self.line))
108
+ self._advance(m.end())
109
+ elif c in "{[(":
110
+ braces += c == "{"
111
+ self.out.append(Tok("punct", c, self.line))
112
+ self._advance(self.i + 1)
113
+ elif c == "}" and stop is not None and braces == 0:
114
+ self._advance(self.i + 1)
115
+ return
116
+ else:
117
+ braces -= c == "}"
118
+ self.out.append(Tok("punct", c, self.line))
119
+ self._advance(self.i + 1)
120
+
121
+ def _template(self, end: int, depth: int, quoted: bool = True) -> str:
122
+ """Read template text up to ``end`` (exclusive), or in a quoted string up to its closing
123
+ quote or the end of its line, lexing each ``${...}`` as code: the code lexer finds where
124
+ an interpolation ends, so its comments, strings and braces never end the template. The
125
+ literal text, for a block label. A backslash escapes only in a quoted string: a heredoc
126
+ keeps it as text."""
127
+ src, text = self.src, []
128
+ while self.i < end:
129
+ if quoted and src[self.i] in '"\n':
130
+ break
131
+ if src.startswith(("$${", "%%{"), self.i):
132
+ text.append(src[self.i + 1:self.i + 3])
133
+ self._advance(self.i + 3)
134
+ elif src.startswith(("${", "%{"), self.i):
135
+ directive = src[self.i] == "%"
136
+ if directive:
137
+ self.out.append(Tok("mark", "%{", self.line))
138
+ # a ``~`` strip marker (``%{~ endfor }``) is part of the opener, not code
139
+ self._advance(self.i + 2 + src.startswith("~", self.i + 2))
140
+ self._code(stop="}", depth=depth + 1)
141
+ if directive:
142
+ self.out.append(Tok("mark", "%}", self.line))
143
+ elif quoted and src[self.i] == "\\" and self.i + 1 < end:
144
+ text.append(self._escape())
145
+ else:
146
+ text.append(src[self.i])
147
+ self._advance(self.i + 1)
148
+ return "".join(text)
149
+
150
+ def _escape(self) -> str:
151
+ """The character a quoted string's escape at ``self.i`` stands for (``\\n``, ``\\t``,
152
+ ``\\u00e9``), moving past it; an unknown escape keeps the character after the backslash."""
153
+ src, i = self.src, self.i
154
+ kind = src[i + 1]
155
+ width = {"u": 4, "U": 8}.get(kind)
156
+ if width:
157
+ digits = src[i + 2:i + 2 + width]
158
+ if len(digits) == width and all(c in string.hexdigits for c in digits):
159
+ code = int(digits, 16)
160
+ if code <= sys.maxunicode and not 0xD800 <= code <= 0xDFFF: # no lone surrogate
161
+ self._advance(i + 2 + width)
162
+ return chr(code)
163
+ self._advance(i + 2)
164
+ return _ESCAPES.get(kind, kind)
165
+
166
+ def _string(self, depth: int) -> None:
167
+ line = self.line
168
+ self._advance(self.i + 1)
169
+ text = self._template(len(self.src), depth)
170
+ self.out.append(Tok("str", text, line))
171
+ if self.i < len(self.src) and self.src[self.i] == '"':
172
+ self._advance(self.i + 1)
173
+
174
+ def _heredoc(self, m: re.Match[str], depth: int) -> None:
175
+ line = self.line
176
+ self._advance(m.end())
177
+ close = re.compile(rf"^[ \t]*{re.escape(m.group(1))}[ \t]*\r?$", re.M)
178
+ found = close.search(self.src, self.i)
179
+ end = found.start() if found else len(self.src)
180
+ text = self._template(end, depth, quoted=False)
181
+ # the literal text, for a description: ``<<-`` strips the common indentation
182
+ self.out.append(Tok("str", textwrap.dedent(text).strip(), line))
183
+ if found:
184
+ self._advance(found.end())
185
+
186
+
187
+ def _punct(toks: list[Tok], i: int, value: str) -> bool:
188
+ """Whether ``toks[i]`` is the punctuation ``value``: never a string's text that reads the same
189
+ (the ``.`` that ends ``"${var.domain}."``)."""
190
+ return 0 <= i < len(toks) and toks[i].kind == "punct" and toks[i].value == value
191
+
192
+
193
+ def _entry(toks: list[Tok], i: int) -> bool:
194
+ """Whether ``toks[i]`` separates an object entry's key from its value: ``=`` or ``:``. A
195
+ block body's attributes take only ``=``."""
196
+ return _punct(toks, i, "=") or _punct(toks, i, ":")
197
+
198
+
199
+ def _mark(toks: list[Tok], i: int, value: str) -> bool:
200
+ """Whether ``toks[i]`` is where a template directive starts (``%{``) or ends (``%}``)."""
201
+ return 0 <= i < len(toks) and toks[i].kind == "mark" and toks[i].value == value
202
+
203
+
204
+ def _traversal(toks: list[Tok], i: int) -> list[str]:
205
+ """The names of ``a.b.c`` starting at ``toks[i]``, up to an index or the end."""
206
+ parts = [toks[i].value]
207
+ j = i + 1
208
+ while j + 1 < len(toks) and _punct(toks, j, ".") and toks[j + 1].kind == "ident":
209
+ parts.append(toks[j + 1].value)
210
+ j += 2
211
+ return parts
212
+
213
+
214
+ def _iterators(toks: list[Tok]) -> frozenset[str]:
215
+ """The iterator names of the file's ``dynamic`` blocks: the label (``dynamic "rule"`` gives
216
+ ``rule.value``), or the ``iterator = name`` that replaces it, written directly in that
217
+ block's body."""
218
+ found = set()
219
+ depth = 0
220
+ bodies: list[int] = [] # the brace depth of each open dynamic block's body
221
+ for i, tok in enumerate(toks):
222
+ if tok.kind == "punct" and tok.value == "{":
223
+ depth += 1
224
+ elif tok.kind == "punct" and tok.value == "}":
225
+ depth -= 1
226
+ while bodies and bodies[-1] > depth:
227
+ bodies.pop()
228
+ elif i + 2 < len(toks) and tok.kind == "ident":
229
+ if tok.value == "dynamic" and toks[i + 1].kind == "str" and _punct(toks, i + 2, "{"):
230
+ found.add(toks[i + 1].value)
231
+ bodies.append(depth + 1)
232
+ elif (
233
+ tok.value == "iterator" and bodies and bodies[-1] == depth
234
+ and _punct(toks, i + 1, "=") and toks[i + 2].kind == "ident"
235
+ and not _punct(toks, i + 3, ".")
236
+ ):
237
+ found.add(toks[i + 2].value)
238
+ return frozenset(found)
239
+
240
+
241
+ def _bound(toks: list[Tok]) -> set[int]:
242
+ """The indexes of the tokens that name a ``for`` binding rather than the configuration:
243
+ ``local`` in ``[for local in var.items : local.name]`` reads the element. An expression's
244
+ names hold from its ``:`` to its closing bracket; a template directive's from the end of
245
+ ``%{ for x in xs }`` to its ``%{ endfor }``."""
246
+ found: set[int] = set()
247
+ frames: list[set[str]] = [] # the names bound inside each open bracket
248
+ questions: list[int] = [] # the ``?`` of each open bracket still waiting for their ``:``
249
+ directives: list[set[str]] = []
250
+ # Each ``for`` whose collection is still being read: its names, its bracket depth (None: a
251
+ # directive) and the directive it opens (how many were open with it). A ``for`` nested in
252
+ # that collection stacks above it.
253
+ pending: list[tuple[set[str], int | None, int]] = []
254
+ open_marks = 0 # directives opened and not yet ended
255
+ for i, tok in enumerate(toks):
256
+ if tok.kind == "punct" and tok.value in "{[(":
257
+ frames.append(set())
258
+ questions.append(0)
259
+ elif tok.kind == "punct" and tok.value in "}])":
260
+ if frames:
261
+ closed = len(frames)
262
+ frames.pop()
263
+ questions.pop()
264
+ pending = [p for p in pending if p[1] is None or p[1] < closed]
265
+ elif tok.kind == "punct" and tok.value == "?" and questions:
266
+ questions[-1] += 1
267
+ elif tok.kind == "punct" and tok.value == ":" and questions and questions[-1]:
268
+ questions[-1] -= 1 # the ``:`` of a conditional
269
+ elif tok.kind == "punct" and tok.value == ":" and pending and pending[-1][1] == len(frames):
270
+ if frames:
271
+ frames[-1] |= pending[-1][0]
272
+ pending.pop()
273
+ elif _mark(toks, i, "%{"):
274
+ open_marks += 1
275
+ elif _mark(toks, i, "%}"):
276
+ # only the end of the ``for`` directive's own ``%{ ... }`` starts its binding
277
+ if pending and pending[-1][1] is None and pending[-1][2] == open_marks:
278
+ directives.append(pending.pop()[0])
279
+ open_marks = max(open_marks - 1, 0)
280
+ elif tok.kind == "ident" and tok.value == "endfor" and _mark(toks, i - 1, "%{"):
281
+ if directives:
282
+ directives.pop()
283
+ elif tok.kind == "ident" and tok.value == "for":
284
+ names, j = set(), i + 1
285
+ while j < len(toks) and toks[j].kind == "ident" and toks[j].value != "in":
286
+ names.add(toks[j].value)
287
+ j += 1 + _punct(toks, j + 1, ",")
288
+ if j < len(toks) and toks[j].kind == "ident" and toks[j].value == "in" and names:
289
+ if _punct(toks, i - 1, "{") or _punct(toks, i - 1, "["):
290
+ pending.append((names, len(frames), 0))
291
+ elif _mark(toks, i - 1, "%{"):
292
+ pending.append((names, None, open_marks))
293
+ elif (
294
+ tok.kind == "ident" and not _punct(toks, i - 1, ".")
295
+ and any(tok.value in names for names in (*frames, *directives))
296
+ ):
297
+ found.add(i)
298
+ return found
299
+
300
+
301
+ def _reference(parts: list[str], iterators: frozenset[str] = frozenset()) -> str | None:
302
+ head = parts[0]
303
+ if head in iterators and parts[1] in ("key", "value"):
304
+ return None
305
+ if head in ("var", "local", "module") and len(parts) >= 2:
306
+ return f"{head}.{parts[1]}"
307
+ if head == "data" and len(parts) >= 3:
308
+ return f"data.{parts[1]}.{parts[2]}"
309
+ if head not in _BUILTIN and len(parts) >= 2 and _RESOURCE_TYPE.match(head):
310
+ return f"{head}.{parts[1]}"
311
+ return None
312
+
313
+
314
+ def read(source: str) -> dict[str, Any]:
315
+ toks = _Lexer(source).run()
316
+ iterators = _iterators(toks)
317
+ bound = _bound(toks)
318
+ defs: list[dict[str, Any]] = []
319
+ refs: list[dict[str, Any]] = []
320
+ depth, nest, scope, kind = 0, 0, "", "" # nest: open ( and [ inside the block body
321
+ current: dict[str, Any] | None = None
322
+ #: ``source`` and ``version`` of a module or required provider, for its doc
323
+ settings: dict[int, dict[str, str]] = {}
324
+ first_ref = 0 # the first reference of the current block, renamed with a provider alias
325
+ required = 0 # the depth of the body of ``required_providers``, while inside it
326
+ wanted_provider: dict[str, Any] | None = None # the required provider being read
327
+ providers = 0 # the depth of the body of a module's ``providers`` map, while inside it
328
+ i = 0
329
+ while i < len(toks):
330
+ tok = toks[i]
331
+ if tok.kind == "punct":
332
+ if tok.value == "{":
333
+ depth += 1
334
+ elif tok.value == "}":
335
+ depth = max(depth - 1, 0)
336
+ if depth < required:
337
+ required = 0
338
+ if depth <= required:
339
+ wanted_provider = None
340
+ if depth < providers:
341
+ providers = 0
342
+ if depth == 0:
343
+ scope, kind, current, nest = "", "", None, 0
344
+ elif tok.value in "([":
345
+ nest += 1
346
+ elif tok.value in ")]":
347
+ nest = max(nest - 1, 0)
348
+ i += 1
349
+ continue
350
+ if depth == 0 and tok.kind == "ident":
351
+ labels = []
352
+ j = i + 1
353
+ while j < len(toks) and toks[j].kind in ("str", "ident"):
354
+ labels.append(toks[j].value)
355
+ j += 1
356
+ if _punct(toks, j, "{"):
357
+ kind = tok.value
358
+ wanted = _BLOCKS.get(kind)
359
+ if wanted is not None and len(labels) == wanted:
360
+ scope = _PREFIX[kind] + ".".join(labels)
361
+ current = {"name": scope, "kind": kind, "line": tok.line, "doc": ""}
362
+ defs.append(current)
363
+ else:
364
+ scope, current = "", None
365
+ first_ref = len(refs)
366
+ depth = 1
367
+ i = j + 1
368
+ continue
369
+ if (
370
+ kind == "terraform" and depth == 1 and nest == 0 and tok.kind == "ident"
371
+ and tok.value == "required_providers" and _punct(toks, i + 1, "{")
372
+ ):
373
+ required = 2
374
+ i += 1
375
+ continue
376
+ # A required provider: ``aws = { source = "hashicorp/aws", version = "~> 5.0" }``, or
377
+ # the older ``aws = "~> 5.0"``.
378
+ if (
379
+ required and depth == required and nest == 0 and tok.kind == "ident"
380
+ and _punct(toks, i + 1, "=")
381
+ ):
382
+ wanted_provider = {
383
+ "name": f"provider.{tok.value}", "kind": "required_provider", "line": tok.line,
384
+ "doc": "",
385
+ }
386
+ defs.append(wanted_provider)
387
+ if i + 2 < len(toks) and toks[i + 2].kind == "str":
388
+ settings[id(wanted_provider)] = {"version": toks[i + 2].value}
389
+ i += 3
390
+ else:
391
+ i += 2
392
+ continue
393
+ if (
394
+ wanted_provider is not None and depth == required + 1 and nest == 0
395
+ and tok.kind == "ident" and tok.value in ("source", "version")
396
+ and _entry(toks, i + 1) and i + 2 < len(toks) and toks[i + 2].kind == "str"
397
+ ):
398
+ settings.setdefault(id(wanted_provider), {})[tok.value] = toks[i + 2].value
399
+ i += 3
400
+ continue
401
+ # The provider configurations of a module call: ``providers = { aws = aws.east }``.
402
+ if (
403
+ providers and depth == providers and tok.kind == "ident"
404
+ and _entry(toks, i - 1)
405
+ ):
406
+ parts = _traversal(toks, i)
407
+ refs.append({"name": "provider." + ".".join(parts[:2]), "line": tok.line,
408
+ "scope": scope})
409
+ i += 2 * len(parts) - 1
410
+ continue
411
+ # An attribute: ``name =`` starting a line of the block body, outside brackets.
412
+ if (
413
+ tok.kind == "ident" and depth == 1 and nest == 0
414
+ and _punct(toks, i + 1, "=")
415
+ and (i == 0 or toks[i - 1].line < tok.line or _punct(toks, i - 1, "{"))
416
+ ):
417
+ if kind == "locals":
418
+ scope = f"local.{tok.value}"
419
+ defs.append({"name": scope, "kind": "local", "line": tok.line, "doc": ""})
420
+ i += 2
421
+ continue
422
+ if (
423
+ current is not None and tok.value == "description" and i + 2 < len(toks)
424
+ and toks[i + 2].kind == "str"
425
+ ):
426
+ current["doc"] = toks[i + 2].value[:MAX_DOC]
427
+ literal = toks[i + 2].value if i + 2 < len(toks) and toks[i + 2].kind == "str" else None
428
+ if current is not None and kind == "module" and tok.value in ("source", "version"):
429
+ if literal is not None:
430
+ settings.setdefault(id(current), {})[tok.value] = literal
431
+ elif current is not None and kind == "provider" and tok.value == "alias":
432
+ # ``provider "aws" { alias = "east" }`` is what ``provider = aws.east`` names
433
+ if literal is not None and _IDENT.fullmatch(literal):
434
+ scope = current["name"] = f"{current['name']}.{literal}"
435
+ for ref in refs[first_ref:]:
436
+ ref["scope"] = scope
437
+ elif kind in ("resource", "data") and tok.value == "provider" and i + 2 < len(toks):
438
+ if toks[i + 2].kind == "ident":
439
+ parts = _traversal(toks, i + 2)
440
+ refs.append({"name": "provider." + ".".join(parts[:2]), "line": tok.line,
441
+ "scope": scope})
442
+ i += 2 + 2 * len(parts) - 1
443
+ continue
444
+ elif kind == "module" and tok.value == "providers" and _punct(toks, i + 2, "{"):
445
+ providers = 2
446
+ if tok.kind == "ident" and not _punct(toks, i - 1, "."):
447
+ parts = _traversal(toks, i)
448
+ name = _reference(parts, iterators) if len(parts) > 1 and i not in bound else None
449
+ if name is not None:
450
+ refs.append({"name": name, "line": tok.line, "scope": scope})
451
+ i += max(1, 2 * len(parts) - 1)
452
+ continue
453
+ i += 1
454
+ for d in defs:
455
+ found = settings.get(id(d))
456
+ if found and not d["doc"]:
457
+ d["doc"] = ", ".join(
458
+ f"{key} {found[key]}" for key in ("source", "version") if key in found
459
+ )[:MAX_DOC]
460
+ return {"defs": defs, "refs": refs}
461
+
462
+
463
+ class _Language:
464
+ interface = 1
465
+ suffixes = (".tf",)
466
+ read = staticmethod(read)
467
+
468
+
469
+ #: The entry point codeterrain loads (``codeterrain.languages``: ``terraform``).
470
+ language = _Language()
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.5
2
+ Name: codeterrain-terraform
3
+ Version: 0.2.0
4
+ Summary: Terraform for codeterrain: resources, modules, providers and references of .tf files in terrain def and refs.
5
+ Project-URL: Homepage, https://github.com/benchli-sports/codeterrain
6
+ Project-URL: Issues, https://github.com/benchli-sports/codeterrain/issues
7
+ Author: Mitbolas GmbH
8
+ License: Apache-2.0
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: codeterrain>=0.3
11
+ Description-Content-Type: text/markdown
12
+
13
+ # codeterrain-terraform
14
+
15
+ Terraform for [codeterrain](https://github.com/benchli-sports/codeterrain): the definitions and
16
+ references of `.tf` files, in `terrain def` and `terrain refs`.
17
+
18
+ ```
19
+ uv tool install "<path-to-the-codeterrain-checkout>[terraform]"
20
+ terrain def logs # resource aws_s3_bucket.logs infra/storage.tf:12
21
+ terrain refs var.region # every expression that reads it, and the block it sits in
22
+ terrain def --kind module # every module call, with its file, source and version
23
+ terrain def --kind required_provider # every pinned provider and its version constraint
24
+ terrain refs provider.aws.east # the resources and module calls that select that provider
25
+ ```
26
+
27
+ Not published yet: install from a checkout of the codeterrain repository, where this pack lives under
28
+ `packages/`. Once published, `pip install "codeterrain[terraform]"` will do the same.
29
+ `terrain doctor` lists the installed packs (`language packs: terraform 0.2.0`).
30
+
31
+ Names follow Terraform's own references: `aws_s3_bucket.logs`, `data.aws_ami.base`, `module.vpc`,
32
+ `var.region`, `local.tags`, `output.url`, and `provider.aws` for both an entry of
33
+ `required_providers` (kind `required_provider`) and a `provider "aws"` block (kind `provider`;
34
+ `provider.aws.east` with `alias = "east"`). A module call's doc is its `source` and `version`, as
35
+ is a required provider's. A resource's `provider = aws.east` and a module's
36
+ `providers = { aws = aws.east }` are references to `provider.aws.east`. References are found in attribute values and inside string
37
+ and heredoc templates. The reader is a tokenizer, not an evaluator: it follows no module source,
38
+ expands no `count` or `for_each` and reads no `.tfvars`. A resource that selects no provider uses
39
+ its default one implicitly; that is not listed as a reference. `.tf.json` files are not read.
@@ -0,0 +1,5 @@
1
+ codeterrain_terraform/__init__.py,sha256=kVHa0A-B7Oy1jPZ382exmv2dUwnneF7vrRGutGYVWeU,21134
2
+ codeterrain_terraform-0.2.0.dist-info/METADATA,sha256=jubjrvWYx3g5QYq9N6wlLW9iWtWYg550jRl00aDq9N8,2320
3
+ codeterrain_terraform-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
4
+ codeterrain_terraform-0.2.0.dist-info/entry_points.txt,sha256=plsI_u-6DJsJNCzEqS1ZztnNU5lcfR3Tz6Db3BG9k9o,67
5
+ codeterrain_terraform-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [codeterrain.languages]
2
+ terraform = codeterrain_terraform:language