Skip to main content

backhopper_core/model/
symbol.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5use std::path::PathBuf;
6
7use serde::{Deserialize, Serialize};
8
9use crate::model::names::{Arity, FunctionName, Mfa, ModuleName, RecordName, TypeName};
10
11/// Where in the source a reference was extracted from. The same
12/// textual `mod:ident(...)` shape means a function call in body
13/// context and a type reference in type-attribute context, so the
14/// extractor must carry the surrounding context with each symbol it
15/// emits.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum RefContext {
18    /// Inside a function body or top-level expression.
19    Body,
20    /// Inside a `-spec`, `-callback`, `-type`, or `-opaque` attribute,
21    /// where `mod:ident(...)` is a type reference, not a function call.
22    TypeAttribute,
23    /// Inside any other attribute form (`-export`, `-define`,
24    /// `-record`, ...), where an identifier is never a local call.
25    OtherAttribute,
26    /// Inside a multi-line string in an attribute, such as the
27    /// documentation prose of `-moduledoc """ ... """`. Every byte is
28    /// string content, so nothing here is a reference of any kind.
29    AttributeString,
30}
31
32impl RefContext {
33    /// True in any attribute region; a local `name(` there is a
34    /// type name, an export entry, a macro parameter, or string
35    /// content, never a call or a clause head.
36    #[must_use]
37    pub fn is_attribute(self) -> bool {
38        !matches!(self, Self::Body)
39    }
40
41    /// True where `mod:fun(...)` may be a real call. False in type
42    /// attributes, where it names a type, and in attribute strings,
43    /// where a documentation example is not a call the patch adds.
44    #[must_use]
45    pub fn holds_calls(self) -> bool {
46        matches!(self, Self::Body | Self::OtherAttribute)
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
51#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
52#[serde(tag = "kind", rename_all = "snake_case")]
53pub enum SymbolKind {
54    Behaviour {
55        module: ModuleName,
56    },
57    Callback {
58        module: ModuleName,
59        function: FunctionName,
60        arity: Arity,
61    },
62    Function {
63        mfa: Mfa,
64    },
65    /// A function reference whose arity could not be determined from
66    /// the source line (the call's closing paren sits on a later
67    /// line). Resolves against a snapshot when *any* export carries
68    /// the name; never produces an arity mismatch.
69    FunctionAnyArity {
70        module: ModuleName,
71        function: FunctionName,
72    },
73    Macro {
74        name: String,
75    },
76    Record {
77        name: RecordName,
78    },
79    Type {
80        module: ModuleName,
81        name: TypeName,
82        arity: Arity,
83    },
84}
85
86/// Which side of the hunk a reference was extracted from. `Added`
87/// lines are requirements the patch introduces; `Context` lines are
88/// pre-existing facts about the target and never drive verdicts.
89/// `Added` orders first so a kind-level dedup keeps the stronger
90/// origin.
91#[derive(
92    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
93)]
94#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
95#[serde(rename_all = "snake_case")]
96pub enum RefOrigin {
97    #[default]
98    Added,
99    Context,
100}
101
102impl RefOrigin {
103    // &self is required by serde's skip_serializing_if predicate shape
104    #[allow(clippy::trivially_copy_pass_by_ref)]
105    #[must_use]
106    pub fn is_added(&self) -> bool {
107        matches!(self, Self::Added)
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub struct SymbolRef {
114    #[serde(flatten)]
115    pub kind: SymbolKind,
116    #[serde(default, skip_serializing_if = "RefOrigin::is_added")]
117    pub origin: RefOrigin,
118}
119
120impl SymbolRef {
121    pub fn function(mfa: Mfa) -> Self {
122        Self {
123            kind: SymbolKind::Function { mfa },
124            origin: RefOrigin::Added,
125        }
126    }
127
128    pub fn function_any_arity(module: ModuleName, function: FunctionName) -> Self {
129        Self {
130            kind: SymbolKind::FunctionAnyArity { module, function },
131            origin: RefOrigin::Added,
132        }
133    }
134
135    pub fn record(name: RecordName) -> Self {
136        Self {
137            kind: SymbolKind::Record { name },
138            origin: RefOrigin::Added,
139        }
140    }
141
142    pub fn macro_use(name: impl Into<String>) -> Self {
143        Self {
144            kind: SymbolKind::Macro { name: name.into() },
145            origin: RefOrigin::Added,
146        }
147    }
148
149    pub fn type_ref(module: ModuleName, name: TypeName, arity: Arity) -> Self {
150        Self {
151            kind: SymbolKind::Type {
152                module,
153                name,
154                arity,
155            },
156            origin: RefOrigin::Added,
157        }
158    }
159
160    #[must_use]
161    pub fn with_origin(mut self, origin: RefOrigin) -> Self {
162        self.origin = origin;
163        self
164    }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
168#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
169pub struct Reference {
170    pub symbol: SymbolRef,
171    pub from_path: PathBuf,
172    pub line: usize,
173}