Detection patterns#
colref uses static AST analysis. It can only detect patterns where the field name appears as a literal in the source. References where the field name is constructed at runtime (e.g. getattr(obj, field_name)) are out of scope by design — static analysis cannot determine what string field_name holds at runtime.
This page documents exactly which patterns are and are not detected for each ORM. The ground truth is the golden test files in test_patterns/.
Output labels#
All three mean the reference was detected. The label indicates how it was found and how confident the match is.
| Result | How found | Confidence |
|---|
| ✅ | AST attribute node (article.title) | Highest — unambiguous |
✅ [string] | Literal string or symbol passed to a known ORM method (.where(title: value), .pluck(:title)) | High — method is known to accept field names |
✅ [symbol] | Symbol literal in general Ruby accessor (article[:title], article.send(:title)) | Medium — not Rails-specific; verify manually |
✅ [getattr] | Literal string in getattr(obj, "field") or attrgetter("field") | Lower — built-in, not model-specific; verify manually |
✅ [sql ref] | Word-boundary substring match inside a raw SQL string (.where("title = ?", value)) | Lower — verify manually, false positives possible |
Django#
Detected#
Attribute access
| Pattern | Example | Result |
|---|
| Read | article.title | ✅ |
| Chained | qs.first().title | ✅ |
| Multi-line chain | Article.objects.get(pk=1).title | ✅ |
| Inside f-string | f"{article.title}" | ✅ |
| Conditional | article.title if article else "" | ✅ |
| List comprehension | [a.title for a in qs] | ✅ |
| Write | article.title = value | ✅ |
| Augmented write | article.title += " suffix" | ✅ |
colref makes no read/write distinction — both are matched as attribute nodes.
getattr / attrgetter
The field name appears as a string literal. The [getattr] label signals lower confidence because getattr and attrgetter are general Python built-ins, not model-specific calls — any object with a matching attribute name will be reported.
| Pattern | Example | Result |
|---|
getattr literal | getattr(article, "title") | ✅ [getattr] |
getattr with default | getattr(article, "title", "") | ✅ [getattr] |
attrgetter | attrgetter("title")(article) | ✅ [getattr] |
operator.attrgetter | operator.attrgetter("title") | ✅ [getattr] |
getattr with variable | getattr(article, field_name) | ❌ out of scope — field name not statically visible |
ORM — keyword argument methods
The field name appears as a keyword argument. Lookup suffixes (__icontains, __in, etc.) are stripped before matching.
| Method | Example | Result |
|---|
filter | .filter(title="x"), .filter(title__icontains="x") | ✅ [string] |
exclude | .exclude(title="x") | ✅ [string] |
get | .get(title="x") | ✅ [string] |
get_or_create | .get_or_create(title="x") | ✅ [string] |
create | .create(title="x") | ✅ [string] |
update (bulk) | .update(title="x") | ✅ [string] |
Q | Q(title="x"), Q(title__in=["x"]) | ✅ [string] |
annotate | .annotate(alias=expr) (keyword name) | ✅ [string] |
ORM — positional string argument methods
The field name appears as a positional string argument. For order_by, a leading - is stripped before matching.
| Method | Example | Result |
|---|
values | .values("title") | ✅ [string] |
values_list | .values_list("title", flat=True) | ✅ [string] |
only | .only("title") | ✅ [string] |
defer | .defer("title") | ✅ [string] |
order_by (asc) | .order_by("title") | ✅ [string] |
order_by (desc) | .order_by("-title") | ✅ [string] |
select_related | .select_related("author") | ✅ [string] |
prefetch_related | .prefetch_related("author") | ✅ [string] |
latest | .latest("title") | ✅ [string] |
earliest | .earliest("title") | ✅ [string] |
distinct (PostgreSQL) | .distinct("title") | ✅ [string] |
ORM — expression and aggregate functions
The field name appears as the first positional string argument.
| Function | Example | Result |
|---|
F | F("title"), .annotate(t=F("title")) | ✅ [string] |
| Aggregates | Max("title"), Min("title"), Avg("title"), Sum("title"), Count("title"), StdDev("title"), Variance("title") | ✅ [string] |
| Database functions | Coalesce("title", Value("")), Concat("title", Value(" ")), Greatest("title", ...), Least("title", ...), NullIf("title", ...) | ✅ [string] |
| Subquery | OuterRef("title"), Subquery(...) | ✅ [string] |
save with update_fields
The field name appears as a string element inside the update_fields list passed to Model.save().
| Pattern | Example | Result |
|---|
| Single field | article.save(update_fields=["title"]) | ✅ [string] |
| Multiple fields | article.save(update_fields=["title", "slug"]) | ✅ [string] |
| Variable list | article.save(update_fields=fields) | ❌ out of scope — list not statically visible |
Raw SQL
| Method | Example | Result |
|---|
.raw() | Article.objects.raw("SELECT title FROM ...") | ✅ [sql ref] |
cursor.execute() | cursor.execute("SELECT title, slug FROM ...") | ✅ [sql ref] |
Not detected#
ORM — uncovered methods
| Pattern | Example | Result |
|---|
update_or_create | .update_or_create(defaults={"title": "x"}) | ❌ |
Meta API
| Pattern | Example | Result |
|---|
_meta.get_field | Article._meta.get_field("title") | ❌ |
vars() subscript | vars(article)["title"] | ❌ |
__dict__ subscript | article.__dict__["title"] | ❌ |
Django admin
| Pattern | Example | Result |
|---|
list_display | list_display = ["title"] | ❌ |
list_filter | list_filter = ["title"] | ❌ |
search_fields | search_fields = ["title"] | ❌ |
readonly_fields | readonly_fields = ["title"] | ❌ |
fieldsets | fieldsets = (None, {"fields": ["title"]}) | ❌ |
ordering | ordering = ["title"] | ❌ |
Django REST Framework
| Pattern | Example | Result |
|---|
Meta.fields | fields = ["title", "slug"] | ❌ |
extra_kwargs | extra_kwargs = {"title": {...}} | ❌ |
Django forms
| Pattern | Example | Result |
|---|
ModelForm.Meta.fields | fields = ["title"] | ❌ |
Rails#
Detected#
Attribute access
| Pattern | Example | Result |
|---|
| Read | article.title | ✅ |
| Chained | Article.find(1).title | ✅ |
| Multi-line chain | Article.where(...).first.title | ✅ |
| String interpolation | "#{article.title}" | ✅ |
| Safe navigation | article&.title | ✅ |
| Write | article.title = value | ✅ |
ActiveRecord — creation
| Method | Example | Result |
|---|
new | Article.new(title: value) | ✅ [string] |
create | Article.create(title: value) | ✅ [string] |
find_or_create_by | Article.find_or_create_by(title: value) | ✅ [string] |
find_or_initialize_by | Article.find_or_initialize_by(title: value) | ✅ [string] |
ActiveRecord — bulk write (Rails 6+)
The field name appears as a hash key. The first positional argument may be a single hash (for insert, insert!, upsert) or an array of hashes (for the _all variants). Bang variants are treated identically to their non-bang counterparts.
| Method | Example | Result |
|---|
insert | Article.insert({title: "a"}) | ✅ [string] |
insert! | Article.insert!({title: "a"}) | ✅ [string] |
insert_all | Article.insert_all([{title: "a"}, ...]) | ✅ [string] |
insert_all! | Article.insert_all!([{title: "a"}]) | ✅ [string] |
upsert | Article.upsert({title: "a"}) | ✅ [string] |
upsert_all | Article.upsert_all([{title: "a"}, ...]) | ✅ [string] |
| Variable argument | Article.insert_all(records) | ❌ out of scope — hash keys not statically visible |
ActiveRecord — instance update
| Method | Example | Result |
|---|
update | article.update(title: value) | ✅ [string] |
assign_attributes | article.assign_attributes(title: value) | ✅ [string] |
update_column (symbol) | article.update_column(:title, value) | ✅ [string] |
update_columns (hash) | article.update_columns(title: value) | ✅ [string] |
ActiveRecord — query methods
| Method | Example | Result |
|---|
where (hash) | .where(title: value) | ✅ [string] |
where (string) | .where("title = ?", value) | ✅ [sql ref] |
where.not | .where.not(title: value) | ✅ [string] |
find_by | .find_by(title: value) | ✅ [string] |
exists? | .exists?(title: value) | ✅ [string] |
order (symbol) | .order(:title) | ✅ [string] |
order (hash) | .order(title: :desc) | ✅ [string] |
order (string) | .order("title ASC") | ✅ [sql ref] |
pluck (symbol) | .pluck(:title) | ✅ [string] |
pluck (string) | .pluck("title") | ✅ [string] |
select (symbol) | .select(:title) | ✅ [string] |
select (string) | .select("title, slug") | ✅ [sql ref] |
group | .group(:title) | ✅ [string] |
pick | .pick(:title) | ✅ [string] |
reorder | .reorder(:title) | ✅ [string] |
update_all | .update_all(title: value) | ✅ [string] |
ActiveRecord — aggregation
| Method | Example | Result |
|---|
minimum | .minimum(:title) | ✅ [string] |
maximum | .maximum(:title) | ✅ [string] |
sum | .sum(:title) | ✅ [string] |
average | .average(:price) | ✅ [string] |
count (column form) | .count(:status) | ✅ [string] |
calculate | .calculate(:sum, :price) | ✅ [string] (second arg) |
Arel
| Pattern | Example | Result |
|---|
| Table subscript | Article.arel_table[:title] | ✅ [string] |
| Arel condition | Article.arel_table[:title].eq(value) | ✅ [string] |
| Implicit self | arel_table[:title] | ✅ [string] |
Raw SQL
| Method | Example | Result |
|---|
find_by_sql (string) | Article.find_by_sql("SELECT title FROM articles ...") | ✅ [sql ref] |
find_by_sql (heredoc) | Article.find_by_sql(<<~SQL) with title in body | ✅ [sql ref] |
execute | connection.execute("UPDATE articles SET title = ...") | ✅ [sql ref] |
select_all | connection.select_all("SELECT title, slug FROM articles") | ✅ [sql ref] |
Hash / symbol access
The field name appears as a symbol literal. These are general Ruby patterns — not Rails-specific — so the [symbol] label signals lower confidence than [string] hits from known ORM methods. Variable symbols (article.send(field_var)) remain out of scope. send and public_send require a receiver.
| Pattern | Example | Result |
|---|
| Symbol subscript | article[:title] | ✅ [symbol] |
read_attribute | article.read_attribute(:title) | ✅ [symbol] |
write_attribute | article.write_attribute(:title, value) | ✅ [symbol] |
send | article.send(:title) | ✅ [symbol] |
public_send | article.public_send(:title) | ✅ [symbol] |
| Variable symbol | article.send(field_var) | ❌ out of scope — symbol not statically visible |
Model declarations (partial)
The scope declaration itself is not matched, but calls inside the scope body are scanned normally.
| Pattern | Example | Result |
|---|
scope (body) | scope :titled, ->(t) { where(title: t) } — detected via where | ✅ [string] |
Not detected#
Model declarations
| Pattern | Example | Result |
|---|
validates | validates :title, presence: true | ❌ |
scope (declaration) | scope :titled, ->(t) { ... } | ❌ |
Serialization / presentation
| Pattern | Example | Result |
|---|
slice | article.slice(:title, :slug) | ✅ [string] |
as_json(only:) | article.as_json(only: [:title]) | ✅ [string] |
as_json(except:) | article.as_json(except: [:created_at]) | ✅ [string] |
to_json(only:) | article.to_json(only: [:title]) | ✅ [string] |
to_xml(only:) | article.to_xml(only: [:title]) | ✅ [string] |
Strong params permit | params.require(:article).permit(:title, :slug) | ❌ |
ActiveModel Serializer attributes | attributes :title, :slug | ❌ |
as_json(only:) with dynamic array | article.as_json(only: fields) | ❌ |
Dynamic / metaprogramming
| Pattern | Example | Result |
|---|
respond_to? | article.respond_to?(:title) | ❌ |
instance_variable_get | article.instance_variable_get(:@title) | ❌ |
attribute_changed? | article.title_changed? | ❌ |
| Dynamic finder | Article.find_by_title(value) | ❌ |