From 0acda9077ab1325ad0035ff646c797da26cadedb Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 8 Apr 2024 15:13:47 +0200 Subject: [PATCH 1/4] Dynamic: More aggressively avoid synthetic naming --- .../ModelGeneration/ModelGeneration.expected | 5 +- shared/mad/codeql/mad/dynamic/GraphExport.qll | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/javascript/ql/test/library-tests/ModelGeneration/ModelGeneration.expected b/javascript/ql/test/library-tests/ModelGeneration/ModelGeneration.expected index d983b77b5383..42b41078bce9 100644 --- a/javascript/ql/test/library-tests/ModelGeneration/ModelGeneration.expected +++ b/javascript/ql/test/library-tests/ModelGeneration/ModelGeneration.expected @@ -7,9 +7,8 @@ typeModel | (aliases).Alias1.prototype | (aliases).Alias1 | Instance | | (aliases).Alias1.prototype | (aliases).Alias1.prototype.foo | ReturnValue | | (aliases).Alias1.prototype.foo | (aliases).Alias1.prototype | Member[foo] | -| (long-access-path).a.shortcut.d | (long-access-path) | Member[a].Member[b].Member[c].Member[d] | -| (long-access-path).a.shortcut.d | (long-access-path) | Member[a].Member[shortcut].Member[d] | -| (long-access-path).a.shortcut.d.e | (long-access-path).a.shortcut.d | Member[e] | +| (long-access-path).a.shortcut.d.e | (long-access-path) | Member[a].Member[b].Member[c].Member[d].Member[e] | +| (long-access-path).a.shortcut.d.e | (long-access-path) | Member[a].Member[shortcut].Member[d].Member[e] | | (reexport).func | (reexport) | Member[func] | | (return-this).FluentInterface | (return-this) | Member[FluentInterface] | | (return-this).FluentInterface.prototype | (return-this).FluentInterface | Instance | diff --git a/shared/mad/codeql/mad/dynamic/GraphExport.qll b/shared/mad/codeql/mad/dynamic/GraphExport.qll index e28c82f47ab3..bfb0ef7845a9 100644 --- a/shared/mad/codeql/mad/dynamic/GraphExport.qll +++ b/shared/mad/codeql/mad/dynamic/GraphExport.qll @@ -100,7 +100,9 @@ module GraphExport< private predicate exposedEdge(Node pred, string path, Node succ) { // Materialize this relation so we can access 'edge' without binding set on 'pred' pred = getAnExposedNode() and - edge(pred, path, succ) + edge(pred, path, succ) and + // Filter trivial edges at this stage (they disturb fan-in and fan-out checks) + not (pred = succ and path = "") } private Node getARelevantNode() { @@ -121,18 +123,82 @@ module GraphExport< exposedEdge(result, path, node) } + pragma[inline] + private RelevantNode getASuccessor(RelevantNode node, string path) { + exposedEdge(node, path, result) + } + private predicate hasPrettyName(RelevantNode node) { exposedName(node, _, "") or suggestedName(node, _) } + /** + * Holds if `node` has multiple predecessors, or one predecessor and an entry point. + */ + private predicate hasFanIn(RelevantNode node) { + exists(int degree | degree = strictcount(getAPredecessor(node, _)) | + degree > 1 + or + // Treat an entry point as an extra in-degree. This is needed to ensure all reachable cycles + // contain at least one node with fan-in (to ensure the node is named). + degree = 1 and + exposedName(node, _, _) + ) + } + + /** + * Holds if `succ` can be merged with `pred` as there are no other ways + * to reach it and it doesn't have a name. + */ + private predicate unificationEdge(RelevantNode pred, RelevantNode succ) { + exposedEdge(pred, "", succ) and + not hasFanIn(succ) and + not exposedName(succ, _, _) and + not S::mustBeNamed(succ) and + not suggestedName(succ, _) + or + pred = succ // ensure all nodes are assigned to an SCC + } + + private module Unification = QlBuiltins::EquivalenceRelation; + + pragma[nomagic] + private Unification::EquivalenceClass getASuccessorUnified(RelevantNode node, string path) { + result = Unification::getEquivalenceClass(getASuccessor(node, path)) + } + + /** Holds if `node` has multiple outgoing edges (after unifying equivalent successors). */ + private predicate hasFanOut(RelevantNode node) { strictcount(getASuccessorUnified(node, _)) > 1 } + + /** Holds if `node` reaches a fan-out without passing through a named node. */ + private predicate reachesFanOut(RelevantNode node) { + not exposedName(node, _, "") and + ( + hasFanOut(node) + or + // Handle the special case where a fan-in is part a cycle of nodes that all have out-degree 1. + getUniqueSuccessorIfUnnamed+(node) = node + or + reachesFanOut(getASuccessor(node, _)) + ) + } + + private RelevantNode getUniqueSuccessorIfUnnamed(RelevantNode node) { + result = getASuccessor(node, _) and + not hasFanOut(node) and // ensures uniqueness + not exposedName(node, _, "") and + not S::mustBeNamed(node) + } + private predicate nodeMustBeNamed(RelevantNode node) { exposedName(node, _, "") or S::mustBeNamed(node) or - strictcount(getAPredecessor(node, _)) > 1 + hasFanIn(node) and + reachesFanOut(node) } /** Gets a type-name to use as a prefix, in case we need to synthesize a name. */ From 686b62cc642e6ac789201319164dece862a9e288 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 9 Apr 2024 13:58:03 +0200 Subject: [PATCH 2/4] Dynamic: add debugging predicates --- shared/mad/codeql/mad/dynamic/GraphExport.qll | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/shared/mad/codeql/mad/dynamic/GraphExport.qll b/shared/mad/codeql/mad/dynamic/GraphExport.qll index bfb0ef7845a9..f9a34ffce05a 100644 --- a/shared/mad/codeql/mad/dynamic/GraphExport.qll +++ b/shared/mad/codeql/mad/dynamic/GraphExport.qll @@ -301,4 +301,30 @@ module GraphExport< kind = "type" ) } + + /** Predicates for debugging purposes. */ + private module Debug { + private predicate edgeToUnnamedNode(RelevantNode pred, RelevantNode succ) { + exposedEdge(pred, _, succ) and + not nodeMustBeNamed(succ) + } + + /** + * Holds if `node` is part of a cycle with a non-empty path string that is not broken + * by a named node -- this leads to divergence in `pathToNode`. + */ + predicate cycle(RelevantNode pred, string step, RelevantNode succ) { + exposedEdge(pred, step, succ) and + step != "" and + not nodeMustBeNamed(pred) and + not nodeMustBeNamed(succ) and + edgeToUnnamedNode*(succ, pred) + } + + /** Holds if `node` should be named could not be named. */ + predicate missingNodeName(RelevantNode node) { + nodeMustBeNamed(node) and + not exists(getNodeName(node)) + } + } } From bad9d4bd4cd6d17084d80faefdafae3e0ac72aab Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 9 Apr 2024 15:26:05 +0200 Subject: [PATCH 3/4] Ruby: sync ApiGraphModelsExport.qll --- config/identical-files.json | 4 + .../data/internal/ApiGraphModelsExport.qll | 124 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExport.qll diff --git a/config/identical-files.json b/config/identical-files.json index d810e30c0c8e..98e6492df5ee 100644 --- a/config/identical-files.json +++ b/config/identical-files.json @@ -333,6 +333,10 @@ "ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExtensions.qll", "python/ql/lib/semmle/python/frameworks/data/internal/ApiGraphModelsExtensions.qll" ], + "ApiGraphModelsExport": [ + "javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsExport.qll", + "ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExport.qll" + ], "Swift declarations test file": [ "swift/ql/test/extractor-tests/declarations/declarations.swift", "swift/ql/test/library-tests/ast/declarations.swift" diff --git a/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExport.qll b/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExport.qll new file mode 100644 index 000000000000..477c0a5d2679 --- /dev/null +++ b/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExport.qll @@ -0,0 +1,124 @@ +/** + * Contains an extension of `GraphExport` that relies on API graph specific functionality. + */ + +private import ApiGraphModels as Shared +private import codeql.mad.dynamic.GraphExport +private import ApiGraphModelsSpecific as Specific + +private module API = Specific::API; + +private import Shared + +/** + * Holds if some proper prefix of `(type, path)` evaluated to `node`, where `remainingPath` + * is bound to the suffix of `path` that was not evaluated yet. + */ +bindingset[type, path] +predicate partiallyEvaluatedModel(string type, string path, API::Node node, string remainingPath) { + exists(int n, AccessPath accessPath | + accessPath = path and + getNodeFromPath(type, accessPath, n) = node and + n > 0 and + // Note that `n < accessPath.getNumToken()` is implied by the use of strictconcat() + remainingPath = + strictconcat(int k | + k = [n .. accessPath.getNumToken() - 1] + | + accessPath.getToken(k), "." order by k + ) + ) +} + +/** + * Holds if `type` and all types leading to `type` should be re-exported. + */ +signature predicate shouldContainTypeSig(string type); + +/** + * Wrapper around `GraphExport` that also exports information about re-exported types. + * + * ### JavaScript example 1 + * For example, suppose `shouldContainType("foo")` holds, and the following is the entry point for a package `bar`: + * ```js + * // bar.js + * module.exports.xxx = require('foo'); + * ``` + * then this would generate the following type model: + * ``` + * foo; bar; Member[xxx] + * ``` + * + * ### JavaScript example 2 + * For a more complex case, suppose the following type model exists: + * ``` + * foo.XYZ; foo; Member[x].Member[y].Member[z] + * ``` + * And the package exports something that matches a prefix of the access path above: + * ```js + * module.exports.blah = require('foo').x.y; + * ``` + * This would result in the following type model: + * ``` + * foo.XYZ; bar; Member[blah].Member[z] + * ``` + * Notice that the access path `Member[blah].Member[z]` consists of an access path generated from the API + * graph, with pieces of the access path from the original type model appended to it. + */ +module TypeGraphExport< + GraphExportSig S, shouldContainTypeSig/1 shouldContainType> +{ + /** Like `shouldContainType` but includes types that lead to `type` via type models. */ + private predicate shouldContainTypeEx(string type) { + shouldContainType(type) + or + exists(string prevType | + shouldContainType(prevType) and + Shared::typeModel(prevType, type, _) + ) + } + + private module Config implements GraphExportSig { + import S + + predicate shouldContain(API::Node node) { + S::shouldContain(node) + or + exists(string type1 | shouldContainTypeEx(type1) | + ModelOutput::getATypeNode(type1).getAValueReachableFromSource() = node.asSink() + or + exists(string type2, string path | + Shared::typeModel(type1, type2, path) and + getNodeFromPath(type2, path, _).getAValueReachableFromSource() = node.asSink() + ) + ) + } + } + + private module ExportedGraph = GraphExport; + + import ExportedGraph + + /** + * Holds if `type1, type2, path` should be emitted as a type model, that is `(type2, path)` leads to an instance of `type1`. + */ + predicate typeModel(string type1, string type2, string path) { + ExportedGraph::typeModel(type1, type2, path) + or + shouldContainTypeEx(type1) and + exists(API::Node node | + // A relevant type is exported directly + Specific::sourceFlowsToSink(ModelOutput::getATypeNode(type1), node) and + ExportedGraph::pathToNode(type2, path, node) + or + // Something that leads to a relevant type, but didn't finish its access path, is exported + exists(string midType, string midPath, string remainingPath, string prefix, API::Node source | + Shared::typeModel(type1, midType, midPath) and + partiallyEvaluatedModel(midType, midPath, source, remainingPath) and + Specific::sourceFlowsToSink(source, node) and + ExportedGraph::pathToNode(type2, prefix, node) and + path = join(prefix, remainingPath) + ) + ) + } +} From 8e5d23fc1cfc12b2f8f9df2430133b2129db52bf Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 8 Apr 2024 11:53:59 +0200 Subject: [PATCH 4/4] Ruby: WIP instantiation for Ruby --- ruby/ql/lib/codeql/ruby/ApiGraphs.qll | 18 ++- .../ruby/frameworks/data/ModelsAsData.qll | 114 ++++++++++++++++++ .../data/internal/ApiGraphModelsSpecific.qll | 52 ++++++++ .../ruby/typetracking/ApiGraphShared.qll | 2 + ruby/ql/src/queries/modeling/GenerateModel.ql | 26 +++- .../utils/modeleditor/GenerateModel.expected | 5 +- 6 files changed, 208 insertions(+), 9 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/ApiGraphs.qll b/ruby/ql/lib/codeql/ruby/ApiGraphs.qll index cc887a9a05c7..580fe3de8214 100644 --- a/ruby/ql/lib/codeql/ruby/ApiGraphs.qll +++ b/ruby/ql/lib/codeql/ruby/ApiGraphs.qll @@ -1047,15 +1047,29 @@ module API { import MkShared - /** Gets the API node corresponding to the module/class object for `mod`. */ + /** Gets the API node corresponding to the module/class object for `mod`, with epsilon edges to descendent modules/classes. */ bindingset[mod] pragma[inline_late] Node getModuleNode(DataFlow::ModuleNode mod) { result = Impl::MkModuleObjectDown(mod) } - /** Gets the API node corresponding to instances of `mod`. */ + /** Gets the API node corresponding to instances of `mod`, with epsilon edges to instances of descendent modules/classes. */ bindingset[mod] pragma[inline_late] Node getModuleInstance(DataFlow::ModuleNode mod) { result = getModuleNode(mod).getInstance() } + + /** Gets the API node corresponding to instances of `mod` with epsilon edges to ancestor modules/classes. */ + bindingset[mod] + pragma[inline_late] + Node getModuleNodeUp(DataFlow::ModuleNode mod) { result = Impl::MkModuleObjectUp(mod) } + + /** Gets the API node corresponding to instances of `mod`, with epsilon edges to instances of ancestor modules/classes. */ + bindingset[mod] + pragma[inline_late] + Node getModuleInstanceUp(DataFlow::ModuleNode mod) { + result = getModuleNodeUp(mod).getInstance() + } + + import Impl } private import Internal diff --git a/ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll b/ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll index 4d57191dc1ed..ab03eba22ac1 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll @@ -60,3 +60,117 @@ private class SummarizedCallableFromModel extends SummarizedCallable { ) } } + +/** + * Specifies which parts of the API graph to export in `ModelExport`. + */ +signature module ModelExportSig { + /** + * Holds if the exported model should contain `node`, if it is publicly accessible. + * + * This ensures that all ways to access `node` will be exported in type models. + */ + predicate shouldContain(API::Node node); + + /** + * Holds if a named must be generated for `node` if it is to be included in the exported graph. + */ + default predicate mustBeNamed(API::Node node) { none() } + + /** + * Holds if the exported model should preserve all paths leading to an instance of `type`, + * including partial ones. It does not need to be closed transitively, `ModelExport` will + * extend this to include type models from which `type` can be derived. + */ + default predicate shouldContainType(string type) { none() } +} + +/** + * Module for exporting type models for a given set of nodes in the API graph. + */ +module ModelExport { + private import codeql.mad.dynamic.GraphExport + private import internal.ApiGraphModelsExport + + private module GraphExportConfig implements GraphExportSig { + predicate edge = Specific::apiGraphHasEdge/3; + + predicate shouldContain = S::shouldContain/1; + + predicate shouldNotContain(API::Node node) { + // Only export def-nodes, exclude use-nodes + node instanceof API::Internal::MkModuleObjectDown + or + node instanceof API::Internal::MkModuleInstanceDown + or + node instanceof API::Internal::MkForwardNode + or + node instanceof API::Internal::MkMethodAccessNode + } + + predicate mustBeNamed(API::Node node) { S::mustBeNamed(node) } + + predicate exposedName(API::Node node, string type, string path) { + path = "" and + exists(DataFlow::ModuleNode mod | + node = API::Internal::MkModuleObjectUp(mod) and + type = mod.getQualifiedName() + "!" + or + node = API::Internal::MkModuleInstanceUp(mod) and + type = mod.getQualifiedName() + ) + } + + private string suggestedMethodName(DataFlow::MethodNode method) { + exists(DataFlow::ModuleNode mod, string name | + method = mod.getOwnSingletonMethod(name) and + result = mod.getQualifiedName() + "." + name + or + method = mod.getOwnInstanceMethod(name) and + result = mod.getQualifiedName() + "#" + name + ) + } + + predicate suggestedName(API::Node node, string type) { + // exists(DataFlow::MethodNode method | + // node.asSink() = method.getAReturnNode() and type = suggestedMethodName(method) + "()" + // ) + none() + } + + bindingset[host] + predicate hasTypeSummary(API::Node host, string path) { + exists(string methodName | + methodReturnsReceiver(host.getMethod(methodName).asCallable()) and + path = "Method[" + methodName + "].ReturnValue" + ) + } + + pragma[nomagic] + private predicate methodReturnsReceiver(DataFlow::MethodNode func) { + getAReceiverRef(func).flowsTo(func.getAReturnNode()) + } + + pragma[nomagic] + private DataFlow::CallNode getAReceiverCall(DataFlow::MethodNode func) { + result = getAReceiverRef(func).getAMethodCall() + } + + pragma[nomagic] + private predicate callReturnsReceiver(DataFlow::CallNode call) { + methodReturnsReceiver(call.getATarget()) + } + + pragma[nomagic] + private DataFlow::LocalSourceNode getAReceiverRef(DataFlow::MethodNode func) { + result = func.getSelfParameter() + or + result = getAReceiverCall(func) and + callReturnsReceiver(result) + } + } + + private module ExportedGraph = TypeGraphExport; + + import ExportedGraph +} diff --git a/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsSpecific.qll b/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsSpecific.qll index e4359f6d4ca7..600521a5f57d 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsSpecific.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsSpecific.qll @@ -27,6 +27,7 @@ import codeql.ruby.ApiGraphs import codeql.ruby.DataFlow::DataFlow as DataFlow private import FlowSummaryImpl::Public private import codeql.ruby.dataflow.internal.DataFlowDispatch as DataFlowDispatch +import codeql.Locations // re-export Location pragma[nomagic] private predicate isUsedTopLevelConstant(string name) { @@ -248,3 +249,54 @@ predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string a } module ModelOutputSpecific { } + +/** + * Holds if the value of `source` is exposed at `sink`. + */ +bindingset[source] +predicate sourceFlowsToSink(API::Node source, API::Node sink) { + // TODO: also establish subclass relationship + source.getAValueReachableFromSource() = sink.asSink() +} + +/** + * Holds if the edge `pred -> succ` labelled with `path` exists in the API graph. + */ +bindingset[pred] +predicate apiGraphHasEdge(API::Node pred, string path, API::Node succ) { + exists(string name | + API::Internal::methodEdge(pred, name, succ) and path = "Method[" + name + "]" + ) + or + API::Internal::elementEdge(pred, succ) and path = "Element" + or + API::Internal::instanceEdge(pred, succ) and path = "Instance" + or + API::Internal::returnEdge(pred, succ) and path = "ReturnValue" + or + exists(DataFlowDispatch::ArgumentPosition pos | + not pos.isSelf() and + API::Internal::argumentEdge(pred, pos, succ) and + path = "Argument[" + FlowSummaryImpl::Input::encodeArgumentPosition(pos) + "]" + ) + or + exists(DataFlowDispatch::ParameterPosition pos | + not pos.isSelf() and + API::Internal::parameterEdge(pred, pos, succ) and + path = "Parameter[" + FlowSummaryImpl::Input::encodeParameterPosition(pos) + "]" + ) + or + path = "" and + API::Internal::epsilonEdge(pred, succ) +} + +pragma[nomagic] +private predicate inheritanceEdge(API::Node pred, API::Node succ) { + exists(DataFlow::ModuleNode mod | + pred = API::Internal::getModuleNodeUp(mod) and + succ = API::Internal::getModuleNodeUp(mod.getAnImmediateAncestor()) + or + pred = API::Internal::getModuleInstanceUp(mod) and + succ = API::Internal::getModuleInstanceUp(mod.getAnImmediateAncestor()) + ) +} diff --git a/ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll b/ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll index 7215116e8ef3..3fe19975ae22 100644 --- a/ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll +++ b/ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll @@ -144,6 +144,8 @@ module ApiGraphShared { private import Cached + predicate epsilonEdge = Cached::epsilonEdge/2; + /** Gets an API node corresponding to the end of forward-tracking to `localSource`. */ pragma[nomagic] private ApiNode forwardEndNode(DataFlow::LocalSourceNode localSource) { diff --git a/ruby/ql/src/queries/modeling/GenerateModel.ql b/ruby/ql/src/queries/modeling/GenerateModel.ql index c7811f12c66b..8b7ef14c6946 100644 --- a/ruby/ql/src/queries/modeling/GenerateModel.ql +++ b/ruby/ql/src/queries/modeling/GenerateModel.ql @@ -8,11 +8,23 @@ private import internal.Types private import internal.Summaries +private import codeql.ruby.ApiGraphs +private import codeql.ruby.DataFlow +private import codeql.ruby.frameworks.data.ModelsAsData -/** - * Holds if `(type2, path)` should be seen as an instance of `type1`. - */ -query predicate typeModel = Types::typeModel/3; +module ModelExportConfig implements ModelExportSig { + predicate shouldContain(API::Node node) { + exists(DataFlow::MethodNode method | node = method.backtrack()) + } + + predicate shouldContainType(string type) { + type = any(DataFlow::ModuleNode mod).getQualifiedName() + ["", "!"] + } +} + +module ExportedModel = ModelExport; + +query predicate typeModel = ExportedModel::typeModel/3; /** * Holds if the value at `(type, path)` should be seen as a flow @@ -35,7 +47,11 @@ query predicate sinkModel(string type, string path, string kind) { none() } * `kind` should be either `value` or `taint`, for value-preserving or taint-preserving steps, * respectively. */ -query predicate summaryModel = Summaries::summaryModel/5; +query predicate summaryModel(string type, string path, string input, string output, string kind) { + Summaries::summaryModel(type, path, input, output, kind) + or + ExportedModel::summaryModel(type, path, input, output, kind) +} /** * Holds if `path` can be substituted for a token `TypeVar[name]`. diff --git a/ruby/ql/test/query-tests/utils/modeleditor/GenerateModel.expected b/ruby/ql/test/query-tests/utils/modeleditor/GenerateModel.expected index 284c7ed17d57..08e260f1257e 100644 --- a/ruby/ql/test/query-tests/utils/modeleditor/GenerateModel.expected +++ b/ruby/ql/test/query-tests/utils/modeleditor/GenerateModel.expected @@ -1,7 +1,8 @@ sourceModel sinkModel +summaryModel +| A! | Method[new] | Argument[0] | ReturnValue | value | typeVariableModel typeModel | M1 | B | | -summaryModel -| A! | Method[new] | Argument[0] | ReturnValue | value | +| M1! | B! | |