Skip to content

Commit

Permalink
Add support for Terraform v1.9 (#2077)
Browse files Browse the repository at this point in the history
  • Loading branch information
wata727 authored Jul 6, 2024
1 parent 45b8f41 commit d8a4b30
Show file tree
Hide file tree
Showing 11 changed files with 997 additions and 102 deletions.
2 changes: 1 addition & 1 deletion docs/user-guide/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TFLint interprets the [Terraform language](https://developer.hashicorp.com/terra

The parser supports Terraform v1.x syntax and semantics. The language compatibility on Terraform v1.x is defined by [Compatibility Promises](https://developer.hashicorp.com/terraform/language/v1-compatibility-promises). TFLint follows this promise. New features are only supported in newer TFLint versions, and bug and experimental features compatibility are not guaranteed.

The latest supported version is Terraform v1.8.
The latest supported version is Terraform v1.9.

## Input Variables

Expand Down
112 changes: 112 additions & 0 deletions terraform/collections/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package collections

// Set represents an unordered set of values of a particular type.
//
// A caller-provided "key function" defines how to produce a comparable unique
// key for each distinct value of type T.
//
// Set operations are not concurrency-safe. Use external locking if multiple
// goroutines might modify the set concurrently or if one goroutine might
// read a set while another is modifying it.
type Set[T any] struct {
members map[UniqueKey[T]]T
key func(T) UniqueKey[T]
}

// NewSet constructs a new set whose element type knows how to calculate its own
// unique keys, by implementing [UniqueKeyer] of itself.
func NewSet[T UniqueKeyer[T]](elems ...T) Set[T] {
return NewSetFunc(T.UniqueKey, elems...)
}

// NewSetFunc constructs a new set with the given "key function".
//
// A valid key function must produce only values of types that can be compared
// for equality using the Go == operator, and must guarantee that each unique
// value of T has a corresponding key that uniquely identifies it. The
// implementer of the key function can decide what constitutes a
// "unique value of T", based on the meaning of type T.
func NewSetFunc[T any](keyFunc func(T) UniqueKey[T], elems ...T) Set[T] {
set := Set[T]{
members: make(map[UniqueKey[T]]T),
key: keyFunc,
}
for _, elem := range elems {
set.Add(elem)
}
return set
}

// NewSetCmp constructs a new set for any comparable type, using the built-in
// == operator as the definition of element equivalence.
func NewSetCmp[T comparable](elems ...T) Set[T] {
return NewSetFunc(cmpUniqueKeyFunc[T], elems...)
}

// Has returns true if the given value is present in the set, or false
// otherwise.
func (s Set[T]) Has(v T) bool {
if len(s.members) == 0 {
// We'll skip calling "s.key" in this case, so that we don't panic
// if called on an uninitialized Set.
return false
}
k := s.key(v)
_, ok := s.members[k]
return ok
}

// Add inserts new members into the set.
//
// If any existing member of the set is considered to be equivalent to a
// given value per the rules in the set's "key function", the old value will
// be discarded and replaced by the new value.
//
// If multiple of the given arguments is considered to be equivalent then
// only the later one is retained.
func (s Set[T]) Add(vs ...T) {
for _, v := range vs {
k := s.key(v)
s.members[k] = v
}
}

// Remove removes the given member from the set, or does nothing if no
// equivalent value was present.
func (s Set[T]) Remove(v T) {
k := s.key(v)
delete(s.members, k)
}

// Elems exposes the internal underlying map representation of the set
// directly, as a pragmatic compromise for efficient iteration.
//
// The result of this function is part of the internal state of the set
// and so callers MUST NOT modify it. If a caller is using locks to ensure
// safe concurrent access then any reads of the resulting map must be
// guarded by the same lock as would be used for other methods that read
// data from the set.
//
// The only correct use of this function is as part of a "for ... range"
// statement using only the values of the resulting map:
//
// for _, elem := range set.Elems() {
// // ...
// }
//
// Do not access or make any assumptions about the keys of the resulting
// map. Their exact values are an implementation detail of the set.
func (s Set[T]) Elems() map[UniqueKey[T]]T {
// This is regrettable but the only viable way to support efficient
// iteration over set members until Go gains support for range
// loops over custom iterator functions.
return s.members
}

// Len returns the number of unique elements in the set.
func (s Set[T]) Len() int {
return len(s.members)
}
53 changes: 53 additions & 0 deletions terraform/collections/set_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package collections

import (
"testing"
)

func TestSet(t *testing.T) {
set := NewSet[testingKey]()

if got, want := set.Len(), 0; got != want {
t.Errorf("wrong initial number of elements\ngot: %#v\nwant: %#v", got, want)
}

set.Add(testingKey("a"))
if got, want := set.Len(), 1; got != want {
t.Errorf("wrong number of elements after adding \"a\"\ngot: %#v\nwant: %#v", got, want)
}

set.Add(testingKey("a"))
set.Add(testingKey("b"))
if got, want := set.Len(), 2; got != want {
t.Errorf("wrong number of elements after re-adding \"a\" and adding \"b\"\ngot: %#v\nwant: %#v", got, want)
}

set.Remove(testingKey("a"))
if got, want := set.Len(), 1; got != want {
t.Errorf("wrong number of elements after removing \"a\"\ngot: %#v\nwant: %#v", got, want)
}

if got, want := set.Has(testingKey("a")), false; got != want {
t.Errorf("set still has \"a\" after removing it")
}
if got, want := set.Has(testingKey("b")), true; got != want {
t.Errorf("set doesn't have \"b\" after adding it")
}
}

func TestSetUninit(t *testing.T) {
// An zero-value set should behave like it's empty for read-only operations.
var zeroSet Set[string]
if got, want := zeroSet.Len(), 0; got != want {
t.Errorf("wrong number of elements\ngot: %d\nwant: %d", got, want)
}
if zeroSet.Has("anything") {
// (this is really just testing that we can call Has without panicking;
// it's unlikely that this would ever fail by successfully lying about
// a particular member being present.)
t.Error("Has reported that \"anything\" is present")
}
}
57 changes: 57 additions & 0 deletions terraform/collections/unique_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package collections

// UniqueKey represents a value that is comparable and uniquely identifies
// another value of type T.
//
// The Go type system offers no way to guarantee at compile time that
// implementations of this type are comparable, but if this interface is
// implemented by an uncomparable type then that will cause runtime panics
// when inserting elements into collection types that use unique keys.
//
// We use this to help with correctness of the unique-key-generator callbacks
// used with the collection types in this package, so help with type parameter
// inference and to raise compile-time errors if an inappropriate callback
// is used as the key generator for a particular collection.
type UniqueKey[T any] interface {
// Implementations must include an IsUniqueKey method with an empty body
// just as a compile-time assertion that they are intended to behave as
// unique keys for a particular other type.
//
// This method is never actually called by the collection types. Other
// callers could potentially call it, but it would be strange and pointless
// to do so.
IsUniqueKey(T)
}

// A UniqueKeyer is a type that knows how to calculate a unique key itself.
type UniqueKeyer[T any] interface {
// UniqueKey returns the unique key of the reciever.
//
// A correct implementation of UniqueKey must return a distinct value
// for each unique value of T, where the uniqueness of T values is decided
// by the implementer. See [UniqueKey] for more information.
//
// Although not enforced directly by the Go type system, it doesn't make
// sense for a type to implement [UniqueKeyer] for any type other than
// itself. Such a nonsensical implementation will not be accepted by
// functions like [NewSet] and [NewMap].
UniqueKey() UniqueKey[T]
}

// cmpUniqueKey is an annoying little adapter used to make arbitrary
// comparable types usable with [Set] and [Map].
//
// It just wraps a single-element array of T around the value, so it
// remains exactly as comparable as T. However, it does unfortunately
// mean redundantly storing T twice -- both as the unique key and the
// value -- in our collections.
type cmpUniqueKey[T comparable] [1]T

func (cmpUniqueKey[T]) IsUniqueKey(T) {}

func cmpUniqueKeyFunc[T comparable](v T) UniqueKey[T] {
return cmpUniqueKey[T]{v}
}
17 changes: 17 additions & 0 deletions terraform/collections/unique_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package collections

type testingKey string

// testingKey is its own UniqueKey, because it's already a comparable type
var _ UniqueKey[testingKey] = testingKey("")
var _ UniqueKeyer[testingKey] = testingKey("")

func (k testingKey) IsUniqueKey(testingKey) {}

// UniqueKey implements UniqueKeyer.
func (k testingKey) UniqueKey() UniqueKey[testingKey] {
return UniqueKey[testingKey](k)
}
Loading

0 comments on commit d8a4b30

Please sign in to comment.