mirror of
https://github.com/github/codeql.git
synced 2026-05-03 12:45:27 +02:00
Ruby: Model ActiveRecord associations
This commit is contained in:
@@ -516,3 +516,175 @@ private module Persistence {
|
||||
override DataFlow::Node getValue() { assignNode.getRhs() = result.asExpr() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A method call inside an ActiveRecord model class that establishes an
|
||||
* association between this model and another model.
|
||||
*
|
||||
* ```rb
|
||||
* class User
|
||||
* has_many :posts
|
||||
* has_one :profile
|
||||
* end
|
||||
* ```
|
||||
*/
|
||||
private class ActiveRecordAssociation extends DataFlow::CallNode {
|
||||
private ActiveRecordModelClass modelClass;
|
||||
|
||||
ActiveRecordAssociation() {
|
||||
not exists(this.asExpr().getExpr().getEnclosingMethod()) and
|
||||
this.asExpr().getExpr().getEnclosingModule() = modelClass and
|
||||
this.getMethodName() = ["has_one", "has_many", "belongs_to", "has_and_belongs_to_many"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class which declares this association.
|
||||
* For example, in
|
||||
* ```rb
|
||||
* class User
|
||||
* has_many :posts
|
||||
* end
|
||||
* ```
|
||||
* the source class is `User`.
|
||||
*/
|
||||
ActiveRecordModelClass getSourceClass() { result = modelClass }
|
||||
|
||||
/**
|
||||
* Gets the class which this association refers to.
|
||||
* For example, in
|
||||
* ```rb
|
||||
* class User
|
||||
* has_many :posts
|
||||
* end
|
||||
* ```
|
||||
* the target class is `Post`.
|
||||
*/
|
||||
ActiveRecordModelClass getTargetClass() {
|
||||
result.getName().toLowerCase() = this.getTargetModelName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the (lowercase) name of the model this association targets.
|
||||
* For example, in `has_many :posts`, this is `post`.
|
||||
*/
|
||||
string getTargetModelName() {
|
||||
exists(string s |
|
||||
s = this.getArgument(0).asExpr().getExpr().getConstantValue().getStringlikeValue()
|
||||
|
|
||||
// has_one :profile
|
||||
// belongs_to :user
|
||||
this.isSingular() and
|
||||
result = s
|
||||
or
|
||||
// has_many :posts
|
||||
// has_many :stories
|
||||
this.isCollection() and
|
||||
pluralize(result) = s
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if this association is one-to-one */
|
||||
predicate isSingular() { this.getMethodName() = ["has_one", "belongs_to"] }
|
||||
|
||||
/** Holds if this association is one-to-many or many-to-many */
|
||||
predicate isCollection() { this.getMethodName() = ["has_many", "has_and_belongs_to_many"] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `input` to plural form.
|
||||
*/
|
||||
bindingset[input]
|
||||
bindingset[result]
|
||||
private string pluralize(string input) {
|
||||
exists(string stem | stem + "y" = input | result = stem + "ies")
|
||||
or
|
||||
result = input + "s"
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a method generated by an ActiveRecord association.
|
||||
* These yield ActiveRecord collection proxies, which act like collections but
|
||||
* add some additional methods.
|
||||
* We exclude `<model>_changed?` and `<model>_previously_changed?` because these
|
||||
* do not yield ActiveRecord instances.
|
||||
* https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
|
||||
*/
|
||||
private class ActiveRecordAssociationMethodCall extends DataFlow::CallNode {
|
||||
ActiveRecordAssociation assoc;
|
||||
|
||||
ActiveRecordAssociationMethodCall() {
|
||||
exists(string model | model = assoc.getTargetModelName() |
|
||||
this.getReceiver().(ActiveRecordInstance).getClass() = assoc.getSourceClass() and
|
||||
(
|
||||
assoc.isCollection() and
|
||||
(
|
||||
this.getMethodName() = pluralize(model) + ["", "=", "<<"]
|
||||
or
|
||||
this.getMethodName() = model + ["_ids", "_ids="]
|
||||
)
|
||||
or
|
||||
assoc.isSingular() and
|
||||
(
|
||||
this.getMethodName() = model + ["", "="] or
|
||||
this.getMethodName() = ["build_", "reload_"] + model or
|
||||
this.getMethodName() = "create_" + model + ["!", ""]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ActiveRecordAssociation getAssociation() { result = assoc }
|
||||
}
|
||||
|
||||
/**
|
||||
* A method call on an ActiveRecord collection proxy that yields one or more
|
||||
* ActiveRecord instances.
|
||||
* Example:
|
||||
* ```rb
|
||||
* class User < ActiveRecord::Base
|
||||
* has_many :posts
|
||||
* end
|
||||
*
|
||||
* User.new.posts.create
|
||||
* ```
|
||||
* https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
|
||||
*/
|
||||
private class ActiveRecordCollectionProxyMethodCall extends DataFlow::CallNode {
|
||||
ActiveRecordCollectionProxyMethodCall() {
|
||||
this.getMethodName() =
|
||||
[
|
||||
"push", "concat", "build", "create", "create!", "delete", "delete_all", "destroy",
|
||||
"destroy_all", "find", "distinct", "reset", "reload"
|
||||
] and
|
||||
(
|
||||
this.getReceiver().(ActiveRecordAssociationMethodCall).getAssociation().isCollection()
|
||||
or
|
||||
exists(ActiveRecordCollectionProxyMethodCall receiver | receiver = this.getReceiver() |
|
||||
receiver.getAssociation().isCollection() and
|
||||
receiver.getMethodName() = ["reset", "reload", "distinct"]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ActiveRecordAssociation getAssociation() {
|
||||
result = this.getReceiver().(ActiveRecordAssociationMethodCall).getAssociation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to an association method which yields ActiveRecord instances.
|
||||
*/
|
||||
private class ActiveRecordAssociationModelInstantiation extends ActiveRecordModelInstantiation instanceof ActiveRecordAssociationMethodCall {
|
||||
override ActiveRecordModelClass getClass() {
|
||||
result = this.(ActiveRecordAssociationMethodCall).getAssociation().getTargetClass()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a method on a collection proxy which yields ActiveRecord instances.
|
||||
*/
|
||||
private class ActiveRecordCollectionProxyModelInstantiation extends ActiveRecordModelInstantiation instanceof ActiveRecordCollectionProxyMethodCall {
|
||||
override ActiveRecordModelClass getClass() {
|
||||
result = this.(ActiveRecordCollectionProxyMethodCall).getAssociation().getTargetClass()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user