Skip to content

Commit

Permalink
Go: extract and expose struct tags, interface method IDs
Browse files Browse the repository at this point in the history
This enables us to distinguish all database types in QL. Previously structs with the same field names and types but differing tags, and interface types with matching method names and at least one non-exported method but declared in differing packages, were impossible or only sometimes possible to distinguish in QL. With this change these types can be distinguished, as well as permitting queries to examine struct field tags, e.g. to read JSON field name associations.
  • Loading branch information
smowton committed Sep 3, 2024
1 parent ea1870f commit 138473d
Show file tree
Hide file tree
Showing 30 changed files with 2,359 additions and 3 deletions.
546 changes: 546 additions & 0 deletions go/downgrades/e47462df302b3e58a60d2e21f99aba63f973326f/go.dbscheme

Large diffs are not rendered by default.

552 changes: 552 additions & 0 deletions go/downgrades/e47462df302b3e58a60d2e21f99aba63f973326f/old.dbscheme

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
description: Remove component-tags and interface-method-id tables
compatibility: full

component_tags.rel: delete
interface_private_method_ids.rel: delete
14 changes: 14 additions & 0 deletions go/extractor/dbscheme/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,20 @@ var ComponentTypesTable = NewTable("component_types",
EntityColumn(TypeType, "tp"),
).KeySet("parent", "index")

// ComponentTagsTable is the table associating composite types with their component types' tags
var ComponentTagsTable = NewTable("component_tags",
EntityColumn(CompositeType, "parent"),
IntColumn("index"),
StringColumn("tag"),
).KeySet("parent", "index")

// InterfacePrivateMethodIdsTable is the table associating interface types with their private method ids
var InterfacePrivateMethodIdsTable = NewTable("interface_private_method_ids",
EntityColumn(InterfaceType, "interface"),
IntColumn("index"),
StringColumn("id"),
).KeySet("interface", "index")

// ArrayLengthTable is the table associating array types with their length (represented as a string
// since Go array lengths are 64-bit and hence do not always fit into a QL integer)
var ArrayLengthTable = NewTable("array_length",
Expand Down
8 changes: 8 additions & 0 deletions go/extractor/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,7 @@ func extractType(tw *trap.Writer, tp types.Type) trap.Label {
name = ""
}
extractComponentType(tw, lbl, i, name, field.Type())
dbscheme.ComponentTagsTable.Emit(tw, lbl, i, tp.Tag(i))
}
case *types.Pointer:
kind = dbscheme.PointerType.Index()
Expand All @@ -1561,6 +1562,13 @@ func extractType(tw *trap.Writer, tp types.Type) trap.Label {
extractMethod(tw, meth)

extractComponentType(tw, lbl, i, meth.Name(), meth.Type())

// meth.Id() will be equal to meth.Name() for an exported method, or
// packge-qualified otherwise.
privateMethodId := meth.Id()
if privateMethodId != meth.Name() {
dbscheme.InterfacePrivateMethodIdsTable.Emit(tw, lbl, i, privateMethodId)
}
}
for i := 0; i < tp.NumEmbeddeds(); i++ {
component := tp.EmbeddedType(i)
Expand Down
6 changes: 6 additions & 0 deletions go/ql/lib/go.dbscheme
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ underlying_type(unique int named: @namedtype ref, int tp: @type ref);
#keyset[parent, index]
component_types(int parent: @compositetype ref, int index: int ref, string name: string ref, int tp: @type ref);

#keyset[parent, index]
component_tags(int parent: @compositetype ref, int index: int ref, string tag: string ref);

#keyset[interface, index]
interface_private_method_ids(int interface: @interfacetype ref, int index: int ref, string id: string ref);

array_length(unique int tp: @arraytype ref, string len: string ref);

type_objects(unique int tp: @type ref, int object: @object ref);
Expand Down
7 changes: 7 additions & 0 deletions go/ql/lib/semmle/go/Scopes.qll
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,13 @@ class Field extends Variable {
this = base.getField(f)
)
}

/**
* Gets the tag associated with this field, or the empty string if this field has no tag.
*/
string getTag() {
declaringType.hasOwnFieldWithTag(_, this.getName(), this.getType(), _, result)
}
}

/**
Expand Down
44 changes: 41 additions & 3 deletions go/ql/lib/semmle/go/Types.qll
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,17 @@ class StructType extends @structtype, CompositeType {
)
}

/**
* Holds if this struct contains a field `name` with type `tp` and tag `tag`;
* `isEmbedded` is true if the field is embedded.
*
* Note that this predicate does not take promoted fields into account.
*/
predicate hasOwnFieldWithTag(int i, string name, Type tp, boolean isEmbedded, string tag) {
this.hasOwnField(i, name, tp, isEmbedded) and
component_tags(this, i, tag)
}

/**
* Get a field with the name `name`; `isEmbedded` is true if the field is embedded.
*
Expand Down Expand Up @@ -569,10 +580,12 @@ class StructType extends @structtype, CompositeType {
override string pp() {
result =
"struct { " +
concat(int i, string name, Type tp |
component_types(this, i, name, tp)
concat(int i, string name, Type tp, string tag, string tagToPrint |
component_types(this, i, name, tp) and
component_tags(this, i, tag) and
(if tag = "" then tagToPrint = "" else tagToPrint = " `" + tag + "`")
|
name + " " + tp.pp(), "; " order by i
name + " " + tp.pp() + tagToPrint, "; " order by i
) + " }"
}

Expand Down Expand Up @@ -740,6 +753,31 @@ class InterfaceType extends @interfacetype, CompositeType {
exists(int i | i >= 0 | component_types(this, i, name, result))
}

/**
* Gets the type of method `id` of this interface type.
*
* This differs from `getMethodType` in that if the method is not exported, the `id`
* will be package-qualified. This means that the set of `id`s` together with any
* embedded types fully distinguishes the interface from any other, whereas the set
* of names matched by `getMethodName` may be ambiguous between interfaces with matching
* exported methods and unexported methods that have matching names but belong to
* different packages.
*
* For example, `interface { Exported() int; notExported() int }` declared in two
* different packages defines two distinct types, but they appear identical according to
* `getMethodType`.
*/
Type getMethodTypeById(string id) {
exists(int i, string name | i >= 0 |
component_types(this, i, name, result) and
(
interface_private_method_ids(this, i, id)
or
name = id and not interface_private_method_ids(this, i, _)
)
)
}

override predicate hasMethod(string m, SignatureType t) { t = this.getMethodType(m) }

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class StructType extends @structtype {
string toString() { result = "struct type" }
}

from StructType st, int index
where component_types(st, index, _, _)
select st, index, ""
Loading

0 comments on commit 138473d

Please sign in to comment.