#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
use oxc_ast_visit::walk;
use oxc_span::ContentEq;
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::extract::{
FlagConstant, FlagConstantRead, FlagDefinition, FlagKeyRegistry, FlagPatterns,
FlagRegistryFacts, FlagRegistryRead, FlagSiteFacts, FlagUse, FlagUseKind,
byte_offset_to_line_col,
};
use oxc_semantic::ScopeFlags;
const BUILTIN_SDK_PATTERNS: &[(&str, usize, &str)] = &[
("useFlag", 0, "LaunchDarkly"),
("useLDFlag", 0, "LaunchDarkly"),
("useFeatureFlag", 0, "LaunchDarkly"),
("variation", 0, "LaunchDarkly"),
("boolVariation", 0, "LaunchDarkly"),
("stringVariation", 0, "LaunchDarkly"),
("numberVariation", 0, "LaunchDarkly"),
("jsonVariation", 0, "LaunchDarkly"),
("useGate", 0, "Statsig"),
("checkGate", 0, "Statsig"),
("useExperiment", 0, "Statsig"),
("useConfig", 0, "Statsig"),
("isEnabled", 0, "Unleash"),
("getVariant", 0, "Unleash"),
("isOn", 0, "GrowthBook"),
("isOff", 0, "GrowthBook"),
("getFeatureValue", 0, "GrowthBook"),
("getTreatment", 0, "Split"),
("useFeatureFlagEnabled", 0, "PostHog"),
("useFeatureFlagPayload", 0, "PostHog"),
("useFeatureFlagVariantKey", 0, "PostHog"),
("getFeatureFlagPayload", 0, "PostHog"),
("getValueAsync", 0, "ConfigCat"),
("getValueDetailsAsync", 0, "ConfigCat"),
("hasFeature", 0, "Flagsmith"),
("useDecision", 0, "Optimizely"),
("getFeatureVariable", 0, "Optimizely"),
("getFeatureVariableBoolean", 0, "Optimizely"),
("getFeatureVariableString", 0, "Optimizely"),
("getFeatureVariableInteger", 0, "Optimizely"),
("getFeatureVariableDouble", 0, "Optimizely"),
("getFeatureVariableJson", 0, "Optimizely"),
("getFeatureVariableJSON", 0, "Optimizely"),
("getStringAssignment", 0, "Eppo"),
("getBooleanAssignment", 0, "Eppo"),
("getNumericAssignment", 0, "Eppo"),
("getIntegerAssignment", 0, "Eppo"),
("getJSONAssignment", 0, "Eppo"),
("getStringAssignmentDetails", 0, "Eppo"),
("getBooleanAssignmentDetails", 0, "Eppo"),
("getNumericAssignmentDetails", 0, "Eppo"),
("getIntegerAssignmentDetails", 0, "Eppo"),
("getJSONAssignmentDetails", 0, "Eppo"),
("getValue", 0, ""),
("useFeature", 0, ""),
("getFeatureFlag", 0, ""),
];
const GENERIC_SDK_NAMES: &[&str] = &["getValue", "isEnabled", "useFeature"];
const FLAG_SOURCE_MARKERS: &[&str] = &[
"flag",
"feature",
"toggle",
"launchdarkly",
"statsig",
"unleash",
"growthbook",
"splitio",
"posthog",
"configcat",
"optimizely",
"@eppo/",
];
const VERCEL_FLAGS_PROVIDER: &str = "Vercel Flags";
const VERCEL_FLAGS_FUNCTIONS: &[&str] = &["flag", "evaluate"];
const BUILTIN_ENV_PREFIXES: &[&str] = &[
"FEATURE_",
"NEXT_PUBLIC_FEATURE_",
"NEXT_PUBLIC_ENABLE_",
"REACT_APP_FEATURE_",
"REACT_APP_ENABLE_",
"VITE_FEATURE_",
"VITE_ENABLE_",
"NUXT_PUBLIC_FEATURE_",
"ENABLE_",
"FF_",
"FLAG_",
"TOGGLE_",
];
#[must_use]
pub fn builtin_sdk_providers() -> Vec<&'static str> {
let mut providers: Vec<&'static str> = Vec::new();
for &(_, _, provider) in BUILTIN_SDK_PATTERNS {
if !provider.is_empty() && !providers.contains(&provider) {
providers.push(provider);
}
}
if !providers.contains(&VERCEL_FLAGS_PROVIDER) {
providers.push(VERCEL_FLAGS_PROVIDER);
}
providers
}
#[must_use]
pub fn builtin_env_prefixes() -> &'static [&'static str] {
BUILTIN_ENV_PREFIXES
}
const CONFIG_OBJECT_KEYWORDS: &[&str] = &[
"feature",
"features",
"featureFlags",
"featureFlag",
"flag",
"flags",
"toggle",
"toggles",
];
#[derive(Debug, Clone, Copy)]
enum FlagRef {
Resolved(usize),
Registry(usize),
}
enum FlagNameArg {
Literal(String),
RegistryMember { registry: String, member: String },
}
#[derive(Debug, Clone, Copy)]
struct Guard {
start: u32,
end: u32,
facts: FlagSiteFacts,
}
struct FlagVisitor<'a> {
results: Vec<FlagUse>,
registry_reads: Vec<FlagRegistryRead>,
line_offsets: &'a [u32],
extra_sdk_patterns: &'a [(String, usize, String)],
extra_env_prefixes: &'a [String],
config_object_heuristics: bool,
vercel_flags_imports: FxHashMap<String, String>,
vercel_flags_namespaces: FxHashSet<String>,
local_registries: FxHashMap<String, Vec<(String, String)>>,
named_imports: FxHashSet<String>,
exported_registries: Vec<FlagKeyRegistry>,
constants: Vec<FlagConstant>,
literal_consts: FxHashMap<String, usize>,
guard_operands: Vec<u32>,
definition_calls: FxHashMap<u32, usize>,
definitions: Vec<FlagDefinition>,
current_guard: Option<Guard>,
block_ends: Vec<u32>,
binding_scopes: Vec<FxHashMap<String, Option<FlagRef>>>,
flag_binding_count: usize,
shadowed_registries: Vec<FxHashSet<String>>,
last_ref: Option<FlagRef>,
last_read_start: Option<u32>,
in_const_declaration: bool,
has_flag_import: bool,
unconfirmed_reads: Vec<FlagRef>,
}
impl<'a> FlagVisitor<'a> {
fn new(
line_offsets: &'a [u32],
extra_sdk_patterns: &'a [(String, usize, String)],
extra_env_prefixes: &'a [String],
config_object_heuristics: bool,
) -> Self {
Self {
results: Vec::new(),
registry_reads: Vec::new(),
line_offsets,
extra_sdk_patterns,
extra_env_prefixes,
config_object_heuristics,
vercel_flags_imports: FxHashMap::default(),
vercel_flags_namespaces: FxHashSet::default(),
local_registries: FxHashMap::default(),
named_imports: FxHashSet::default(),
exported_registries: Vec::new(),
constants: Vec::new(),
literal_consts: FxHashMap::default(),
guard_operands: Vec::new(),
definition_calls: FxHashMap::default(),
definitions: Vec::new(),
current_guard: None,
block_ends: Vec::new(),
binding_scopes: vec![FxHashMap::default()],
flag_binding_count: 0,
shadowed_registries: Vec::new(),
last_ref: None,
last_read_start: None,
in_const_declaration: false,
has_flag_import: false,
unconfirmed_reads: Vec::new(),
}
}
fn read_count(&self) -> usize {
self.results.len() + self.registry_reads.len()
}
fn new_flag_use(
&self,
flag_name: String,
kind: FlagUseKind,
offset: u32,
sdk_name: Option<String>,
) -> FlagUse {
let (line, col) = byte_offset_to_line_col(self.line_offsets, offset);
FlagUse {
flag_name,
kind,
line,
col,
guard_span_start: self.current_guard.map(|guard| guard.start),
guard_span_end: self.current_guard.map(|guard| guard.end),
sdk_name,
facts: self
.current_guard
.map_or_else(FlagSiteFacts::default, |guard| guard.facts),
}
}
fn push_flag_use(
&mut self,
flag_name: String,
kind: FlagUseKind,
offset: u32,
sdk_name: Option<String>,
) {
let flag_use = self.new_flag_use(flag_name, kind, offset, sdk_name);
self.last_ref = Some(FlagRef::Resolved(self.results.len()));
self.last_read_start = Some(offset);
self.results.push(flag_use);
}
fn check_env_var(&mut self, expr: &StaticMemberExpression<'_>) {
if let Some(env_name) = extract_env_name(expr)
&& self.is_flag_env_name(env_name)
{
self.push_flag_use(
env_name.to_string(),
FlagUseKind::EnvVar,
expr.span.start,
None,
);
}
}
fn check_sdk_call(&mut self, call: &CallExpression<'_>) {
let func_name = match &call.callee {
Expression::Identifier(id) => id.name.as_str(),
Expression::StaticMemberExpression(member) => member.property.name.as_str(),
_ => return,
};
if self.check_vercel_flags_call(call) {
return;
}
let extra_sdk_patterns: &'a [(String, usize, String)] = self.extra_sdk_patterns;
let pattern = BUILTIN_SDK_PATTERNS
.iter()
.find(|(name, _, _)| *name == func_name)
.map(|&(_, name_arg_idx, provider)| (name_arg_idx, provider))
.or_else(|| {
extra_sdk_patterns
.iter()
.find(|(name, _, _)| name == func_name)
.map(|(_, name_arg_idx, provider)| (*name_arg_idx, provider.as_str()))
});
let Some((name_arg_idx, provider)) = pattern else {
return;
};
let sdk_name = (!provider.is_empty()).then(|| provider.to_string());
let reads_before = self.read_count();
match extract_flag_name_arg(&call.arguments, name_arg_idx) {
Some(FlagNameArg::Literal(flag_name)) => {
self.push_flag_use(flag_name, FlagUseKind::SdkCall, call.span.start, sdk_name);
}
Some(FlagNameArg::RegistryMember { registry, member }) => {
self.record_registry_read(registry, member, call.span.start, sdk_name);
}
None => {}
}
if self.read_count() > reads_before
&& !self.has_flag_import
&& self.is_generic_sdk_name(func_name)
&& let Some(flag_ref) = self.last_ref
{
self.unconfirmed_reads.push(flag_ref);
}
}
fn is_generic_sdk_name(&self, name: &str) -> bool {
GENERIC_SDK_NAMES.contains(&name)
&& !self
.extra_sdk_patterns
.iter()
.any(|(pattern, _, _)| pattern == name)
}
fn mark_unconfirmed_reads(&mut self) {
for flag_ref in std::mem::take(&mut self.unconfirmed_reads) {
let flag_use = match flag_ref {
FlagRef::Resolved(index) => self.results.get_mut(index),
FlagRef::Registry(index) => self
.registry_reads
.get_mut(index)
.map(|read| &mut read.flag_use),
};
if let Some(flag_use) = flag_use {
flag_use.facts = flag_use.facts.with_unconfirmed_sdk(true);
}
}
}
fn record_registry_read(
&mut self,
registry: String,
member: String,
offset: u32,
sdk_name: Option<String>,
) {
if self
.shadowed_registries
.iter()
.any(|names| names.contains(®istry))
{
return;
}
if let Some(members) = self.local_registries.get(®istry) {
let key = members
.iter()
.find(|(name, _)| *name == member)
.map(|(_, key)| key.clone());
if let Some(key) = key {
self.push_flag_use(key, FlagUseKind::SdkCall, offset, sdk_name);
}
return;
}
if !self.named_imports.contains(®istry) {
return;
}
let flag_use = self.new_flag_use(String::new(), FlagUseKind::SdkCall, offset, sdk_name);
self.last_ref = Some(FlagRef::Registry(self.registry_reads.len()));
self.last_read_start = Some(offset);
self.registry_reads.push(FlagRegistryRead {
registry,
member,
flag_use,
});
}
fn check_vercel_flags_call(&mut self, call: &CallExpression<'_>) -> bool {
let Some(imported_name) = self.vercel_flags_imported_name(call) else {
return false;
};
let flag_name = match imported_name {
"flag" => extract_object_string_property_arg(&call.arguments, 0, "key"),
"evaluate" => extract_string_arg(&call.arguments, 0),
_ => None,
};
let Some(flag_name) = flag_name else {
return false;
};
let defines = imported_name == "flag";
self.push_flag_use(
flag_name,
FlagUseKind::SdkCall,
call.span.start,
Some(VERCEL_FLAGS_PROVIDER.to_string()),
);
if defines {
self.definition_calls
.insert(call.span.start, self.results.len() - 1);
}
true
}
fn record_definition(&mut self, decl: &VariableDeclarator<'_>, binding: &str) {
if self.definition_calls.is_empty() || !self.in_const_declaration {
return;
}
let Some(Expression::CallExpression(call)) = decl.init.as_ref().map(unwrap_value) else {
return;
};
let Some(&index) = self.definition_calls.get(&call.span.start) else {
return;
};
let Some(flag_use) = self.results.get_mut(index) else {
return;
};
flag_use.facts = flag_use.facts.with_definition(true);
self.definitions.push(FlagDefinition {
binding: binding.to_string(),
line: flag_use.line,
col: flag_use.col,
});
}
fn vercel_flags_imported_name<'b>(&'b self, call: &'b CallExpression<'_>) -> Option<&'b str> {
match &call.callee {
Expression::Identifier(id) => self
.vercel_flags_imports
.get(id.name.as_str())
.map(String::as_str),
Expression::StaticMemberExpression(member) => {
let Expression::Identifier(object) = &member.object else {
return None;
};
self.vercel_flags_namespaces
.contains(object.name.as_str())
.then_some(member.property.name.as_str())
}
_ => None,
}
}
fn collect_imports(&mut self, program: &Program<'_>) {
for stmt in &program.body {
match stmt {
Statement::ImportDeclaration(decl) => {
self.collect_vercel_flags_import(decl);
self.collect_named_imports(decl);
if imports_values(decl) && is_flag_source(decl.source.value.as_str()) {
self.has_flag_import = true;
}
}
Statement::VariableDeclaration(decl) if requires_flag_source(decl) => {
self.has_flag_import = true;
}
Statement::TSImportEqualsDeclaration(decl) if import_equals_flag_source(decl) => {
self.has_flag_import = true;
}
_ => {}
}
}
}
fn collect_named_imports(&mut self, decl: &ImportDeclaration<'_>) {
if decl.import_kind.is_type() {
return;
}
for spec in decl.specifiers.iter().flatten() {
if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = spec
&& !specifier.import_kind.is_type()
{
self.named_imports.insert(specifier.local.name.to_string());
}
}
}
fn collect_vercel_flags_import(&mut self, decl: &ImportDeclaration<'_>) {
if !is_vercel_flags_source(decl.source.value.as_str()) || decl.import_kind.is_type() {
return;
}
let Some(specifiers) = &decl.specifiers else {
return;
};
for spec in specifiers {
match spec {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
if specifier.import_kind.is_type() {
continue;
}
let imported_name = specifier.imported.name();
if VERCEL_FLAGS_FUNCTIONS.contains(&imported_name.as_str()) {
self.vercel_flags_imports
.insert(specifier.local.name.to_string(), imported_name.to_string());
}
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
self.vercel_flags_namespaces
.insert(specifier.local.name.to_string());
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => {}
}
}
}
fn collect_flag_registries(&mut self, program: &Program<'_>) {
let mut exports: Vec<(String, String)> = Vec::new();
for stmt in &program.body {
match stmt {
Statement::VariableDeclaration(decl) => {
self.collect_const_object_registries(decl);
self.collect_literal_constants(decl);
}
Statement::TSEnumDeclaration(enumd) => {
self.collect_enum_registry(enumd);
}
Statement::ExportDeclaration(export) => {
if let Declaration::VariableDeclaration(decl) = &export.declaration {
self.collect_literal_constants(decl);
}
let declared = self.collect_declared_registries(&export.declaration);
exports.extend(declared.into_iter().map(|name| (name.clone(), name)));
}
Statement::ExportNamedDeclaration(export) => {
collect_exported_names(export, &mut exports);
}
_ => {}
}
}
for (local, exported) in exports {
if let Some(members) = self.local_registries.get(&local) {
self.exported_registries.push(FlagKeyRegistry {
export_name: exported,
members: members.clone(),
});
}
}
}
fn collect_declared_registries(&mut self, declaration: &Declaration<'_>) -> Vec<String> {
match declaration {
Declaration::VariableDeclaration(decl) => self.collect_const_object_registries(decl),
Declaration::TSEnumDeclaration(enumd) => {
self.collect_enum_registry(enumd).into_iter().collect()
}
_ => Vec::new(),
}
}
fn collect_const_object_registries(&mut self, decl: &VariableDeclaration<'_>) -> Vec<String> {
let mut names = Vec::new();
if !decl.kind.is_const() {
return names;
}
for declarator in &decl.declarations {
let BindingPattern::BindingIdentifier(id) = &declarator.id else {
continue;
};
let Some(members) = declarator.init.as_ref().and_then(as_const_object_members) else {
continue;
};
let name = id.name.to_string();
self.local_registries.insert(name.clone(), members);
names.push(name);
}
names
}
fn collect_literal_constants(&mut self, decl: &VariableDeclaration<'_>) {
if !decl.kind.is_const() {
return;
}
for declarator in &decl.declarations {
let BindingPattern::BindingIdentifier(id) = &declarator.id else {
continue;
};
if !self.is_flag_env_name(id.name.as_str()) {
continue;
}
let Some(value) = declarator.init.as_ref().and_then(literal_source) else {
continue;
};
let (line, col) = byte_offset_to_line_col(self.line_offsets, id.span.start);
self.literal_consts
.insert(id.name.to_string(), self.constants.len());
self.constants.push(FlagConstant {
name: id.name.to_string(),
value,
line,
col,
reads: Vec::new(),
});
}
}
fn record_constant_read(&mut self, ident: &IdentifierReference<'_>, guard: Guard) {
let name = ident.name.as_str();
let Some(&index) = self.literal_consts.get(name) else {
return;
};
if !self.guard_operands.contains(&ident.span.start) {
return;
}
if self
.shadowed_registries
.iter()
.any(|names| names.contains(name))
{
return;
}
let (line, col) = byte_offset_to_line_col(self.line_offsets, ident.span.start);
if let Some(constant) = self.constants.get_mut(index) {
constant.reads.push(FlagConstantRead {
line,
col,
facts: guard.facts,
});
}
}
fn collect_enum_registry(&mut self, enumd: &TSEnumDeclaration<'_>) -> Option<String> {
let members: Vec<(String, String)> = enumd
.body
.members
.iter()
.filter_map(|member| {
let value = string_value(member.initializer.as_ref()?)?;
let name = match &member.id {
TSEnumMemberName::Identifier(id) => id.name.to_string(),
TSEnumMemberName::String(name) | TSEnumMemberName::ComputedString(name) => {
name.value.to_string()
}
TSEnumMemberName::ComputedTemplateString(_) => return None,
};
Some((name, value))
})
.collect();
if members.is_empty() {
return None;
}
let name = enumd.id.name.to_string();
self.local_registries.insert(name.clone(), members);
Some(name)
}
fn check_config_object(&mut self, expr: &StaticMemberExpression<'_>) -> bool {
if !self.config_object_heuristics {
return false;
}
let Some((obj_name, prop_name)) = extract_config_object_access(expr) else {
return false;
};
if !CONFIG_OBJECT_KEYWORDS
.iter()
.any(|kw| obj_name.eq_ignore_ascii_case(kw) || prop_name.eq_ignore_ascii_case(kw))
{
return false;
}
self.push_flag_use(
format!("{obj_name}.{prop_name}"),
FlagUseKind::ConfigObject,
expr.span.start,
None,
);
true
}
fn is_flag_env_name(&self, name: &str) -> bool {
BUILTIN_ENV_PREFIXES
.iter()
.any(|prefix| name.starts_with(prefix))
|| self
.extra_env_prefixes
.iter()
.any(|prefix| name.starts_with(prefix.as_str()))
}
fn visit_guard_test<'b>(&mut self, test: &Expression<'b>, guard: Guard)
where
Self: Visit<'b>,
{
let outer = self.current_guard.replace(guard);
let outer_operands = if self.literal_consts.is_empty() {
None
} else {
let mut operands = Vec::new();
collect_direct_operands(test, &mut operands);
Some(std::mem::replace(&mut self.guard_operands, operands))
};
self.visit_expression(test);
if let Some(outer_operands) = outer_operands {
self.guard_operands = outer_operands;
}
self.current_guard = outer;
}
fn lookup_binding(&self, name: &str) -> Option<FlagRef> {
if self.flag_binding_count == 0 {
return None;
}
self.binding_scopes
.iter()
.rev()
.find_map(|scope| scope.get(name))
.copied()
.flatten()
}
fn bind(&mut self, name: &str, flag_ref: Option<FlagRef>) {
if flag_ref.is_none() && self.lookup_binding(name).is_none() {
return;
}
if flag_ref.is_some() {
self.flag_binding_count += 1;
}
if let Some(scope) = self.binding_scopes.last_mut() {
scope.insert(name.to_string(), flag_ref);
}
}
fn attach_guard(&mut self, flag_ref: FlagRef, guard: Guard) {
let flag_use = match flag_ref {
FlagRef::Resolved(index) => self.results.get_mut(index),
FlagRef::Registry(index) => self
.registry_reads
.get_mut(index)
.map(|read| &mut read.flag_use),
};
if let Some(flag_use) = flag_use
&& flag_use.guard_span_start.is_none()
{
flag_use.guard_span_start = Some(guard.start);
flag_use.guard_span_end = Some(guard.end);
flag_use.facts = guard.facts;
}
}
fn visit_function_scope(&mut self, walk_scope: impl FnOnce(&mut Self)) {
self.binding_scopes.push(FxHashMap::default());
self.shadowed_registries.push(FxHashSet::default());
walk_scope(self);
self.shadowed_registries.pop();
self.binding_scopes.pop();
}
fn note_binding(&mut self, name: &str) {
let is_registry_name = self.local_registries.contains_key(name)
|| self.named_imports.contains(name)
|| self.literal_consts.contains_key(name);
if !is_registry_name {
return;
}
if let Some(names) = self.shadowed_registries.last_mut() {
names.insert(name.to_string());
}
}
fn visit_block(&mut self, end: u32, walk_block: impl FnOnce(&mut Self)) {
self.block_ends.push(end);
walk_block(self);
self.block_ends.pop();
}
}
impl<'a> Visit<'a> for FlagVisitor<'_> {
fn visit_program(&mut self, program: &Program<'a>) {
self.collect_imports(program);
self.collect_flag_registries(program);
self.visit_block(program.span.end, |visitor| {
walk::walk_program(visitor, program);
});
}
fn visit_import_declaration(&mut self, decl: &ImportDeclaration<'a>) {
self.collect_vercel_flags_import(decl);
}
fn visit_if_statement(&mut self, stmt: &IfStatement<'a>) {
let guard_end = if stmt.alternate.is_none()
&& is_negated_test(&stmt.test)
&& exits_block(&stmt.consequent)
{
self.block_ends
.last()
.map_or(stmt.span.end, |&end| end.max(stmt.span.end))
} else {
stmt.span.end
};
let facts = FlagSiteFacts::default()
.with_empty_branch(
is_empty_statement(&stmt.consequent)
&& stmt.alternate.as_ref().is_none_or(is_empty_statement),
)
.with_identical_branches(
stmt.alternate
.as_ref()
.is_some_and(|alternate| stmt.consequent.content_eq(alternate)),
);
self.visit_guard_test(
&stmt.test,
Guard {
start: stmt.span.start,
end: guard_end,
facts,
},
);
self.visit_statement(&stmt.consequent);
if let Some(alt) = &stmt.alternate {
self.visit_statement(alt);
}
}
fn visit_conditional_expression(&mut self, expr: &ConditionalExpression<'a>) {
let facts = FlagSiteFacts::default()
.with_empty_branch(
is_empty_value(&expr.consequent, &expr.alternate)
&& is_empty_value(&expr.alternate, &expr.consequent),
)
.with_identical_branches(expr.consequent.content_eq(&expr.alternate));
self.visit_guard_test(
&expr.test,
Guard {
start: expr.span.start,
end: expr.span.end,
facts,
},
);
self.visit_expression(&expr.consequent);
self.visit_expression(&expr.alternate);
}
fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) {
if expr.operator == LogicalOperator::And && is_jsx(&expr.right) {
let facts = FlagSiteFacts::default().with_empty_branch(is_empty_fragment(&expr.right));
self.visit_guard_test(
&expr.left,
Guard {
start: expr.span.start,
end: expr.span.end,
facts,
},
);
self.visit_expression(&expr.right);
return;
}
walk::walk_logical_expression(self, expr);
}
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
self.check_sdk_call(call);
walk::walk_call_expression(self, call);
}
fn visit_member_expression(&mut self, expr: &MemberExpression<'a>) {
if let MemberExpression::StaticMemberExpression(static_expr) = expr {
self.check_env_var(static_expr);
if self.check_config_object(static_expr) {
return;
}
}
walk::walk_member_expression(self, expr);
}
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
let Some(guard) = self.current_guard else {
return;
};
if let Some(flag_ref) = self.lookup_binding(ident.name.as_str()) {
self.attach_guard(flag_ref, guard);
}
if !self.literal_consts.is_empty() {
self.record_constant_read(ident, guard);
}
}
fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) {
let outer = std::mem::replace(&mut self.in_const_declaration, decl.kind.is_const());
walk::walk_variable_declaration(self, decl);
self.in_const_declaration = outer;
}
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
let before = self.read_count();
walk::walk_variable_declarator(self, decl);
let BindingPattern::BindingIdentifier(id) = &decl.id else {
return;
};
self.record_definition(decl, id.name.as_str());
let flag_ref = (self.in_const_declaration
&& self.read_count() == before + 1
&& decl.init.as_ref().and_then(flag_value_read_start) == self.last_read_start)
.then_some(self.last_ref)
.flatten();
self.bind(id.name.as_str(), flag_ref);
}
fn visit_function(&mut self, func: &Function<'a>, flags: ScopeFlags) {
self.visit_function_scope(|visitor| walk::walk_function(visitor, func, flags));
}
fn visit_arrow_function_expression(&mut self, func: &ArrowFunctionExpression<'a>) {
self.visit_function_scope(|visitor| walk::walk_arrow_function_expression(visitor, func));
}
fn visit_function_body(&mut self, body: &FunctionBody<'a>) {
self.visit_block(body.span.end, |visitor| {
walk::walk_function_body(visitor, body);
});
}
fn visit_block_statement(&mut self, block: &BlockStatement<'a>) {
self.shadowed_registries.push(FxHashSet::default());
self.visit_block(block.span.end, |visitor| {
walk::walk_block_statement(visitor, block);
});
self.shadowed_registries.pop();
}
fn visit_binding_identifier(&mut self, ident: &BindingIdentifier<'a>) {
self.note_binding(ident.name.as_str());
}
}
fn is_flag_source(source: &str) -> bool {
let source = source.to_ascii_lowercase();
FLAG_SOURCE_MARKERS
.iter()
.any(|marker| source.contains(marker))
}
fn imports_values(decl: &ImportDeclaration<'_>) -> bool {
if decl.import_kind.is_type() {
return false;
}
let Some(specifiers) = &decl.specifiers else {
return true;
};
specifiers.is_empty()
|| specifiers.iter().any(|spec| match spec {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
!specifier.import_kind.is_type()
}
_ => true,
})
}
fn import_equals_flag_source(decl: &TSImportEqualsDeclaration<'_>) -> bool {
if decl.import_kind.is_type() {
return false;
}
let TSModuleReference::ExternalModuleReference(reference) = &decl.module_reference else {
return false;
};
is_flag_source(reference.expression.value.as_str())
}
fn requires_flag_source(decl: &VariableDeclaration<'_>) -> bool {
decl.declarations
.iter()
.filter_map(|declarator| declarator.init.as_ref())
.filter_map(required_source)
.any(is_flag_source)
}
fn required_source<'b>(expr: &'b Expression<'_>) -> Option<&'b str> {
let call = match unwrap_value(expr) {
Expression::CallExpression(call) => call,
Expression::StaticMemberExpression(member) => match unwrap_value(&member.object) {
Expression::CallExpression(call) => call,
_ => return None,
},
_ => return None,
};
let Expression::Identifier(callee) = &call.callee else {
return None;
};
if callee.name != "require" {
return None;
}
match call.arguments.first() {
Some(Argument::StringLiteral(source)) => Some(source.value.as_str()),
_ => None,
}
}
fn is_vercel_flags_source(source: &str) -> bool {
source == "flags"
|| source.starts_with("flags/")
|| source == "@vercel/flags"
|| source.starts_with("@vercel/flags/")
}
fn unwrap_value<'b, 'a>(mut expr: &'b Expression<'a>) -> &'b Expression<'a> {
loop {
expr = match expr {
Expression::ParenthesizedExpression(inner) => &inner.expression,
Expression::TSAsExpression(inner) => &inner.expression,
Expression::TSSatisfiesExpression(inner) => &inner.expression,
Expression::TSNonNullExpression(inner) => &inner.expression,
_ => return expr,
};
}
}
fn is_jsx(expr: &Expression<'_>) -> bool {
matches!(
unwrap_value(expr),
Expression::JSXElement(_) | Expression::JSXFragment(_)
)
}
fn collect_direct_operands(test: &Expression<'_>, operands: &mut Vec<u32>) {
match unwrap_value(test) {
Expression::Identifier(ident) => operands.push(ident.span.start),
Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::LogicalNot => {
collect_direct_operands(&unary.argument, operands);
}
Expression::LogicalExpression(logical) => {
collect_direct_operands(&logical.left, operands);
collect_direct_operands(&logical.right, operands);
}
Expression::BinaryExpression(binary) if binary.operator.is_equality() => {
if is_literal(&binary.right) {
collect_direct_operands(&binary.left, operands);
} else if is_literal(&binary.left) {
collect_direct_operands(&binary.right, operands);
}
}
_ => {}
}
}
fn is_empty_statement(stmt: &Statement<'_>) -> bool {
match stmt {
Statement::EmptyStatement(_) => true,
Statement::BlockStatement(block) => block.body.is_empty(),
_ => false,
}
}
fn is_empty_value(arm: &Expression<'_>, other: &Expression<'_>) -> bool {
match unwrap_value(arm) {
Expression::NullLiteral(_) => true,
Expression::Identifier(id) => id.name == "undefined",
Expression::UnaryExpression(unary) => unary.operator == UnaryOperator::Void,
Expression::BooleanLiteral(boolean) => !boolean.value && is_jsx(other),
_ => is_empty_fragment(arm),
}
}
fn is_empty_fragment(expr: &Expression<'_>) -> bool {
matches!(unwrap_value(expr), Expression::JSXFragment(fragment) if fragment.children.is_empty())
}
fn is_negated_test(expr: &Expression<'_>) -> bool {
matches!(
unwrap_value(expr),
Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::LogicalNot
)
}
fn exits_block(stmt: &Statement<'_>) -> bool {
match stmt {
Statement::ReturnStatement(_) | Statement::ThrowStatement(_) => true,
Statement::BlockStatement(block) => block.body.last().is_some_and(exits_block),
_ => false,
}
}
fn flag_value_read_start(expr: &Expression<'_>) -> Option<u32> {
match unwrap_value(expr) {
Expression::AwaitExpression(inner) => flag_value_read_start(&inner.argument),
Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::LogicalNot => {
flag_value_read_start(&unary.argument)
}
Expression::BinaryExpression(binary) if binary.operator.is_equality() => {
if is_literal(&binary.right) {
flag_value_read_start(&binary.left)
} else if is_literal(&binary.left) {
flag_value_read_start(&binary.right)
} else {
None
}
}
Expression::CallExpression(call) => Some(call.span.start),
Expression::StaticMemberExpression(member) => Some(member.span.start),
_ => None,
}
}
fn literal_source(expr: &Expression<'_>) -> Option<String> {
match unwrap_value(expr) {
Expression::BooleanLiteral(boolean) => Some(boolean.value.to_string()),
Expression::NumericLiteral(number) => Some(
number
.raw
.as_ref()
.map_or_else(|| number.value.to_string(), ToString::to_string),
),
other => string_value(other).map(|value| format!("'{value}'")),
}
}
fn is_literal(expr: &Expression<'_>) -> bool {
matches!(
unwrap_value(expr),
Expression::StringLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NumericLiteral(_)
| Expression::NullLiteral(_)
)
}
fn as_const_object_members(init: &Expression<'_>) -> Option<Vec<(String, String)>> {
let mut expr = init;
loop {
expr = match expr {
Expression::ParenthesizedExpression(inner) => &inner.expression,
Expression::TSSatisfiesExpression(inner) => &inner.expression,
_ => break,
};
}
let Expression::TSAsExpression(as_expr) = expr else {
return None;
};
if !as_expr.type_annotation.is_const_type_reference() {
return None;
}
let Expression::ObjectExpression(object) = unwrap_value(&as_expr.expression) else {
return None;
};
let members: Vec<(String, String)> = object
.properties
.iter()
.filter_map(|property| {
let ObjectPropertyKind::ObjectProperty(property) = property else {
return None;
};
if property.computed {
return None;
}
let name = property.key.static_name()?.to_string();
Some((name, string_value(&property.value)?))
})
.collect();
(!members.is_empty()).then_some(members)
}
fn string_value(expr: &Expression<'_>) -> Option<String> {
match unwrap_value(expr) {
Expression::StringLiteral(lit) => Some(lit.value.to_string()),
Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
.quasis
.first()
.and_then(|quasi| quasi.value.cooked.as_ref())
.map(ToString::to_string),
_ => None,
}
}
fn collect_exported_names(
export: &ExportNamedDeclaration<'_>,
exports: &mut Vec<(String, String)>,
) {
if export.export_kind.is_type() {
return;
}
for spec in &export.specifiers {
if !spec.export_kind.is_type() {
exports.push((
spec.local.name().to_string(),
spec.exported.name().to_string(),
));
}
}
}
fn extract_env_name<'b>(expr: &'b StaticMemberExpression<'_>) -> Option<&'b str> {
let Expression::StaticMemberExpression(inner) = &expr.object else {
return None;
};
if inner.property.name.as_str() != "env" {
return None;
}
let is_env_object = match &inner.object {
Expression::Identifier(id) => id.name.as_str() == "process",
Expression::ImportMeta(_) => true,
_ => false,
};
is_env_object.then(|| expr.property.name.as_str())
}
fn extract_string_arg(args: &[Argument<'_>], index: usize) -> Option<String> {
args.get(index).and_then(|arg| {
if let Argument::StringLiteral(lit) = arg {
Some(lit.value.to_string())
} else {
None
}
})
}
fn extract_flag_name_arg(args: &[Argument<'_>], index: usize) -> Option<FlagNameArg> {
match args.get(index)? {
Argument::StringLiteral(lit) => Some(FlagNameArg::Literal(lit.value.to_string())),
Argument::StaticMemberExpression(member) => {
let Expression::Identifier(object) = &member.object else {
return None;
};
Some(FlagNameArg::RegistryMember {
registry: object.name.to_string(),
member: member.property.name.to_string(),
})
}
Argument::ComputedMemberExpression(member) => {
let (Expression::Identifier(object), Expression::StringLiteral(key)) =
(&member.object, &member.expression)
else {
return None;
};
Some(FlagNameArg::RegistryMember {
registry: object.name.to_string(),
member: key.value.to_string(),
})
}
_ => None,
}
}
fn extract_object_string_property_arg(
args: &[Argument<'_>],
index: usize,
property_name: &str,
) -> Option<String> {
let Some(Argument::ObjectExpression(obj)) = args.get(index) else {
return None;
};
for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
continue;
};
if prop
.key
.static_name()
.is_some_and(|key| key.as_ref() == property_name)
&& let Expression::StringLiteral(lit) = &prop.value
{
return Some(lit.value.to_string());
}
}
None
}
fn extract_config_object_access(expr: &StaticMemberExpression<'_>) -> Option<(String, String)> {
let prop_name = expr.property.name.to_string();
match &expr.object {
Expression::Identifier(id) => Some((id.name.to_string(), prop_name)),
Expression::StaticMemberExpression(inner) => {
if matches!(&inner.object, Expression::Identifier(_)) {
Some((inner.property.name.to_string(), prop_name))
} else {
None
}
}
_ => None,
}
}
#[derive(Debug, Default)]
pub(crate) struct ExtractedFlags {
pub flag_uses: Vec<FlagUse>,
pub registry_facts: Option<Box<FlagRegistryFacts>>,
}
pub(crate) fn extract_flags(
program: &Program<'_>,
line_offsets: &[u32],
patterns: &FlagPatterns,
) -> ExtractedFlags {
let mut visitor = FlagVisitor::new(
line_offsets,
&patterns.sdk_patterns,
&patterns.env_prefixes,
patterns.config_object_heuristics,
);
visitor.visit_program(program);
visitor.mark_unconfirmed_reads();
let registry_facts = FlagRegistryFacts {
registries: visitor.exported_registries,
reads: visitor.registry_reads,
constants: visitor
.constants
.into_iter()
.filter(|constant| !constant.reads.is_empty())
.collect(),
definitions: visitor.definitions,
};
ExtractedFlags {
flag_uses: visitor.results,
registry_facts: (!registry_facts.is_empty()).then(|| Box::new(registry_facts)),
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_span::SourceType;
fn extract_from_source(source: &str) -> Vec<FlagUse> {
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
extract_flags(
&parser_return.program,
&line_offsets,
&FlagPatterns::default(),
)
.flag_uses
}
fn extract_with_config_objects(source: &str) -> Vec<FlagUse> {
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
extract_flags(
&parser_return.program,
&line_offsets,
&FlagPatterns {
config_object_heuristics: true,
..FlagPatterns::default()
},
)
.flag_uses
}
#[test]
fn detects_process_env_feature_flag() {
let flags = extract_from_source("if (process.env.FEATURE_NEW_CHECKOUT) { doStuff(); }");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "FEATURE_NEW_CHECKOUT");
assert_eq!(flags[0].kind, FlagUseKind::EnvVar);
assert!(flags[0].guard_span_start.is_some());
}
#[test]
fn detects_next_public_enable_prefix() {
let flags = extract_from_source("if (process.env.NEXT_PUBLIC_ENABLE_BETA) {}");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "NEXT_PUBLIC_ENABLE_BETA");
}
#[test]
fn ignores_non_flag_env_vars() {
let flags = extract_from_source("const url = process.env.DATABASE_URL;");
assert!(flags.is_empty());
}
#[test]
fn detects_negated_env_flag() {
let flags = extract_from_source("if (!process.env.FEATURE_X) { fallback(); }");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "FEATURE_X");
}
#[test]
fn detects_launchdarkly_use_flag() {
let flags = extract_from_source("const flag = useFlag('new-checkout');");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "new-checkout");
assert_eq!(flags[0].kind, FlagUseKind::SdkCall);
assert_eq!(flags[0].sdk_name.as_deref(), Some("LaunchDarkly"));
}
#[test]
fn detects_statsig_use_gate() {
let flags = extract_from_source("if (useGate('beta-feature')) {}");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "beta-feature");
assert_eq!(flags[0].sdk_name.as_deref(), Some("Statsig"));
}
#[test]
fn detects_unleash_is_enabled() {
let flags = extract_from_source("client.isEnabled('feature-x')");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "feature-x");
}
#[test]
fn detects_growthbook_get_feature_value() {
let flags = extract_from_source("const val = getFeatureValue('parser', false);");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "parser");
assert_eq!(flags[0].sdk_name.as_deref(), Some("GrowthBook"));
}
#[test]
fn detects_posthog_hooks() {
let flags = extract_from_source(
"const enabled = useFeatureFlagEnabled('new-checkout');\n\
const payload = useFeatureFlagPayload('checkout-copy');\n\
const variant = useFeatureFlagVariantKey('pricing-test');",
);
let names: Vec<_> = flags.iter().map(|flag| flag.flag_name.as_str()).collect();
assert_eq!(names, ["new-checkout", "checkout-copy", "pricing-test"]);
assert!(
flags
.iter()
.all(|flag| flag.sdk_name.as_deref() == Some("PostHog"))
);
}
#[test]
fn detects_vercel_flags_object_key_and_core_evaluate_from_imports() {
let flags = extract_from_source(
"import { flag, evaluate as evalFlag } from 'flags/next';\n\
export const showSale = flag({ key: 'summer-sale', decide: () => false });\n\
const value = await evalFlag('show-new-feature', false);",
);
let names: Vec<_> = flags.iter().map(|flag| flag.flag_name.as_str()).collect();
assert_eq!(names, ["summer-sale", "show-new-feature"]);
assert!(
flags
.iter()
.all(|flag| flag.sdk_name.as_deref() == Some("Vercel Flags"))
);
}
#[test]
fn detects_vercel_flags_namespace_imports() {
let flags = extract_from_source(
"import * as vercelFlags from '@vercel/flags';\n\
const value = await vercelFlags.evaluate('show-new-feature', false);\n\
export const showSale = vercelFlags.flag({ key: 'summer-sale', decide: () => false });",
);
let names: Vec<_> = flags.iter().map(|flag| flag.flag_name.as_str()).collect();
assert_eq!(names, ["show-new-feature", "summer-sale"]);
assert!(
flags
.iter()
.all(|flag| flag.sdk_name.as_deref() == Some("Vercel Flags"))
);
}
#[test]
fn detects_vercel_flags_calls_before_import_declaration() {
let flags = extract_from_source(
"export const showSale = flag({ key: 'summer-sale', decide: () => false });\n\
import { flag } from 'flags/next';",
);
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "summer-sale");
assert_eq!(flags[0].sdk_name.as_deref(), Some("Vercel Flags"));
}
#[test]
fn ignores_unimported_vercel_like_function_names() {
let flags = extract_from_source(
"function math() { return evaluate('2 + 2'); }\n\
function marker() { return flag({ key: 'ui-row' }); }",
);
assert!(flags.is_empty());
}
#[test]
fn detects_configcat_detail_evaluation() {
let flags = extract_from_source(
"const details = await client.getValueDetailsAsync('new-checkout', false);",
);
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "new-checkout");
assert_eq!(flags[0].sdk_name.as_deref(), Some("ConfigCat"));
}
#[test]
fn detects_optimizely_decisions_and_variables() {
let flags = extract_from_source(
"const [decision] = useDecision('checkout-flow');\n\
const copy = optimizelyClient.getFeatureVariableString('checkout-flow', 'copy', userId, attrs);\n\
const json = optimizelyClient.getFeatureVariableJson('checkout-flow', 'json', userId, attrs);",
);
assert_eq!(flags.len(), 3);
assert!(flags.iter().all(|flag| flag.flag_name == "checkout-flow"));
assert!(
flags
.iter()
.all(|flag| flag.sdk_name.as_deref() == Some("Optimizely"))
);
}
#[test]
fn detects_eppo_typed_assignments() {
let flags = extract_from_source(
"const value = client.getBooleanAssignment('new-onboarding', subject, {}, false);\n\
const details = client.getStringAssignmentDetails('copy-test', subject, {}, 'control');\n\
const payload = client.getJSONAssignmentDetails('payload-test', subject, {}, {});",
);
let names: Vec<_> = flags.iter().map(|flag| flag.flag_name.as_str()).collect();
assert_eq!(names, ["new-onboarding", "copy-test", "payload-test"]);
assert!(
flags
.iter()
.all(|flag| flag.sdk_name.as_deref() == Some("Eppo"))
);
}
#[test]
fn ignores_sdk_call_without_string_arg() {
let flags = extract_from_source("useFlag(dynamicKey);");
assert!(flags.is_empty());
}
#[test]
fn config_objects_off_by_default() {
let flags = extract_from_source("if (config.features.newCheckout) {}");
assert!(flags.is_empty());
}
#[test]
fn detects_config_features_when_enabled() {
let flags = extract_with_config_objects("if (config.features.newCheckout) {}");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "features.newCheckout");
assert_eq!(flags[0].kind, FlagUseKind::ConfigObject);
}
#[test]
fn detects_flags_object() {
let flags = extract_with_config_objects("if (flags.enableV2) {}");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "flags.enableV2");
}
#[test]
fn ignores_non_flag_config_object() {
let flags = extract_with_config_objects("const host = config.database.host;");
assert!(flags.is_empty());
}
#[test]
fn captures_if_guard_span() {
let source = "if (process.env.FEATURE_X) {\n doStuff();\n}";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert!(flags[0].guard_span_start.is_some());
assert!(flags[0].guard_span_end.is_some());
}
#[test]
fn captures_ternary_guard_span() {
let source = "const x = useFlag('beta') ? newFlow() : oldFlow();";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert!(flags[0].guard_span_start.is_some());
}
#[test]
fn detects_custom_sdk_pattern() {
let allocator = Allocator::default();
let source = "isFeatureActive('my-flag');";
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let custom = FlagPatterns {
sdk_patterns: vec![("isFeatureActive".to_string(), 0, "Internal".to_string())],
..FlagPatterns::default()
};
let flags = extract_flags(&parser_return.program, &line_offsets, &custom).flag_uses;
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "my-flag");
assert_eq!(flags[0].sdk_name.as_deref(), Some("Internal"));
}
#[test]
fn custom_sdk_pattern_can_use_vercel_object_function_name() {
let allocator = Allocator::default();
let source = "flag('internal-flag');";
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let custom = FlagPatterns {
sdk_patterns: vec![("flag".to_string(), 0, "Internal".to_string())],
..FlagPatterns::default()
};
let flags = extract_flags(&parser_return.program, &line_offsets, &custom).flag_uses;
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "internal-flag");
assert_eq!(flags[0].sdk_name.as_deref(), Some("Internal"));
}
#[test]
fn detects_custom_env_prefix() {
let allocator = Allocator::default();
let source = "if (process.env.MYAPP_ENABLE_V2) {}";
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let custom = FlagPatterns {
env_prefixes: vec!["MYAPP_ENABLE_".to_string()],
..FlagPatterns::default()
};
let flags = extract_flags(&parser_return.program, &line_offsets, &custom).flag_uses;
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "MYAPP_ENABLE_V2");
}
fn extract_facts(source: &str) -> ExtractedFlags {
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
extract_flags(
&parser_return.program,
&line_offsets,
&FlagPatterns::default(),
)
}
fn extract_with_sdk_patterns(source: &str, names: &[&str]) -> Vec<FlagUse> {
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, SourceType::tsx()).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let patterns = FlagPatterns {
sdk_patterns: names
.iter()
.map(|name| ((*name).to_string(), 0, "InHouse".to_string()))
.collect(),
..FlagPatterns::default()
};
extract_flags(&parser_return.program, &line_offsets, &patterns).flag_uses
}
fn unconfirmed(flags: &[FlagUse]) -> Vec<bool> {
flags
.iter()
.map(|flag| flag.facts.unconfirmed_sdk())
.collect()
}
#[test]
fn generic_sdk_names_without_an_sdk_import_are_unconfirmed() {
let flags = extract_from_source(
"client.isEnabled('a');\n\
getValue('b');\n\
useFeature('c');",
);
assert_eq!(unconfirmed(&flags), vec![true, true, true]);
}
#[test]
fn specific_sdk_names_without_an_import_stay_confirmed() {
let flags = extract_from_source(
"useFlag('a');\n\
checkGate('b');\n\
getFeatureValue('c');",
);
assert_eq!(unconfirmed(&flags), vec![false, false, false]);
}
#[test]
fn a_vendor_sdk_import_confirms_generic_sdk_names() {
let flags = extract_from_source(
"import { useUnleashClient } from '@unleash/proxy-client-react';\n\
const client = useUnleashClient();\n\
client.isEnabled('a');",
);
assert_eq!(unconfirmed(&flags), vec![false]);
}
#[test]
fn a_flag_module_import_confirms_generic_sdk_names() {
for source in [
"import { getValue } from './featureFlags';\ngetValue('a');",
"import flags from '@/lib/flags';\nflags.getValue('a');",
"import { useFeature } from '../toggles/client';\nuseFeature('a');",
] {
let flags = extract_from_source(source);
assert_eq!(unconfirmed(&flags), vec![false], "{source}");
}
}
#[test]
fn a_top_level_require_of_an_sdk_confirms_generic_sdk_names() {
let flags = extract_from_source(
"const { initialize } = require('unleash-client');\n\
const client = initialize({});\n\
client.isEnabled('a');",
);
assert_eq!(unconfirmed(&flags), vec![false]);
}
#[test]
fn an_unrelated_import_does_not_confirm_generic_sdk_names() {
let flags = extract_from_source(
"import { form } from './form';\n\
import type { Flags } from './flags';\n\
form.getValue('email');",
);
assert_eq!(unconfirmed(&flags), vec![true]);
}
#[test]
fn an_import_equals_require_of_an_sdk_confirms_generic_sdk_names() {
let flags = extract_from_source(
"import unleash = require('unleash-client');\n\
const client = unleash.initialize({});\n\
client.isEnabled('a');",
);
assert_eq!(unconfirmed(&flags), vec![false]);
}
#[test]
fn an_import_of_only_inline_types_does_not_confirm_generic_sdk_names() {
let flags = extract_from_source(
"import { type Flags, type Keys } from './flags';\n\
form.getValue('email');",
);
assert_eq!(unconfirmed(&flags), vec![true]);
}
#[test]
fn a_side_effect_or_mixed_import_of_a_flag_module_confirms_generic_sdk_names() {
for source in [
"import './flags';\ngetValue('a');",
"import { type Flags, getValue } from './flags';\ngetValue('a');",
] {
let flags = extract_from_source(source);
assert_eq!(unconfirmed(&flags), vec![false], "{source}");
}
}
#[test]
fn guarded_and_bound_generic_sites_stay_unconfirmed() {
let flags = extract_from_source(
"if (client.isEnabled('a')) { run(); }\n\
function view() {\n\
const on = getValue('b');\n\
if (on) { run(); }\n\
}",
);
assert_eq!(flags.len(), 2);
assert!(flags.iter().all(|flag| flag.guard_span_start.is_some()));
assert_eq!(unconfirmed(&flags), vec![true, true]);
}
#[test]
fn a_generic_name_in_sdk_patterns_is_confirmed() {
let flags = extract_with_sdk_patterns("client.isEnabled('a');", &["isEnabled"]);
assert_eq!(unconfirmed(&flags), vec![false]);
}
#[test]
fn generic_registry_reads_without_an_sdk_import_are_unconfirmed() {
let facts = extract_facts(
"import { FLAGS } from './keys';\n\
const LOCAL = { A: 'a' } as const;\n\
isEnabled(LOCAL.A);\n\
isEnabled(FLAGS.B);",
);
assert_eq!(unconfirmed(&facts.flag_uses), vec![true]);
let reads = &facts.registry_facts.expect("registry facts").reads;
assert_eq!(reads.len(), 1);
assert!(reads[0].flag_use.facts.unconfirmed_sdk());
}
fn guard_text<'s>(source: &'s str, flag: &FlagUse) -> &'s str {
let start = flag.guard_span_start.expect("guard start") as usize;
let end = flag.guard_span_end.expect("guard end") as usize;
&source[start..end]
}
#[test]
fn detects_import_meta_env_flag() {
let flags = extract_from_source("if (import.meta.env.VITE_FEATURE_CHAT) { chat(); }");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "VITE_FEATURE_CHAT");
assert_eq!(flags[0].kind, FlagUseKind::EnvVar);
assert!(flags[0].guard_span_start.is_some());
}
#[test]
fn ignores_non_flag_import_meta_env() {
let flags = extract_from_source(
"const url = import.meta.env.VITE_API_URL;\nconst other = import.meta.url;",
);
assert!(flags.is_empty());
}
#[test]
fn resolves_local_as_const_registry_member() {
let flags = extract_from_source(
"const FLAGS = { NewCheckout: 'new-checkout', Beta: `beta` } as const;\n\
const a = useFlag(FLAGS.NewCheckout);\n\
const b = useFlag(FLAGS['Beta']);",
);
let names: Vec<_> = flags.iter().map(|flag| flag.flag_name.as_str()).collect();
assert_eq!(names, ["new-checkout", "beta"]);
assert!(
flags
.iter()
.all(|flag| flag.sdk_name.as_deref() == Some("LaunchDarkly"))
);
}
#[test]
fn resolves_local_enum_registry_member() {
let flags = extract_from_source(
"enum Gates { Beta = 'beta-gate', Count = 3 }\n\
if (useGate(Gates.Beta)) {}\n\
useGate(Gates.Count);",
);
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "beta-gate");
assert!(flags[0].guard_span_start.is_some());
}
#[test]
fn mutable_object_is_not_a_local_registry() {
let facts = extract_facts(
"const FLAGS = { NewCheckout: 'new-checkout' };\nuseFlag(FLAGS.NewCheckout);",
);
assert!(facts.flag_uses.is_empty());
assert!(facts.registry_facts.is_none());
}
#[test]
fn keeps_imported_registry_read_for_project_analysis() {
let source = "import { FLAGS } from './flags';\n\
if (useFlag(FLAGS.NewCheckout)) { render(); }";
let facts = extract_facts(source);
assert!(facts.flag_uses.is_empty());
let reads = &facts.registry_facts.expect("registry facts").reads;
assert_eq!(reads.len(), 1);
assert_eq!(reads[0].registry, "FLAGS");
assert_eq!(reads[0].member, "NewCheckout");
assert_eq!(reads[0].flag_use.sdk_name.as_deref(), Some("LaunchDarkly"));
assert_eq!(reads[0].flag_use.line, 2);
assert!(reads[0].flag_use.guard_span_start.is_some());
}
#[test]
fn keeps_registry_reads_only_for_named_value_imports() {
let facts = extract_facts(
"import type { TypeFlags } from './types';\n\
import * as all from './flags';\n\
function View(props) {\n\
useFlag(props.flagKey);\n\
useFlag(TypeFlags.A);\n\
useFlag(all.B);\n\
}",
);
assert!(facts.flag_uses.is_empty());
assert!(facts.registry_facts.is_none());
}
#[test]
fn records_exported_registries_only() {
let facts = extract_facts(
"export const FLAGS = { A: 'a' } as const satisfies Record<string, string>;\n\
export enum Gates { B = 'b' }\n\
const Local = { C: 'c' } as const;\n\
const Hidden = { D: 'd' } as const;\n\
export { Local as Renamed };\n\
export const Plain = { E: 'e' };",
);
let registries = facts.registry_facts.expect("registry facts").registries;
let names: Vec<_> = registries
.iter()
.map(|registry| registry.export_name.as_str())
.collect();
assert_eq!(names, ["FLAGS", "Gates", "Renamed"]);
assert_eq!(registries[2].members, [("C".to_string(), "c".to_string())]);
}
#[test]
fn a_binding_that_shadows_a_registry_is_not_a_registry() {
let facts = extract_facts(
"import { FLAGS } from './flags';\n\
const LOCAL = { A: 'local-a' } as const;\n\
export function f(LOCAL) { return useFlag(LOCAL.A); }\n\
function g(FLAGS) { return useFlag(FLAGS.Chat); }\n\
const h = () => { const LOCAL = pick(); return useFlag(LOCAL.A); };\n\
function k() { try { run(); } catch (FLAGS) { useFlag(FLAGS.Chat); } }\n\
export const outer = useFlag(LOCAL.A);\n\
export const imported = () => useFlag(FLAGS.Chat);",
);
let names: Vec<_> = facts
.flag_uses
.iter()
.map(|flag| flag.flag_name.as_str())
.collect();
assert_eq!(names, ["local-a"]);
assert_eq!(facts.flag_uses[0].line, 7);
let reads = &facts.registry_facts.expect("registry facts").reads;
assert_eq!(reads.len(), 1);
assert_eq!(reads[0].flag_use.line, 8);
}
#[test]
fn module_without_registries_has_no_registry_facts() {
let facts = extract_facts("const FLAGS = { A: 1 } as const;\nuseFlag('a');");
assert!(facts.registry_facts.is_none());
assert_eq!(facts.flag_uses.len(), 1);
}
#[test]
fn jsx_logical_and_is_a_guard() {
let source = "const View = () => <div>{useFlag('beta') && <Beta />}</div>;";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert_eq!(guard_text(source, &flags[0]), "useFlag('beta') && <Beta />");
}
#[test]
fn logical_and_without_jsx_is_not_a_guard() {
let flags = extract_from_source("const run = useFlag('beta') && start();");
assert_eq!(flags.len(), 1);
assert!(flags[0].guard_span_start.is_none());
}
#[test]
fn const_binding_takes_the_guard_of_a_jsx_ternary() {
let source = "function View() {\n\
const enabled = useFlag('beta');\n\
return <div>{enabled ? <New /> : <Old />}</div>;\n\
}";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].line, 2);
assert_eq!(guard_text(source, &flags[0]), "enabled ? <New /> : <Old />");
}
#[test]
fn const_binding_takes_the_guard_of_a_jsx_logical_and() {
let source = "function View() {\n\
const enabled = await getFeatureValue('beta', false) === true;\n\
return <div>{!enabled && <Old />}</div>;\n\
}";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert_eq!(guard_text(source, &flags[0]), "!enabled && <Old />");
}
#[test]
fn negated_early_return_guards_the_rest_of_the_block() {
let source = "function View() {\n\
const enabled = useFlag('beta');\n\
if (!enabled) return null;\n\
return <New />;\n\
}\n\
after();";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert_eq!(
guard_text(source, &flags[0]),
"if (!enabled) return null;\nreturn <New />;\n}"
);
}
#[test]
fn direct_negated_early_return_guards_the_rest_of_the_block() {
let source = "function run() {\n\
if (!process.env.FEATURE_JOBS) { log(); return; }\n\
jobs();\n\
}";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert!(guard_text(source, &flags[0]).ends_with("jobs();\n}"));
}
#[test]
fn early_return_without_negation_keeps_the_if_span() {
let source = "function run() {\n\
if (process.env.FEATURE_JOBS) return;\n\
jobs();\n\
}";
let flags = extract_from_source(source);
assert_eq!(flags.len(), 1);
assert_eq!(
guard_text(source, &flags[0]),
"if (process.env.FEATURE_JOBS) return;"
);
}
#[test]
fn let_binding_does_not_take_a_guard() {
let flags = extract_from_source("let enabled = useFlag('beta');\nif (enabled) { run(); }");
assert_eq!(flags.len(), 1);
assert!(flags[0].guard_span_start.is_none());
}
#[test]
fn shadowed_binding_does_not_take_a_guard() {
let flags = extract_from_source(
"const enabled = useFlag('beta');\n\
function inner() { const enabled = compute(); if (enabled) { run(); } }",
);
assert_eq!(flags.len(), 1);
assert!(flags[0].guard_span_start.is_none());
}
#[test]
fn binding_from_a_wrapped_call_does_not_take_a_guard() {
let flags =
extract_from_source("const enabled = wrap(useFlag('beta'));\nif (enabled) { run(); }");
assert_eq!(flags.len(), 1);
assert!(flags[0].guard_span_start.is_none());
}
#[test]
fn detects_sdk_call_nested_in_an_if_test() {
let flags = extract_from_source("if (variation('beta', false) === true) { run(); }");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].flag_name, "beta");
assert!(flags[0].guard_span_start.is_some());
}
#[test]
fn config_object_access_is_one_read() {
let flags = extract_with_config_objects("const on = config.features.newCheckout;");
let names: Vec<_> = flags.iter().map(|flag| flag.flag_name.as_str()).collect();
assert_eq!(names, ["features.newCheckout"]);
}
fn only_flag(source: &str) -> FlagUse {
let mut flags = extract_from_source(source);
assert_eq!(flags.len(), 1, "one flag read in {source}");
flags.remove(0)
}
#[test]
fn identical_if_branches_ignore_whitespace_and_comments() {
let flag =
only_flag("if (process.env.FEATURE_X) {\n run(1);\n} else {\n run( 1 ) ; // same\n}");
assert!(flag.facts.identical_branches());
assert!(!flag.facts.empty_branch());
}
#[test]
fn a_one_token_difference_is_not_identical() {
let flag = only_flag("if (process.env.FEATURE_X) { run(1); } else { run(2); }");
assert!(!flag.facts.identical_branches());
}
#[test]
fn identical_ternary_arms_are_identical_branches() {
let flag = only_flag("const v = process.env.FEATURE_X ? pick('a') : pick('a');");
assert!(flag.facts.identical_branches());
}
#[test]
fn a_guard_without_code_in_any_branch_is_an_empty_branch() {
for source in [
"if (process.env.FEATURE_X) {}",
"if (process.env.FEATURE_X) ;",
"if (!process.env.FEATURE_X) {}",
"if (process.env.FEATURE_X) {} else {}",
"if (process.env.FEATURE_X === false) ; else {}",
] {
assert!(only_flag(source).facts.empty_branch(), "{source}");
}
}
#[test]
fn a_guard_with_code_in_one_branch_is_not_an_empty_branch() {
for source in [
"if (process.env.FEATURE_X) { run(); }",
"if (process.env.FEATURE_X) { run(); } else {}",
"if (process.env.FEATURE_X) {} else { run(); }",
"if (!process.env.FEATURE_X) { run(); } else {}",
"if (process.env.FEATURE_X === false) { run(); } else {}",
"if (!process.env.FEATURE_X) return;",
] {
assert!(!only_flag(source).facts.empty_branch(), "{source}");
}
}
#[test]
fn ternary_and_jsx_guards_are_empty_only_when_no_arm_renders() {
for source in [
"const v = useFlag('beta') ? null : undefined;",
"const v = useFlag('beta') ? <></> : null;",
"const v = useFlag('beta') ? void 0 : null;",
"const v = useFlag('beta') ? false : <></>;",
"const v = <div>{useFlag('beta') && <></>}</div>;",
] {
assert!(only_flag(source).facts.empty_branch(), "{source}");
}
for source in [
"const v = useFlag('beta') ? null : <Old />;",
"const v = useFlag('beta') ? undefined : <Old />;",
"const v = useFlag('beta') ? false : <Old />;",
"const v = useFlag('beta') ? <></> : <Old />;",
"const v = !useFlag('beta') ? <Beta /> : null;",
"const v = useFlag('beta') ? <Beta /> : null;",
"const v = useFlag('beta') ? <Beta /> : false;",
"const v = useFlag('beta') ? false : 1;",
"const v = <div>{useFlag('beta') && <Beta />}</div>;",
] {
assert!(!only_flag(source).facts.empty_branch(), "{source}");
}
}
#[test]
fn a_bound_read_takes_the_facts_of_its_guard() {
let flag = only_flag("const on = useFlag('beta');\nif (on) { run(); } else { run(); }");
assert!(flag.facts.identical_branches());
}
#[test]
fn a_read_without_a_guard_has_no_facts() {
let flag = only_flag("track(useFlag('beta'));");
assert_eq!(flag.facts, FlagSiteFacts::default());
}
fn constants(source: &str) -> Vec<FlagConstant> {
extract_facts(source)
.registry_facts
.map(|facts| facts.constants)
.unwrap_or_default()
}
#[test]
fn literal_const_flags_tested_by_a_guard_are_constants() {
let source = "const FEATURE_NEW_UI = true;\n\
if (FEATURE_NEW_UI) { run(); }\n\
export const ENABLE_BETA = false;\n\
export const pick = (): number => (ENABLE_BETA ? 1 : 2);\n\
const FF_MODE = 'on';\n\
if (ready && FF_MODE === 'on') { run(); }\n\
const FEATURE_BANNER = 1;\n\
export const View = () => <div>{FEATURE_BANNER && <Banner />}</div>;\n";
let found = constants(source);
let summary: Vec<(&str, &str, u32, usize)> = found
.iter()
.map(|c| (c.name.as_str(), c.value.as_str(), c.line, c.reads.len()))
.collect();
assert_eq!(
summary,
vec![
("FEATURE_NEW_UI", "true", 1, 1),
("ENABLE_BETA", "false", 3, 1),
("FF_MODE", "'on'", 5, 1),
("FEATURE_BANNER", "1", 7, 1),
]
);
assert_eq!(found[0].reads[0].line, 2);
assert!(
extract_from_source(source).is_empty(),
"constants are not per-site flag reads"
);
}
#[test]
fn let_bindings_calls_shadows_and_plain_names_are_not_constants() {
for source in [
"let FEATURE_LET = true;\nif (FEATURE_LET) { run(); }",
"let FEATURE_R = true;\nFEATURE_R = false;\nif (FEATURE_R) { run(); }",
"const FEATURE_CALL = readFlag();\nif (FEATURE_CALL) { run(); }",
"const FEATURE_S = true;\nfunction f(FEATURE_S: boolean) { if (FEATURE_S) { run(); } }",
"const FEATURE_UNUSED = true;\nlog(FEATURE_UNUSED);",
"const DEBUG = true;\nif (DEBUG) { run(); }",
"const ENABLE_FEATURES = '--enable-features=';\n\
const on = (arg: string) => (arg.startsWith(ENABLE_FEATURES) ? 1 : 0);",
] {
assert!(constants(source).is_empty(), "{source}");
}
}
#[test]
fn a_constant_read_takes_the_facts_of_its_guard() {
let found = constants("const FEATURE_X = true;\nconst v = FEATURE_X ? null : <></>;");
assert!(found[0].reads[0].facts.empty_branch());
}
#[test]
fn a_bound_vercel_flag_call_is_a_definition() {
let source = "import { flag, evaluate } from 'flags/next';\n\
export const showBanner = flag({\n\
key: 'show-banner',\n\
decide: () => Boolean(process.env.FEATURE_BANNER),\n\
});\n\
const value = await evaluate('show-banner');\n";
let facts = extract_facts(source);
let definition = facts
.flag_uses
.iter()
.find(|flag| flag.flag_name == "show-banner" && flag.line == 2)
.expect("definition site");
assert!(definition.facts.definition());
let evaluate = facts
.flag_uses
.iter()
.find(|flag| flag.line == 6)
.expect("evaluate site");
assert!(!evaluate.facts.definition(), "evaluate reads the flag");
let definitions = facts
.registry_facts
.map(|registry| registry.definitions)
.unwrap_or_default();
assert_eq!(
definitions,
vec![FlagDefinition {
binding: "showBanner".to_string(),
line: definition.line,
col: definition.col,
}]
);
}
#[test]
fn an_unbound_vercel_flag_call_is_not_a_definition() {
let facts = extract_facts(
"import { flag } from 'flags/next';\nregister(flag({ key: 'loose', decide: () => false }));",
);
assert!(!facts.flag_uses[0].facts.definition());
assert!(facts.registry_facts.is_none());
}
#[test]
fn builtin_sdk_providers_are_distinct_and_ordered() {
let providers = builtin_sdk_providers();
assert!(!providers.is_empty());
let mut sorted = providers.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
sorted.len(),
providers.len(),
"providers must be distinct: {providers:?}"
);
assert!(
!providers.contains(&""),
"empty provider labels must not leak into the surfaced list"
);
assert_eq!(providers.first(), Some(&"LaunchDarkly"));
assert_eq!(providers.last(), Some(&VERCEL_FLAGS_PROVIDER));
}
#[test]
fn builtin_env_prefixes_match_source_constant() {
let prefixes = builtin_env_prefixes();
assert_eq!(prefixes, BUILTIN_ENV_PREFIXES);
assert!(prefixes.contains(&"FEATURE_"));
assert!(prefixes.contains(&"TOGGLE_"));
}
}