1use std::collections::HashMap;
2
3use chrono::Utc;
4use lazy_static::lazy_static;
5use serde_json::Value;
6
7use crate::evaluation::cmab_evaluator::evaluate_cmab;
8use crate::evaluation::comparisons::{
9 compare_arrays, compare_numbers, compare_str_with_regex, compare_strings_in_array,
10 compare_time, compare_versions,
11};
12use crate::evaluation::dynamic_returnable::DynamicReturnable;
13use crate::evaluation::dynamic_string::DynamicString;
14use crate::evaluation::dynamic_value::DynamicValue;
15use crate::evaluation::evaluation_types::SecondaryExposure;
16use crate::evaluation::evaluator_context::{EvaluatorContext, IdListResolution};
17use crate::evaluation::evaluator_value::{EvaluatorValue, MemoizedEvaluatorValue};
18use crate::evaluation::get_unit_id::get_unit_id;
19use crate::evaluation::user_agent_parsing::UserAgentParser;
20use crate::interned_string::InternedString;
21use crate::specs_response::explicit_params::ExplicitParameters;
22use crate::specs_response::spec_types::{Condition, Rule, Spec};
23use crate::{dyn_value, log_w, unwrap_or_return, ExperimentEvaluationOptions, StatsigErr};
24
25use super::country_lookup::CountryLookup;
26
27const TAG: &str = "Evaluator";
28
29pub struct Evaluator;
30
31lazy_static! {
32 static ref EMPTY_STR: String = String::new();
33 static ref EMPTY_DYNAMIC_VALUE: DynamicValue = DynamicValue::new();
34 static ref DISABLED_RULE: InternedString = InternedString::from_str_ref("disabled");
35 static ref SALT: InternedString = InternedString::from_str_ref("salt");
36}
37
38#[derive(Clone, Debug)]
39pub enum SpecType {
40 Gate,
41 DynamicConfig,
42 Experiment,
43 Layer,
44 ParameterStore,
45}
46
47#[derive(PartialEq, Eq, Debug)]
48pub enum Recognition {
49 Unrecognized,
50 Recognized,
51}
52
53impl Evaluator {
54 pub fn evaluate(
55 ctx: &mut EvaluatorContext,
56 spec_name: &str,
57 spec_type: &SpecType,
58 ) -> Result<Recognition, StatsigErr> {
59 let spec_name_intern = InternedString::from_str_ref(spec_name);
60
61 let opt_spec = match spec_type {
62 SpecType::Gate => ctx.specs_data.feature_gates.get(&spec_name_intern),
63 SpecType::DynamicConfig => ctx.specs_data.dynamic_configs.get(&spec_name_intern),
64 SpecType::Experiment => ctx.specs_data.dynamic_configs.get(&spec_name_intern),
65 SpecType::Layer => ctx.specs_data.layer_configs.get(&spec_name_intern),
66 SpecType::ParameterStore => {
67 return evaluate_param_store_reason(ctx, spec_name.to_string())
68 }
69 }
70 .map(|sp| sp.as_spec_ref());
71
72 if try_apply_override(ctx, spec_name, spec_type, opt_spec) {
73 return Ok(Recognition::Recognized);
74 }
75
76 if try_apply_config_mapping(ctx, spec_name, spec_type, opt_spec) {
77 return Ok(Recognition::Recognized);
78 }
79
80 if evaluate_cmab(ctx, spec_name, spec_type) {
81 return Ok(Recognition::Recognized);
82 }
83
84 let spec = unwrap_or_return!(opt_spec, Ok(Recognition::Unrecognized));
85
86 if ctx.result.name.is_none() {
87 ctx.result.name = Some(spec_name_intern);
88 }
89
90 if ctx.result.id_type.is_none() {
91 ctx.result.id_type = Some(InternedString::from_str_ref(&spec.id_type));
92 }
93
94 if ctx.result.version.is_none() {
95 if let Some(version) = spec.version {
96 ctx.result.version = Some(version);
97 }
98 }
99
100 if let Some(is_active) = spec.is_active {
101 ctx.result.is_experiment_active = is_active;
102 }
103
104 if let Some(has_shared_params) = spec.has_shared_params {
105 ctx.result.is_in_layer = has_shared_params;
106 }
107
108 if let Some(explicit_params) = &spec.explicit_parameters {
109 ctx.result.explicit_parameters = Some(explicit_params.clone());
110 }
111
112 if spec.use_new_layer_eval == Some(true) && matches!(spec_type, SpecType::Layer) {
113 return new_layer_eval(ctx, spec);
114 }
115
116 for rule in &spec.rules {
117 evaluate_rule(ctx, rule)?;
118
119 if ctx.result.unsupported {
120 return Ok(Recognition::Recognized);
121 }
122
123 if !ctx.result.bool_value {
124 continue;
125 }
126
127 if evaluate_config_delegate(ctx, rule)? {
128 ctx.finalize_evaluation(spec, Some(rule));
129 return Ok(Recognition::Recognized);
130 }
131
132 let did_pass = evaluate_pass_percentage(ctx, rule, &spec.salt);
133
134 if did_pass {
135 ctx.result.bool_value = rule.return_value.get_bool() != Some(false);
136 ctx.result.json_value = Some(rule.return_value.clone());
137 } else {
138 ctx.result.bool_value = spec.default_value.get_bool() == Some(true);
139 ctx.result.json_value = Some(spec.default_value.clone());
140 }
141
142 ctx.result.rule_id = Some(rule.id.clone());
143 ctx.result.group_name = rule.group_name.clone();
144 ctx.result.is_experiment_group = rule.is_experiment_group.unwrap_or(false);
145 ctx.result.is_experiment_active = spec.is_active.unwrap_or(false);
146 ctx.finalize_evaluation(spec, Some(rule));
147 return Ok(Recognition::Recognized);
148 }
149
150 ctx.result.bool_value = spec.default_value.get_bool() == Some(true);
151 ctx.result.json_value = Some(spec.default_value.clone());
152 ctx.result.rule_id = match spec.enabled {
153 true => Some(InternedString::default_rule_id()),
154 false => Some(DISABLED_RULE.clone()),
155 };
156 ctx.finalize_evaluation(spec, None);
157
158 Ok(Recognition::Recognized)
159 }
160}
161
162fn new_layer_eval<'a>(
163 ctx: &mut EvaluatorContext<'a>,
164 spec: &'a Spec,
165) -> Result<Recognition, StatsigErr> {
166 let mut has_delegate = false;
167 let mut passed = false;
168 let mut rule_id: Option<&'a InternedString> = Some(InternedString::default_rule_id_ref());
169 let mut delegate_name: Option<InternedString> = None;
170 let mut rule_ids: HashMap<InternedString, InternedString> = HashMap::new();
171 let mut value: HashMap<String, Value> = HashMap::new();
172 let mut group_name: Option<InternedString> = None;
173 let mut is_experiment_group = false;
174 let mut explicit_parameters: Option<ExplicitParameters> = None;
175 let mut secondary_exposures: Vec<SecondaryExposure> = Vec::new();
176 let mut undelegated_secondary_exposures: Vec<SecondaryExposure> = Vec::new();
177
178 for rule in &spec.rules {
179 evaluate_rule(ctx, rule)?;
180 secondary_exposures.append(&mut ctx.result.secondary_exposures);
181 undelegated_secondary_exposures.append(&mut ctx.result.secondary_exposures);
182 ctx.result.secondary_exposures.clear();
183
184 if ctx.result.unsupported {
185 return Ok(Recognition::Recognized);
186 }
187
188 if !ctx.result.bool_value {
189 continue;
190 }
191
192 let did_pass = evaluate_pass_percentage(ctx, rule, &spec.salt);
193 if !did_pass {
194 continue;
195 }
196
197 if evaluate_config_delegate(ctx, rule)? {
198 if has_delegate {
199 continue;
200 }
201 let delegate_value = match &ctx.result.json_value {
202 Some(val) => val.get_json(),
203 None => continue,
204 };
205 let mut has_reused_parameter = false;
206 if let Some(json_map) = &delegate_value {
207 for k in json_map.keys() {
208 if value.contains_key(k) {
209 has_reused_parameter = true;
210 break;
211 }
212 }
213 }
214
215 if has_reused_parameter {
216 continue;
217 }
218
219 update_parameter_values(&mut value, &mut rule_ids, delegate_value, &rule.id);
220
221 secondary_exposures.append(&mut ctx.result.secondary_exposures);
222 ctx.result.secondary_exposures.clear();
223
224 has_delegate = true;
225 passed = ctx.result.bool_value;
226 delegate_name = ctx.result.config_delegate.clone();
227 group_name = ctx.result.group_name.clone();
228 rule_id = Some(&rule.id);
229 is_experiment_group = rule.is_experiment_group.unwrap_or(false);
230 explicit_parameters = ctx.result.explicit_parameters.clone();
231 } else {
232 update_parameter_values(
233 &mut value,
234 &mut rule_ids,
235 rule.return_value.get_json(),
236 &rule.id,
237 );
238 }
239 }
240 update_parameter_values(
241 &mut value,
242 &mut rule_ids,
243 spec.default_value.get_json(),
244 InternedString::default_rule_id_ref(),
245 );
246 ctx.result.bool_value = passed;
247 ctx.result.config_delegate = delegate_name;
248 ctx.result.group_name = group_name;
249 ctx.result.rule_id = rule_id.cloned();
250 ctx.result.json_value = Some(DynamicReturnable::from_map(value));
251 ctx.result.is_experiment_group = is_experiment_group;
252 ctx.result.is_experiment_active = spec.is_active.unwrap_or(false);
253 ctx.result.explicit_parameters = explicit_parameters;
254 ctx.result.secondary_exposures = secondary_exposures;
255 ctx.result.undelegated_secondary_exposures = Some(undelegated_secondary_exposures);
256 ctx.result.parameter_rule_ids = Some(rule_ids);
257 ctx.finalize_evaluation(spec, None);
258 Ok(Recognition::Recognized)
259}
260
261fn update_parameter_values(
262 value: &mut HashMap<String, Value>,
263 rule_ids: &mut HashMap<InternedString, InternedString>,
264 values_to_apply: Option<HashMap<String, Value>>,
265 rule_id: &InternedString,
266) {
267 let json_map = match values_to_apply {
268 Some(map) => map,
269 None => return,
270 };
271 for (k, v) in json_map {
272 let parameter_name = InternedString::from_str_ref(&k);
273 if let std::collections::hash_map::Entry::Vacant(e) = value.entry(k) {
274 e.insert(v);
275 rule_ids.insert(parameter_name.clone(), rule_id.clone());
276 }
277 }
278}
279
280fn try_apply_config_mapping(
281 ctx: &mut EvaluatorContext,
282 spec_name: &str,
283 spec_type: &SpecType,
284 opt_spec: Option<&Spec>,
285) -> bool {
286 let overrides = match &ctx.specs_data.overrides {
287 Some(overrides) => overrides,
288 None => return false,
289 };
290
291 let override_rules = match &ctx.specs_data.override_rules {
292 Some(override_rules) => override_rules,
293 None => return false,
294 };
295
296 let mapping_list = match overrides.get(spec_name) {
297 Some(mapping_list) => mapping_list,
298 None => return false,
299 };
300
301 let spec_salt = match opt_spec {
302 Some(spec) => &spec.salt,
303 None => InternedString::empty_ref(),
304 };
305
306 for mapping in mapping_list {
307 for override_rule in &mapping.rules {
308 let start_time = override_rule.start_time.unwrap_or_default();
309
310 if start_time > Utc::now().timestamp_millis() {
311 continue;
312 }
313
314 let rule = match override_rules.get(&override_rule.rule_name) {
315 Some(rule) => rule,
316 None => continue,
317 };
318 match evaluate_rule(ctx, rule) {
319 Ok(_) => {}
320 Err(_) => {
321 ctx.reset_result();
322 continue;
323 }
324 }
325
326 if !ctx.result.bool_value || ctx.result.unsupported {
327 ctx.reset_result();
328 continue;
329 }
330 ctx.reset_result();
331 let pass = evaluate_pass_percentage(ctx, rule, spec_salt);
332 if pass {
333 ctx.result.override_config_name = Some(mapping.new_config_name.clone());
334 match Evaluator::evaluate(ctx, mapping.new_config_name.as_str(), spec_type) {
335 Ok(Recognition::Recognized) => {
336 return true;
337 }
338 _ => {
339 ctx.reset_result();
340 break;
341 }
342 }
343 }
344 }
345 }
346
347 false
348}
349
350fn try_apply_override(
351 ctx: &mut EvaluatorContext,
352 spec_name: &str,
353 spec_type: &SpecType,
354 opt_spec: Option<&Spec>,
355) -> bool {
356 let adapter = match &ctx.override_adapter {
357 Some(adapter) => adapter,
358 None => return false,
359 };
360
361 match spec_type {
362 SpecType::Gate => adapter.get_gate_override(ctx.user.user_ref, spec_name, &mut ctx.result),
363
364 SpecType::DynamicConfig => {
365 adapter.get_dynamic_config_override(ctx.user.user_ref, spec_name, &mut ctx.result)
366 }
367
368 SpecType::Experiment => {
369 adapter.get_experiment_override(ctx.user.user_ref, spec_name, &mut ctx.result, opt_spec)
370 }
371
372 SpecType::Layer => {
373 adapter.get_layer_override(ctx.user.user_ref, spec_name, &mut ctx.result)
374 }
375
376 SpecType::ParameterStore => {
377 adapter.get_parameter_store_override(ctx.user.user_ref, spec_name, &mut ctx.result)
378 }
379 }
380}
381
382fn evaluate_rule<'a>(ctx: &mut EvaluatorContext<'a>, rule: &'a Rule) -> Result<(), StatsigErr> {
383 let mut all_conditions_pass = true;
384 for condition_hash in &rule.conditions {
386 let opt_condition = ctx.specs_data.condition_map.get(condition_hash);
388 let condition = if let Some(c) = opt_condition {
389 c
390 } else {
391 log_w!(TAG, "Unsupported - Condition not found: {}", condition_hash);
392 ctx.result.unsupported = true;
393 return Ok(());
394 };
395
396 evaluate_condition(ctx, condition)?;
397
398 if !ctx.result.bool_value {
399 all_conditions_pass = false;
400 }
401 }
402
403 ctx.result.bool_value = all_conditions_pass;
404
405 Ok(())
406}
407
408fn evaluate_condition<'a>(
409 ctx: &mut EvaluatorContext<'a>,
410 condition: &'a Condition,
411) -> Result<(), StatsigErr> {
412 let temp_value: Option<DynamicValue>;
413 let target_value = condition
414 .target_value
415 .as_ref()
416 .map(|v| v.as_ref())
417 .unwrap_or(EvaluatorValue::empty().as_ref());
418 let condition_type = condition.condition_type.as_str();
419
420 let value: &DynamicValue = match condition_type {
421 "public" => {
422 ctx.result.bool_value = true;
423 return Ok(());
424 }
425 "fail_gate" | "pass_gate" => {
426 evaluate_nested_gate(ctx, target_value, condition_type)?;
427 return Ok(());
428 }
429 "experiment_group" => {
430 let group_name = evaluate_experiment_group(ctx, &condition.field);
431 match group_name {
432 Some(name) => {
433 temp_value = Some(DynamicValue::from(name));
434 temp_value.as_ref()
435 }
436 None => None,
437 }
438 }
439 "ua_based" => match ctx.user.get_user_value(&condition.field) {
440 Some(value) => Some(value),
441 None => {
442 temp_value = UserAgentParser::get_value_from_user_agent(
443 ctx.user,
444 &condition.field,
445 &mut ctx.result.override_reason,
446 ctx.should_user_third_party_parser,
447 );
448 temp_value.as_ref()
449 }
450 },
451 "ip_based" => match ctx.user.get_user_value(&condition.field) {
452 Some(value) => Some(value),
453 None => {
454 temp_value = CountryLookup::get_value_from_ip(ctx.user, &condition.field, ctx);
455 temp_value.as_ref()
456 }
457 },
458 "user_field" => ctx.user.get_user_value(&condition.field),
459 "environment_field" => {
460 temp_value = ctx.user.get_value_from_environment(&condition.field);
461 temp_value.as_ref()
462 }
463 "current_time" => {
464 temp_value = Some(DynamicValue::for_timestamp_evaluation(
465 Utc::now().timestamp_millis(),
466 ));
467 temp_value.as_ref()
468 }
469 "user_bucket" => {
470 temp_value = Some(get_hash_for_user_bucket(ctx, condition));
471 temp_value.as_ref()
472 }
473 "target_app" => ctx.app_id,
474 "unit_id" => ctx.user.get_unit_id(&condition.id_type),
475 _ => {
476 log_w!(
477 TAG,
478 "Unsupported - Unknown condition type: {}",
479 condition_type
480 );
481 ctx.result.unsupported = true;
482 return Ok(());
483 }
484 }
485 .unwrap_or(&EMPTY_DYNAMIC_VALUE);
486
487 let operator = match &condition.operator {
490 Some(operator) => operator.as_str(),
491 None => {
492 log_w!(TAG, "Unsupported - Operator is None",);
493 ctx.result.unsupported = true;
494 return Ok(());
495 }
496 };
497
498 ctx.result.bool_value = match operator {
499 "gt" | "gte" | "lt" | "lte" => compare_numbers(value, target_value, operator),
501
502 "version_gt" | "version_gte" | "version_lt" | "version_lte" | "version_eq"
504 | "version_neq" => compare_versions(value, target_value, operator),
505
506 "any"
508 | "none"
509 | "str_starts_with_any"
510 | "str_ends_with_any"
511 | "str_contains_any"
512 | "str_contains_none" => compare_strings_in_array(value, target_value, operator, true),
513 "any_case_sensitive" | "none_case_sensitive" => {
514 compare_strings_in_array(value, target_value, operator, false)
515 }
516 "str_matches" => compare_str_with_regex(value, target_value),
517
518 "before" | "after" | "on" => compare_time(value, target_value, operator),
520
521 "eq" => target_value.is_equal_to_dynamic_value(value),
523 "neq" => !target_value.is_equal_to_dynamic_value(value),
524
525 "in_segment_list" | "not_in_segment_list" => {
527 evaluate_id_list(ctx, operator, target_value, value)
528 }
529
530 "array_contains_any"
531 | "array_contains_none"
532 | "array_contains_all"
533 | "not_array_contains_all" => compare_arrays(value, target_value, operator),
534
535 _ => {
536 log_w!(TAG, "Unsupported - Unknown operator: {}", operator);
537 ctx.result.unsupported = true;
538 return Ok(());
539 }
540 };
541
542 Ok(())
543}
544
545fn evaluate_id_list(
546 ctx: &mut EvaluatorContext<'_>,
547 op: &str,
548 target_value: &MemoizedEvaluatorValue,
549 value: &DynamicValue,
550) -> bool {
551 let is_in_list = is_in_id_list(ctx, target_value, value);
552
553 if op == "not_in_segment_list" {
554 return !is_in_list;
555 }
556
557 is_in_list
558}
559
560fn is_in_id_list(
561 ctx: &mut EvaluatorContext<'_>,
562 target_value: &MemoizedEvaluatorValue,
563 value: &DynamicValue,
564) -> bool {
565 let list_name = unwrap_or_return!(&target_value.string_value, false);
566 let dyn_str = unwrap_or_return!(&value.string_value, false);
567 let hashed = ctx.hashing.sha256(&dyn_str.value);
568 let lookup_id: String = hashed.chars().take(8).collect();
569
570 match ctx.id_list_resolver {
571 IdListResolution::MapLookup(id_lists) => {
572 let list = unwrap_or_return!(id_lists.get(list_name.value.as_str()), false);
573
574 list.ids.contains(&lookup_id)
575 }
576 IdListResolution::Callback(callback) => {
577 callback(list_name.value.as_str(), lookup_id.as_str())
578 }
579 }
580}
581
582fn evaluate_experiment_group<'a>(
583 ctx: &mut EvaluatorContext<'a>,
584 experiment_name: &Option<DynamicString>,
585) -> Option<String> {
586 let exp_name = match experiment_name {
587 Some(name) => &name.value,
588 None => {
589 return None;
590 }
591 };
592 let statsig = match &ctx.statsig {
593 Some(s) => s,
594 None => {
595 ctx.result.unsupported = true;
596 return None;
597 }
598 };
599 let res = statsig.get_experiment_with_options(
600 ctx.user.user_ref,
601 exp_name.as_str(),
602 ExperimentEvaluationOptions {
603 disable_exposure_logging: ctx.disable_exposure_logging,
604 user_persisted_values: None,
605 },
606 );
607 res.group_name
608}
609
610fn evaluate_nested_gate<'a>(
611 ctx: &mut EvaluatorContext<'a>,
612 target_value: &'a MemoizedEvaluatorValue,
613 condition_type: &'a str,
614) -> Result<(), StatsigErr> {
615 let gate_name = target_value
616 .string_value
617 .as_ref()
618 .map(|name| &name.value)
619 .unwrap_or(InternedString::empty_ref());
620
621 match ctx.nested_gate_memo.get(gate_name) {
622 Some((previous_bool, previous_rule_id)) => {
623 ctx.result.bool_value = *previous_bool;
624 ctx.result.rule_id = previous_rule_id.clone();
625 }
626 None => {
627 ctx.prep_for_nested_evaluation()?;
628
629 let _ = Evaluator::evaluate(ctx, gate_name.as_str(), &SpecType::Gate)?;
630
631 if ctx.result.unsupported {
632 return Ok(());
633 }
634
635 if !gate_name.as_str().is_empty() {
636 ctx.nested_gate_memo.insert(
637 gate_name.clone(),
638 (ctx.result.bool_value, ctx.result.rule_id.clone()),
639 );
640 }
641 }
642 }
643
644 if !&gate_name.starts_with("segment:") {
645 let res = &ctx.result;
646 let expo = SecondaryExposure {
647 gate: gate_name.clone(),
648 gate_value: InternedString::from_bool(res.bool_value),
649 rule_id: res.rule_id.clone().unwrap_or_default(),
650 };
651
652 if res.sampling_rate.is_none() {
653 ctx.result.has_seen_analytical_gates = Option::from(true);
654 }
655
656 ctx.result.secondary_exposures.push(expo);
657 }
658
659 if condition_type == "fail_gate" {
660 ctx.result.bool_value = !ctx.result.bool_value;
661 }
662 Ok(())
663}
664
665fn evaluate_config_delegate<'a>(
666 ctx: &mut EvaluatorContext<'a>,
667 rule: &'a Rule,
668) -> Result<bool, StatsigErr> {
669 let delegate = unwrap_or_return!(&rule.config_delegate, Ok(false));
670 let delegate_spec = unwrap_or_return!(ctx.specs_data.dynamic_configs.get(delegate), Ok(false));
671
672 ctx.result.undelegated_secondary_exposures = Some(ctx.result.secondary_exposures.clone());
673
674 ctx.prep_for_nested_evaluation()?;
675 let recognition = Evaluator::evaluate(ctx, delegate, &SpecType::Experiment)?;
676 if recognition == Recognition::Unrecognized {
677 ctx.result.undelegated_secondary_exposures = None;
678 return Ok(false);
679 }
680
681 ctx.result.explicit_parameters = delegate_spec.as_spec_ref().explicit_parameters.clone();
682 ctx.result.config_delegate = rule.config_delegate.clone();
683
684 Ok(true)
685}
686
687fn evaluate_pass_percentage(
688 ctx: &mut EvaluatorContext,
689 rule: &Rule,
690 spec_salt: &InternedString,
691) -> bool {
692 if rule.pass_percentage == 100f64 {
693 return true;
694 }
695
696 if rule.pass_percentage == 0f64 {
697 return false;
698 }
699
700 let rule_salt = rule.salt.as_deref().unwrap_or(rule.id.as_str());
701 let unit_id = get_unit_id(ctx, &rule.id_type);
702 let input = format!("{spec_salt}.{rule_salt}.{unit_id}");
703 match ctx.hashing.evaluation_hash(&input) {
704 Some(hash) => ((hash % 10000) as f64) < rule.pass_percentage * 100.0,
705 None => false,
706 }
707}
708
709fn get_hash_for_user_bucket(ctx: &mut EvaluatorContext, condition: &Condition) -> DynamicValue {
710 let unit_id = get_unit_id(ctx, &condition.id_type);
711
712 let mut salt = InternedString::empty_ref();
713
714 if let Some(add_values) = &condition.additional_values {
715 if let Some(v) = add_values.get(&SALT) {
716 salt = v;
717 }
718 }
719
720 let input = format!("{salt}.{unit_id}");
721 let hash = ctx.hashing.evaluation_hash(&input).unwrap_or(1);
722 dyn_value!(hash % 1000)
723}
724
725fn evaluate_param_store_reason(
726 ctx: &mut EvaluatorContext,
727 spec_name: String,
728) -> Result<Recognition, StatsigErr> {
729 let spec_name_intern = InternedString::from_str_ref(&spec_name);
730 let has_param_store = ctx
731 .specs_data
732 .param_stores
733 .as_ref()
734 .and_then(|stores| stores.get(&spec_name_intern))
735 .is_some();
736 Ok(if has_param_store {
737 Recognition::Recognized
738 } else {
739 Recognition::Unrecognized
740 })
741}