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.

ResultHow foundConfidence
✅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
PatternExampleResult
Readarticle.title✅
Chainedqs.first().title✅
Multi-line chainArticle.objects.get(pk=1).title✅
Inside f-stringf"{article.title}"✅
Conditionalarticle.title if article else ""✅
List comprehension[a.title for a in qs]✅
Writearticle.title = value✅
Augmented writearticle.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.

PatternExampleResult
getattr literalgetattr(article, "title")✅ [getattr]
getattr with defaultgetattr(article, "title", "")✅ [getattr]
attrgetterattrgetter("title")(article)✅ [getattr]
operator.attrgetteroperator.attrgetter("title")✅ [getattr]
getattr with variablegetattr(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.

MethodExampleResult
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]
QQ(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.

MethodExampleResult
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.

FunctionExampleResult
FF("title"), .annotate(t=F("title"))✅ [string]
AggregatesMax("title"), Min("title"), Avg("title"), Sum("title"), Count("title"), StdDev("title"), Variance("title")✅ [string]
Database functionsCoalesce("title", Value("")), Concat("title", Value(" ")), Greatest("title", ...), Least("title", ...), NullIf("title", ...)✅ [string]
SubqueryOuterRef("title"), Subquery(...)✅ [string]
save with update_fields

The field name appears as a string element inside the update_fields list passed to Model.save().

PatternExampleResult
Single fieldarticle.save(update_fields=["title"])✅ [string]
Multiple fieldsarticle.save(update_fields=["title", "slug"])✅ [string]
Variable listarticle.save(update_fields=fields)❌ out of scope — list not statically visible
Raw SQL
MethodExampleResult
.raw()Article.objects.raw("SELECT title FROM ...")✅ [sql ref]
cursor.execute()cursor.execute("SELECT title, slug FROM ...")✅ [sql ref]

Not detected#

ORM — uncovered methods
PatternExampleResult
update_or_create.update_or_create(defaults={"title": "x"})❌
Meta API
PatternExampleResult
_meta.get_fieldArticle._meta.get_field("title")❌
vars() subscriptvars(article)["title"]❌
__dict__ subscriptarticle.__dict__["title"]❌
Django admin
PatternExampleResult
list_displaylist_display = ["title"]❌
list_filterlist_filter = ["title"]❌
search_fieldssearch_fields = ["title"]❌
readonly_fieldsreadonly_fields = ["title"]❌
fieldsetsfieldsets = (None, {"fields": ["title"]})❌
orderingordering = ["title"]❌
Django REST Framework
PatternExampleResult
Meta.fieldsfields = ["title", "slug"]❌
extra_kwargsextra_kwargs = {"title": {...}}❌
Django forms
PatternExampleResult
ModelForm.Meta.fieldsfields = ["title"]❌

Rails#

Detected#

Attribute access
PatternExampleResult
Readarticle.title✅
ChainedArticle.find(1).title✅
Multi-line chainArticle.where(...).first.title✅
String interpolation"#{article.title}"✅
Safe navigationarticle&.title✅
Writearticle.title = value✅
ActiveRecord — creation
MethodExampleResult
newArticle.new(title: value)✅ [string]
createArticle.create(title: value)✅ [string]
find_or_create_byArticle.find_or_create_by(title: value)✅ [string]
find_or_initialize_byArticle.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.

MethodExampleResult
insertArticle.insert({title: "a"})✅ [string]
insert!Article.insert!({title: "a"})✅ [string]
insert_allArticle.insert_all([{title: "a"}, ...])✅ [string]
insert_all!Article.insert_all!([{title: "a"}])✅ [string]
upsertArticle.upsert({title: "a"})✅ [string]
upsert_allArticle.upsert_all([{title: "a"}, ...])✅ [string]
Variable argumentArticle.insert_all(records)❌ out of scope — hash keys not statically visible
ActiveRecord — instance update
MethodExampleResult
updatearticle.update(title: value)✅ [string]
assign_attributesarticle.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
MethodExampleResult
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
MethodExampleResult
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
PatternExampleResult
Table subscriptArticle.arel_table[:title]✅ [string]
Arel conditionArticle.arel_table[:title].eq(value)✅ [string]
Implicit selfarel_table[:title]✅ [string]
Raw SQL
MethodExampleResult
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]
executeconnection.execute("UPDATE articles SET title = ...")✅ [sql ref]
select_allconnection.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.

PatternExampleResult
Symbol subscriptarticle[:title]✅ [symbol]
read_attributearticle.read_attribute(:title)✅ [symbol]
write_attributearticle.write_attribute(:title, value)✅ [symbol]
sendarticle.send(:title)✅ [symbol]
public_sendarticle.public_send(:title)✅ [symbol]
Variable symbolarticle.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.

PatternExampleResult
scope (body)scope :titled, ->(t) { where(title: t) } — detected via where✅ [string]

Not detected#

Model declarations
PatternExampleResult
validatesvalidates :title, presence: true❌
scope (declaration)scope :titled, ->(t) { ... }❌
Serialization / presentation
PatternExampleResult
slicearticle.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 permitparams.require(:article).permit(:title, :slug)❌
ActiveModel Serializer attributesattributes :title, :slug❌
as_json(only:) with dynamic arrayarticle.as_json(only: fields)❌
Dynamic / metaprogramming
PatternExampleResult
respond_to?article.respond_to?(:title)❌
instance_variable_getarticle.instance_variable_get(:@title)❌
attribute_changed?article.title_changed?❌
Dynamic finderArticle.find_by_title(value)❌