use std::collections::BTreeSet;
#[derive(Clone, Copy, Debug)]
pub(crate) struct Section {
pub(crate) path: &'static str,
text: &'static str,
from: Option<&'static str>,
until: Option<&'static str>,
}
impl Section {
pub(crate) const fn file(path: &'static str, text: &'static str) -> Self {
Self {
path,
text,
from: None,
until: None,
}
}
pub(crate) const fn between(
path: &'static str,
text: &'static str,
from: &'static str,
until: Option<&'static str>,
) -> Self {
Self {
path,
text,
from: Some(from),
until,
}
}
fn text(&self) -> &'static str {
let start = self.from.map_or(0, |marker| {
self.text
.find(marker)
.unwrap_or_else(|| panic!("{}: section marker {marker:?} not found", self.path))
});
let rest = &self.text[start..];
let end = self.until.map_or(rest.len(), |marker| {
rest.find(marker)
.unwrap_or_else(|| panic!("{}: section end {marker:?} not found", self.path))
});
&rest[..end]
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Census {
pub(crate) kinds: BTreeSet<String>,
pub(crate) computed: usize,
pub(crate) every: bool,
}
impl Census {
pub(crate) fn of(sections: &[Section]) -> Self {
let mut census = Self::default();
for section in sections {
census.scan(&lex(section.text()));
}
census
}
fn scan(&mut self, tokens: &[Tok]) {
let tokens = strip_test_items(tokens);
for i in 0..tokens.len() {
let Some(method) = store_call(&tokens, i) else {
continue;
};
let args = call_args(&tokens, i + 2);
match method {
"list" | "list_at_revision" | "list_page_at_revision" => {
match args.get(2).and_then(|arg| single_str(arg)) {
Some(kind) => {
self.kinds.insert(kind.to_owned());
}
None => self.computed += 1,
}
}
"get" => match args.first().and_then(|key| keyed_kind(&tokens, i, key)) {
Some(kind) => {
self.kinds.insert(kind);
}
None => self.computed += 1,
},
"current_catalog" => self.every = true,
_ => {}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Tok {
Ident(String),
Str(String),
Punct(char),
Other,
}
pub(crate) fn lex(src: &str) -> Vec<Tok> {
let chars: Vec<char> = src.chars().collect();
let mut out = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
let next = chars.get(i + 1).copied();
if c.is_whitespace() {
i += 1;
} else if c == '/' && next == Some('/') {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
} else if c == '/' && next == Some('*') {
i = skip_block_comment(&chars, i);
} else if c == '"' {
let (text, end) = read_str(&chars, i + 1);
out.push(Tok::Str(text));
i = end;
} else if let Some((tok, end)) = (c == 'r' || c == 'b')
.then(|| raw_or_byte_string(&chars, i))
.flatten()
{
out.push(tok);
i = end;
} else if c == '\'' {
i = skip_char_or_lifetime(&chars, i);
out.push(Tok::Other);
} else if c.is_alphabetic() || c == '_' {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
out.push(Tok::Ident(chars[start..i].iter().collect()));
} else if c.is_ascii_digit() {
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
out.push(Tok::Other);
} else {
out.push(Tok::Punct(c));
i += 1;
}
}
out
}
fn skip_block_comment(chars: &[char], start: usize) -> usize {
let mut depth = 0usize;
let mut i = start;
while i < chars.len() {
if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
depth += 1;
i += 2;
} else if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
depth -= 1;
i += 2;
if depth == 0 {
return i;
}
} else {
i += 1;
}
}
i
}
fn read_str(chars: &[char], mut i: usize) -> (String, usize) {
let mut text = String::new();
while i < chars.len() {
match chars[i] {
'\\' => {
if let Some(escaped) = chars.get(i + 1) {
text.push(*escaped);
}
i += 2;
}
'"' => return (text, i + 1),
c => {
text.push(c);
i += 1;
}
}
}
(text, i)
}
fn raw_or_byte_string(chars: &[char], i: usize) -> Option<(Tok, usize)> {
let mut j = i;
if chars.get(j) == Some(&'b') {
j += 1;
}
let raw = chars.get(j) == Some(&'r');
if raw {
j += 1;
}
if i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
return None;
}
if !raw {
return (j > i && chars.get(j) == Some(&'"')).then(|| {
let (text, end) = read_str(chars, j + 1);
(Tok::Str(text), end)
});
}
let hashes = chars[j..].iter().take_while(|c| **c == '#').count();
j += hashes;
if chars.get(j) != Some(&'"') {
return None;
}
let body = j + 1;
let mut k = body;
while k < chars.len() {
if chars[k] == '"' && chars[k + 1..].iter().take(hashes).all(|c| *c == '#') {
let text: String = chars[body..k].iter().collect();
return Some((Tok::Str(text), k + 1 + hashes));
}
k += 1;
}
Some((Tok::Other, chars.len()))
}
fn skip_char_or_lifetime(chars: &[char], i: usize) -> usize {
match (chars.get(i + 1), chars.get(i + 2)) {
(Some('\\'), _) => {
let mut j = i + 3;
while j < chars.len() && chars[j] != '\'' {
j += 1;
}
j + 1
}
(Some(_), Some('\'')) => i + 3,
_ => {
let mut j = i + 1;
while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') {
j += 1;
}
j
}
}
}
pub(crate) fn strip_test_items(tokens: &[Tok]) -> Vec<Tok> {
let mut out = Vec::with_capacity(tokens.len());
let mut i = 0;
while i < tokens.len() {
if is_cfg_test(tokens, i) {
i = end_of_item(tokens, i + 7);
} else {
out.push(tokens[i].clone());
i += 1;
}
}
out
}
fn is_cfg_test(tokens: &[Tok], i: usize) -> bool {
let want = [
Tok::Punct('#'),
Tok::Punct('['),
Tok::Ident("cfg".into()),
Tok::Punct('('),
Tok::Ident("test".into()),
Tok::Punct(')'),
Tok::Punct(']'),
];
tokens.get(i..i + want.len()) == Some(&want[..])
}
fn end_of_item(tokens: &[Tok], mut i: usize) -> usize {
let mut depth = 0usize;
while i < tokens.len() {
match tokens[i] {
Tok::Punct('(' | '[') => depth += 1,
Tok::Punct(')' | ']') => depth = depth.saturating_sub(1),
Tok::Punct(';') if depth == 0 => return i + 1,
Tok::Punct('{') if depth == 0 => return matching_close(tokens, i) + 1,
_ => {}
}
i += 1;
}
i
}
fn matching_close(tokens: &[Tok], open: usize) -> usize {
let mut depth = 0usize;
for (i, tok) in tokens.iter().enumerate().skip(open) {
match tok {
Tok::Punct('(' | '[' | '{') => depth += 1,
Tok::Punct(')' | ']' | '}') => {
depth -= 1;
if depth == 0 {
return i;
}
}
_ => {}
}
}
tokens.len()
}
pub(crate) fn store_call(tokens: &[Tok], i: usize) -> Option<&str> {
if tokens.get(i) != Some(&Tok::Punct('.')) || i == 0 {
return None;
}
let Some(Tok::Ident(method)) = tokens.get(i + 1) else {
return None;
};
if tokens.get(i + 2) != Some(&Tok::Punct('(')) {
return None;
}
let receiver_is_store = match &tokens[i - 1] {
Tok::Ident(name) => name == "store",
Tok::Punct(')') => {
i >= 3
&& tokens[i - 2] == Tok::Punct('(')
&& tokens[i - 3] == Tok::Ident("store".into())
}
Tok::Str(_) | Tok::Punct(_) | Tok::Other => false,
};
receiver_is_store.then_some(method.as_str())
}
pub(crate) fn call_args(tokens: &[Tok], open: usize) -> Vec<Vec<Tok>> {
let close = matching_close(tokens, open);
let mut args = vec![Vec::new()];
let mut depth = 0usize;
for tok in &tokens[open + 1..close.min(tokens.len())] {
match tok {
Tok::Punct('(' | '[' | '{') => depth += 1,
Tok::Punct(')' | ']' | '}') => depth = depth.saturating_sub(1),
Tok::Punct(',') if depth == 0 => {
args.push(Vec::new());
continue;
}
_ => {}
}
if let Some(last) = args.last_mut() {
last.push(tok.clone());
}
}
args.retain(|a| !a.is_empty());
args
}
fn single_str(arg: &[Tok]) -> Option<&str> {
match arg {
[Tok::Str(s)] => Some(s),
_ => None,
}
}
fn keyed_kind(tokens: &[Tok], call: usize, key: &[Tok]) -> Option<String> {
let key = key.strip_prefix(&[Tok::Punct('&')][..]).unwrap_or(key);
if let Some(kind) = key_constructor_kind(key) {
return Some(kind);
}
let [Tok::Ident(name)] = key else {
return None;
};
let name = Tok::Ident(name.clone());
for i in (0..call).rev() {
if tokens[i] == Tok::Ident("fn".into()) {
return None;
}
let binds_name = tokens[i] == Tok::Ident("let".into())
&& (tokens.get(i + 1) == Some(&name)
|| (tokens.get(i + 1) == Some(&Tok::Ident("mut".into()))
&& tokens.get(i + 2) == Some(&name)));
if binds_name {
let eq = (i..call).find(|j| tokens[*j] == Tok::Punct('='))?;
let semi = (eq..call).find(|j| tokens[*j] == Tok::Punct(';'))?;
return key_constructor_kind(&tokens[eq + 1..semi]);
}
}
None
}
fn key_constructor_kind(expr: &[Tok]) -> Option<String> {
let at = expr.windows(4).position(|w| {
w[0] == Tok::Ident("ResourceKey".into())
&& w[1] == Tok::Punct(':')
&& w[2] == Tok::Punct(':')
&& matches!(&w[3], Tok::Ident(c) if c == "namespaced" || c == "cluster_scoped")
})?;
let open = at + 4;
if expr.get(open) != Some(&Tok::Punct('(')) {
return None;
}
let args = call_args(expr, open);
args.get(2)
.and_then(|arg| single_str(arg))
.map(str::to_owned)
}
#[cfg(test)]
mod tests {
use super::*;
fn census(src: &'static str) -> Census {
Census::of(&[Section::file("fixture.rs", src)])
}
fn kinds(src: &'static str) -> Vec<String> {
census(src).kinds.into_iter().collect()
}
#[test]
fn it_sees_every_read_shape_it_claims() {
let found = kinds(
r#"
async fn tick(&self) {
let a = self.store.list("", "v1", "Pod", None).await;
let b = self
.store
.list(
"apps",
"v1",
"ReplicaSet",
self.namespace.as_deref(),
)
.await;
let c = this.store().list_at_revision("", "v1", "Node", None).await;
let key = ResourceKey::namespaced("", "v1", "Secret", ns, &name);
let d = self.store.get(&key).await;
let e = store.get(&ResourceKey::cluster_scoped("", "v1", "PersistentVolume", n)).await;
let mut slice_key = engenho_store::resource::ResourceKey::namespaced(
"discovery.k8s.io", "v1", "EndpointSlice", ns, svc,
);
let f = self.store.get(&slice_key).await;
}
"#,
);
assert_eq!(
found,
[
"EndpointSlice",
"Node",
"PersistentVolume",
"Pod",
"ReplicaSet",
"Secret"
]
);
}
#[test]
fn a_read_takes_the_nearest_binding_above_it() {
let found = kinds(
r#"
fn f() {
let key = ResourceKey::namespaced("", "v1", "ConfigMap", ns, &n);
store.get(&key);
let key = ResourceKey::namespaced("", "v1", "Secret", ns, &n);
store.get(&key);
}
"#,
);
assert_eq!(found, ["ConfigMap", "Secret"]);
}
#[test]
fn it_does_not_see_what_is_not_a_read() {
let c = census(
r##"
// self.store.list("", "v1", "InAComment", None)
/* store.list("", "v1", "InABlock", None) */
const DOC: &str = "self.store.list(\"\", \"v1\", \"InAString\", None)";
const RAW: &str = r#"store.list("", "v1", "InARawString", None)"#;
const QUOTE: char = '"';
fn f<'a>(x: &'a Value) {
let spec = pod.get("spec");
let m = self.local.lock().await.get(key);
let z = zone.list();
let put_key = ResourceKey::namespaced("", "v1", "WrittenOnly", ns, n);
self.store.propose(Put { key: put_key });
}
#[cfg(test)]
mod tests {
fn t() { store.list("", "v1", "InATest", None); }
}
#[cfg(test)]
fn helper() { store.list("", "v1", "InATestFn", None); }
fn after() { store.list("", "v1", "AfterTheTests", None); }
"##,
);
assert_eq!(c.kinds.into_iter().collect::<Vec<_>>(), ["AfterTheTests"]);
assert_eq!(c.computed, 0);
assert!(!c.every);
}
#[test]
fn a_binding_in_another_function_is_not_this_ones() {
let c = census(
r#"
fn one() { let key = ResourceKey::namespaced("", "v1", "Elsewhere", ns, n); }
fn two(key: &ResourceKey) { self.store.get(key); }
"#,
);
assert!(c.kinds.is_empty());
assert_eq!(c.computed, 1);
}
#[test]
fn an_escaped_quote_char_does_not_hide_the_next_read() {
let found = kinds(
r#"
fn f() {
let q = '\'';
let d = '"';
store.list("", "v1", "Visible", None);
}
"#,
);
assert_eq!(found, ["Visible"]);
}
#[test]
fn a_computed_kind_is_counted_not_named() {
let c = census(
r"
fn f() {
for d in namespaced_kinds() {
store.list(d.group, d.version, d.kind, Some(ns));
}
for key in &candidates { self.store.get(key); }
let lease = lease_key(node);
self.store.get(&lease);
}
",
);
assert!(c.kinds.is_empty());
assert_eq!(c.computed, 3);
}
#[test]
fn reading_the_whole_catalog_reads_every_kind() {
assert!(census("fn f() { let c = store.current_catalog().await; }").every);
}
#[test]
fn a_section_is_the_text_between_its_markers() {
const SRC: &str = r#"
struct A; fn a() { store.list("", "v1", "Alpha", None); }
struct B; fn b() { store.list("", "v1", "Beta", None); }
"#;
let a = Census::of(&[Section::between(
"fixture.rs",
SRC,
"struct A",
Some("struct B"),
)]);
let b = Census::of(&[Section::between("fixture.rs", SRC, "struct B", None)]);
assert_eq!(a.kinds.into_iter().collect::<Vec<_>>(), ["Alpha"]);
assert_eq!(b.kinds.into_iter().collect::<Vec<_>>(), ["Beta"]);
}
#[test]
#[should_panic(expected = "section marker")]
fn a_missing_marker_fails_loudly() {
let _ = Census::of(&[Section::between(
"fixture.rs",
"fn f() {}",
"struct Gone",
None,
)]);
}
}