1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Regression tests pinning PR #354: a protocol named in an **impl position**
//! may be qualified, and must resolve through its own namespace.
//!
//! `defrecord`/`deftype`/`reify`/`extend-type`/`extend-protocol` used to look
//! the protocol up with `lookup_in_ns(current_ns, "mp/IThing")` — passing the
//! whole symbol string. A qualified name is neither interned nor referred under
//! that string in the current ns, so a cross-namespace impl failed with
//! "mp/IThing is not a protocol" even though the protocol was loaded and
//! `(resolve 'mini.proto/IThing)` was truthy. That sinks every design where the
//! protocol and its implementations live in different namespaces — which is
//! most of them.
//!
//! `resolve_protocol_sym` fixed it, but its three call sites were lost across a
//! `main` merge, leaving the helper dead and the bug back. Nothing caught that,
//! because nothing tested it. These tests are that test.
use std::sync::Arc;
use cljrs_reader::Parser;
use cljrs_runtime::env::env::{Env, GlobalEnv};
use cljrs_value::Value;
fn make_env() -> (Arc<GlobalEnv>, Env) {
let globals = cljrs_runtime::Runtime::builder()
.execution_mode(cljrs_runtime::ExecutionMode::TreeWalk)
.build()
.expect("runtime")
.into_globals();
let env = Env::new(globals.clone(), "user");
(globals, env)
}
/// Evaluate `src` and return the last value rendered with `pr-str`.
fn eval_pr(src: &str) -> String {
let (_globals, mut env) = make_env();
let mut parser = Parser::new(src.to_string(), "<test>".to_string());
let forms = parser.parse_all().expect("parse error");
let mut result = Value::Nil;
for form in forms {
result = cljrs_runtime::interp::eval::eval(&form, &mut env).expect("eval error");
}
match result {
Value::Str(s) => s.get().as_str().to_string(),
// The type name, not `{:?}`: a `Value` may be a `Uuid`, and CodeQL
// reads Debug-formatting one into a panic as logging it in cleartext.
// Which type came back instead of a string is what this assertion is
// actually about.
other => panic!("expected a string from pr-str, got a {}", other.type_name()),
}
}
/// A protocol defined in `mini.proto`, with the caller back in `user` and
/// `mp` aliased to it — the shape every port/adapter design takes.
const PRELUDE: &str = "(ns mini.proto)
(defprotocol IThing (-describe [this]))
(ns user)
(alias 'mp 'mini.proto)
";
// ── The three impl sites ─────────────────────────────────────────────────────
#[test]
fn defrecord_implements_a_protocol_from_another_namespace() {
assert_eq!(
eval_pr(&format!(
"{PRELUDE}
(defrecord R [n] mp/IThing (-describe [_] [:record n]))
(pr-str (mp/-describe (->R 1)))"
)),
"[:record 1]"
);
}
#[test]
fn deftype_implements_a_protocol_from_another_namespace() {
assert_eq!(
eval_pr(&format!(
"{PRELUDE}
(deftype T [n] mp/IThing (-describe [_] [:type n]))
(pr-str (mp/-describe (->T 2)))"
)),
"[:type 2]"
);
}
#[test]
fn reify_implements_a_protocol_from_another_namespace() {
assert_eq!(
eval_pr(&format!(
"{PRELUDE}
(pr-str (mp/-describe (reify mp/IThing (-describe [_] :reified))))"
)),
":reified"
);
}
#[test]
fn extend_type_names_a_protocol_from_another_namespace() {
assert_eq!(
eval_pr(&format!(
"{PRELUDE}
(extend-type String mp/IThing (-describe [s] [:string s]))
(pr-str (mp/-describe \"hi\"))"
)),
"[:string \"hi\"]"
);
}
#[test]
fn extend_protocol_names_a_protocol_from_another_namespace() {
assert_eq!(
eval_pr(&format!(
"{PRELUDE}
(extend-protocol mp/IThing Long (-describe [n] [:long n]))
(pr-str (mp/-describe 7))"
)),
"[:long 7]"
);
}
// ── Fully qualified, no alias ────────────────────────────────────────────────
#[test]
fn a_fully_qualified_protocol_name_resolves_without_an_alias() {
assert_eq!(
eval_pr(
"(ns mini.proto)
(defprotocol IThing (-describe [this]))
(ns user)
(defrecord R [] mini.proto/IThing (-describe [_] :qualified))
(pr-str (mini.proto/-describe (->R)))"
),
":qualified"
);
}
// ── The unqualified case still resolves in the current ns ────────────────────
#[test]
fn an_unqualified_protocol_name_still_resolves_in_the_current_ns() {
assert_eq!(
eval_pr(
"(defprotocol P (-describe [this]))
(defrecord R [] P (-describe [_] :same-ns))
(pr-str (-describe (->R)))"
),
":same-ns"
);
}