mod admission;
mod annotations;
mod anonymous_stateful;
mod await_purity;
mod coalesce;
mod comparator_contract;
mod compat_deny;
mod contains_domain;
mod conventions_confinement;
mod conversions;
mod determinism;
mod dialect_gate;
mod effects_assertions;
mod external_check;
mod fn_values;
mod harvest;
mod infer;
mod manifest;
mod map_keys;
mod markup_check;
mod modules;
mod native_admission;
mod native_choice_dead_end;
mod no_world_reads;
mod option_conditions;
mod option_rules;
mod protocols;
mod range_refinement;
mod ref_projection;
mod resolve;
mod signature;
mod strict;
mod structs;
mod temp_dominance;
mod type_resolution;
mod ufcs;
mod validate;
use std::collections::BTreeMap;
use std::sync::Arc;
pub use admission::validate_admission;
pub use annotations::{
check as check_annotations, mismatches as annotation_mismatches, resolve as resolve_annotation,
};
pub use anonymous_stateful::check as check_anonymous_stateful;
pub use await_purity::{
check as await_purity_diagnostics, condition_callees as await_condition_callees, hir_has_await,
};
pub use brink_ir::FileId;
pub use brink_ir::ResolutionMap;
pub use brink_project_config::ProjectConfig;
pub use coalesce::{
CoalesceChain, CoalesceShape, CoalesceStep, CoalesceTable, project_has_coalesce,
to_lir_lookup as coalesce_lir_lookup,
};
pub use comparator_contract::{
check as comparator_contract_diagnostics, comparator_callees, hir_has_comparator_site,
};
pub use conventions_confinement::{
conventions_confinement_diagnostics, conventions_module_diagnostics,
conventions_pointer_unresolvable_diagnostics, conventions_unconfigured_diagnostics,
is_path_shaped_conventions_pointer,
};
pub use dialect_gate::Dialect;
pub use effects_assertions::{
assertion_defs as effects_assertion_defs, check as effects_assertion_diagnostics,
effect_atom_name,
};
pub use external_check::{
ExternalCheckSeverity, InferredType, ResolvedParam, ResolvedType,
SemanticTypeDiagnosticSeverity, SymbolMeta, ValueMeta,
};
pub use harvest::{
CueHarvest, HarvestIndex, HarvestNames, HarvestSite, SpanHarvest, SpanNames, harvest,
};
pub use infer::{
BodyTypes, CallGraph, CoalesceError, Def, DirectCallArgMismatch, EffectAtoms, EffectRow,
FieldAssignMismatch, FnRow, InferenceResult, InferredSig, LambdaAnnotationMismatch,
LambdaEscapeSlot, SccGraph, Ty, TypedAssignMismatch, UfcsCallArgs, ValueCallFact,
ValueCallKind, assignable, call_edges, coalesce, collect_external_sigs, def_body,
def_effect_atoms, effects_project, erase_fn_rows, infer_project, inferable_defs,
inferable_defs_from_index, ref_assignable, referenced_globals, scc_graph, solve_scc,
solve_scc_effects, unify, unify_all,
};
pub use manifest::{ModuleMap, ResolvedModule};
pub use native_admission::validate_native_accept_list;
pub use native_choice_dead_end::check as check_native_choice_dead_end;
pub use no_world_reads::check as no_world_reads_diagnostics;
pub use protocols::{
Protocol, ProtocolImplDecl, check_protocol_impls, check_reserved_names,
is_reserved_protocol_name, iterate_element_ty, iterate_val_ty,
};
pub use resolve::ImportScope;
pub use signature::{Sig, local_signature, signature};
pub use strict::{
LintLevel, LintPolicy, TypePolicy, effective_severity, native_strict_only_error,
resolve_type_policy,
};
pub use structs::{ShapeInfo, ShapeTable, declared_shapes};
pub use ufcs::{
NodeKey, SideTable, UfcsArgMismatch, UfcsTable, UfcsVerdict, project_has_ufcs_call,
resolve as resolve_ufcs_calls, to_lir_lookup as ufcs_lir_lookup,
};
#[doc(hidden)]
pub mod test_support {
pub use crate::resolve::{is_builtin_function, is_t1b_stdlib_name};
}
use brink_format::DefinitionId;
use brink_ir::{
Diagnostic, DiagnosticCode, DocBlock, HirFile, HostManifest, ManifestExternal, SemanticTypeDef,
SymbolIndex, SymbolKind, SymbolManifest,
};
use brink_project_config::ConfigWarning;
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AnalysisOptions {
pub host_manifest: Option<HostManifest>,
pub external_check: ExternalCheckSeverity,
pub semantic_type_check: SemanticTypeDiagnosticSeverity,
pub dialect: Dialect,
pub types: Option<TypePolicy>,
pub lints: LintPolicy,
pub emit_debug_info: bool,
pub conventions: Option<String>,
}
impl AnalysisOptions {
#[must_use]
pub fn type_policy(&self) -> TypePolicy {
resolve_type_policy(self.dialect, self.types)
}
pub fn apply_project_config(
&mut self,
config: &ProjectConfig,
dialect_overridden: bool,
types_overridden: bool,
) -> Vec<ConfigWarning> {
if !dialect_overridden && let Some(dialect) = config.dialect {
self.dialect = dialect;
}
if !types_overridden && let Some(types) = config.types {
self.types = Some(types);
}
let mut warnings = Vec::new();
if let Some(pointer) = config.conventions.as_deref() {
if is_path_shaped_conventions_pointer(pointer) {
self.conventions = Some(pointer.to_owned());
} else {
match validate_conventions_preset(pointer, BUILTIN_CONVENTION_PRESETS) {
Ok(()) => {
self.conventions = Some(pointer.to_owned());
if !INJECTABLE_CONVENTION_PRESETS.contains(&pointer) {
warnings.push(ConfigWarning(format!(
"[project] conventions = \"{pointer}\" names a recognized \
built-in preset, not injectable yet — no conventions \
applied (#2080/#1840)"
)));
}
}
Err(warning) => warnings.push(warning),
}
}
}
let mut overrides = BTreeMap::new();
for (code, level) in &config.lints {
match validate_lint_code(code) {
Ok(()) => {
overrides.insert(code.clone(), *level);
}
Err(warning) => warnings.push(warning),
}
}
for code in config.fix.keys() {
if let Err(warning) = validate_fix_code(code) {
warnings.push(warning);
}
}
self.lints.overrides = overrides;
self.lints.deny_warnings = config.deny_warnings.unwrap_or(false);
warnings
}
pub fn apply_lint_overrides(
&mut self,
overrides: &BTreeMap<String, LintLevel>,
deny_warnings: Option<bool>,
) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
for (code, level) in overrides {
match validate_lint_code(code) {
Ok(()) => {
self.lints.overrides.insert(code.clone(), *level);
}
Err(warning) => warnings.push(warning),
}
}
if let Some(deny_warnings) = deny_warnings {
self.lints.deny_warnings = deny_warnings;
}
warnings
}
}
fn validate_lint_code(code: &str) -> Result<(), ConfigWarning> {
match DiagnosticCode::from_str_code(code) {
Some(parsed) if parsed.is_overridable() => Ok(()),
Some(_) => Err(ConfigWarning(format!(
"[lints] `{code}` is not overridable (its default severity is `Error`); ignored"
))),
None => Err(ConfigWarning(format!(
"[lints] `{code}` is not a recognized diagnostic code; ignored"
))),
}
}
fn validate_fix_code(code: &str) -> Result<(), ConfigWarning> {
match DiagnosticCode::from_str_code(code) {
Some(_) => Ok(()),
None => Err(ConfigWarning(format!(
"[fix] `{code}` is not a recognized diagnostic code; ignored"
))),
}
}
const BUILTIN_CONVENTION_PRESETS: &[&str] = &["screenplay"];
const INJECTABLE_CONVENTION_PRESETS: &[&str] = &[];
fn validate_conventions_preset(pointer: &str, presets: &[&str]) -> Result<(), ConfigWarning> {
if presets.contains(&pointer) {
return Ok(());
}
if presets.is_empty() {
Err(ConfigWarning(format!(
"[project] conventions = \"{pointer}\" names a built-in preset, but no built-in \
preset has shipped yet (#1720); use a project-relative path to a `.brink` \
conventions module instead"
)))
} else {
Err(ConfigWarning(format!(
"[project] conventions = \"{pointer}\" is not a recognized built-in preset name and \
is not a project-relative path to a `.brink` conventions module (no `/`, `\\`, or \
`.brink` extension); ignored"
)))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnalysisResult {
pub index: Arc<SymbolIndex>,
pub resolutions: ResolutionMap,
pub diagnostics: Vec<Diagnostic>,
pub symbol_meta: BTreeMap<DefinitionId, SymbolMeta>,
}
#[must_use]
pub fn symbol_index(files: &[(FileId, &SymbolManifest)]) -> (Arc<SymbolIndex>, Vec<Diagnostic>) {
let (index, diagnostics) = manifest::merge_manifests(files);
(Arc::new(index), diagnostics)
}
#[must_use]
pub fn symbol_index_with_modules(
files: &[(FileId, &SymbolManifest)],
modules: &ModuleMap,
dialect: Dialect,
is_native: bool,
) -> (Arc<SymbolIndex>, Vec<Diagnostic>) {
let (index, diagnostics) =
manifest::merge_manifests_with_modules(files, modules, dialect, is_native);
(Arc::new(index), diagnostics)
}
#[must_use]
pub fn resolve(
file: FileId,
manifest: &SymbolManifest,
index: &SymbolIndex,
scope: &ImportScope,
) -> (Arc<ResolutionMap>, Vec<Diagnostic>) {
let (map, diagnostics) = resolve::resolve_file(index, scope, file, manifest);
(Arc::new(map), diagnostics)
}
pub fn analyze(files: &[(FileId, &HirFile, &SymbolManifest)]) -> AnalysisResult {
analyze_with_options(files, &AnalysisOptions::default())
}
pub fn analyze_with_options(
files: &[(FileId, &HirFile, &SymbolManifest)],
opts: &AnalysisOptions,
) -> AnalysisResult {
let modules = ModuleMap::new();
let manifest_inputs: Vec<(FileId, &SymbolManifest)> = files
.iter()
.map(|&(id, _hir, manifest)| (id, manifest))
.collect();
let (index, mut diagnostics) =
symbol_index_with_modules(&manifest_inputs, &modules, opts.dialect, false);
let mut resolutions = ResolutionMap::new();
let mut scopes: BTreeMap<FileId, ImportScope> = BTreeMap::new();
for &(file_id, hir, manifest) in files {
let scope = ImportScope::new(hir.module.as_ref().map(|m| m.name.clone()), &hir.imports);
let (file_map, file_diags) = resolve(file_id, manifest, &index, &scope);
resolutions.extend(Arc::unwrap_or_clone(file_map));
diagnostics.extend(file_diags);
scopes.insert(file_id, scope);
}
let hir_files: Vec<(FileId, &HirFile)> = files.iter().map(|&(id, hir, _)| (id, hir)).collect();
diagnostics.extend(conventions_confinement_diagnostics(
&hir_files,
&modules,
opts.conventions.as_deref(),
));
finish_analysis(
files,
index,
resolutions,
diagnostics,
opts,
false,
None,
&scopes,
)
}
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "each parameter is an independently-necessary per-file input (issue #2272 added \
`scope`, the file's own declared-module ImportScope) — bundling them would just \
move the count into a struct with no consumer of its own"
)]
pub fn per_file_diagnostics(
file: FileId,
hir: &HirFile,
file_resolutions: &ResolutionMap,
index: &SymbolIndex,
dialect: Dialect,
is_native: bool,
host_manifest: Option<&HostManifest>,
scope: &ImportScope,
) -> Vec<Diagnostic> {
let files = [(file, hir)];
let mut out = validate::validate(&files);
if !is_native {
out.extend(dialect_gate::check(&files, file_resolutions, dialect));
}
out.extend(option_rules::check(&files, file_resolutions));
out.extend(temp_dominance::check(file, hir, is_native));
out.extend(compat_deny::knot_temp_from_stitch::check(
file, hir, is_native,
));
if dialect == Dialect::Brink {
out.extend(annotations::check(file, hir, index, host_manifest, scope));
out.extend(fn_values::check(&files, file_resolutions, index));
out.extend(ref_projection::check(&files, file_resolutions, index));
out.extend(protocols::check_reserved_names(&files));
}
if dialect == Dialect::Brink || is_native {
out.extend(structs::check_duplicates(&files));
out.extend(annotations::check_reserved_type_names(&files));
out.extend(map_keys::check(&files));
out.extend(map_keys::check_duplicate_keys(&files));
}
if is_native {
out.extend(fn_values::check_native_bare_refs(
&files,
file_resolutions,
index,
));
}
out.extend(markup_check::check(&[(file, hir)], host_manifest));
out
}
#[must_use]
pub fn project_inline_docs(
files: &[(FileId, &SymbolManifest)],
) -> BTreeMap<(SymbolKind, String), DocBlock> {
collect_inline_docs(files)
}
#[must_use]
pub fn external_meta_diagnostics(
index: &SymbolIndex,
inline_docs: &BTreeMap<(SymbolKind, String), DocBlock>,
opts: &AnalysisOptions,
) -> (BTreeMap<DefinitionId, SymbolMeta>, Vec<Diagnostic>) {
let (types, registered) = manifest_maps(opts.host_manifest.as_ref());
let has_manifest = opts.host_manifest.is_some();
let check_unknown_types =
has_manifest || opts.semantic_type_check == SemanticTypeDiagnosticSeverity::Error;
let (mut symbol_meta, mut diagnostics) = external_check::analyze_externals(
index,
inline_docs,
&types,
®istered,
opts.external_check,
check_unknown_types,
);
let (callable_meta, callable_diags) = external_check::enrich_callables(
index,
inline_docs,
&types,
opts.external_check,
check_unknown_types,
);
diagnostics.extend(callable_diags);
symbol_meta.extend(callable_meta);
(symbol_meta, diagnostics)
}
#[must_use]
pub fn call_site_metas(
index: &SymbolIndex,
metas: &BTreeMap<DefinitionId, SymbolMeta>,
) -> BTreeMap<String, SymbolMeta> {
metas
.iter()
.filter_map(|(id, meta)| {
index.symbols.get(id).and_then(|s| {
(s.kind == SymbolKind::External).then(|| (s.name.clone(), meta.clone()))
})
})
.collect()
}
#[must_use]
pub fn file_value_meta(
file: FileId,
hir: &HirFile,
index: &SymbolIndex,
inline_docs: &BTreeMap<(SymbolKind, String), DocBlock>,
) -> BTreeMap<DefinitionId, SymbolMeta> {
external_check::infer_value_meta(&[(file, hir)], index, inline_docs)
}
#[must_use]
pub fn file_call_site_diagnostics(
file: FileId,
hir: &HirFile,
metas: &BTreeMap<String, SymbolMeta>,
) -> Vec<Diagnostic> {
let name_to_meta: BTreeMap<&str, &SymbolMeta> = metas
.iter()
.map(|(name, meta)| (name.as_str(), meta))
.collect();
external_check::check_call_sites(&[(file, hir)], &name_to_meta)
}
#[must_use]
pub fn module_diagnostics(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
) -> Vec<Diagnostic> {
modules::check(files, index, resolutions)
}
#[must_use]
pub fn strict_diagnostics(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
opts: &AnalysisOptions,
is_native: bool,
strict_inference: Option<&InferenceResult>,
inline_docs: &BTreeMap<(SymbolKind, String), DocBlock>,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
if opts.type_policy() == TypePolicy::Strict {
let config_err = if is_native {
None
} else {
strict::config_error(opts.dialect, files.first().map(|&(f, _)| f))
};
if let Some(diag) = config_err {
diagnostics.push(diag);
} else {
let owned_inference;
let inference = if let Some(inf) = strict_inference {
inf
} else {
owned_inference = infer::infer_project(
files,
index,
resolutions,
opts.host_manifest.as_ref(),
inline_docs,
);
&owned_inference
};
diagnostics.extend(strict::check(
files,
index,
inference,
resolutions,
opts.host_manifest.as_ref(),
));
let external_sigs =
infer::collect_external_sigs(index, opts.host_manifest.as_ref(), inline_docs);
diagnostics.extend(strict::check_external_escapes(index, &external_sigs));
}
}
diagnostics
}
#[must_use]
pub fn whole_project_diagnostics(
files: &[(FileId, &HirFile, &SymbolManifest)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
opts: &AnalysisOptions,
is_native: bool,
strict_inference: Option<&InferenceResult>,
) -> (Vec<Diagnostic>, BTreeMap<DefinitionId, SymbolMeta>) {
let manifest_inputs: Vec<(FileId, &SymbolManifest)> = files
.iter()
.map(|&(id, _hir, manifest)| (id, manifest))
.collect();
let hir_inputs: Vec<(FileId, &HirFile)> = files.iter().map(|&(id, hir, _)| (id, hir)).collect();
let inline_docs = collect_inline_docs(&manifest_inputs);
let mut diagnostics = module_diagnostics(&hir_inputs, index, resolutions);
diagnostics.extend(strict_diagnostics(
&hir_inputs,
index,
resolutions,
opts,
is_native,
strict_inference,
&inline_docs,
));
let (mut symbol_meta, ext_diags) = external_meta_diagnostics(index, &inline_docs, opts);
diagnostics.extend(ext_diags);
let cs_metas = call_site_metas(index, &symbol_meta);
for &(file_id, hir, _) in files {
symbol_meta.extend(file_value_meta(file_id, hir, index, &inline_docs));
}
if opts.external_check != ExternalCheckSeverity::Off {
for &(file_id, hir, _) in files {
diagnostics.extend(file_call_site_diagnostics(file_id, hir, &cs_metas));
}
}
let needs_effects = hir_inputs.iter().any(|&(_, hir)| {
hir_has_effects_assertion(hir)
|| await_purity::hir_has_await(hir)
|| comparator_contract::hir_has_comparator_site(hir)
});
if opts.dialect == Dialect::Brink && needs_effects {
let rows =
infer::effects_project(&hir_inputs, index, resolutions, opts.host_manifest.as_ref());
for &(file_id, hir) in &hir_inputs {
let scope = ImportScope::new(hir.module.as_ref().map(|m| m.name.clone()), &hir.imports);
diagnostics.extend(effects_assertions::check(
file_id, hir, index, &scope, &rows,
));
diagnostics.extend(await_purity::check(file_id, hir, index, resolutions, &rows));
diagnostics.extend(comparator_contract::check(
file_id,
hir,
index,
resolutions,
&rows,
));
}
}
for &(file_id, hir, _) in files {
diagnostics.extend(no_world_reads::check(
file_id,
hir,
&hir_inputs,
index,
resolutions,
&symbol_meta,
));
}
if hir_inputs
.iter()
.any(|&(_, hir)| ufcs::project_has_ufcs_call(hir))
{
let owned_inference;
let inference = if let Some(inf) = strict_inference {
inf
} else {
owned_inference = infer::infer_project(
&hir_inputs,
index,
resolutions,
opts.host_manifest.as_ref(),
&inline_docs,
);
&owned_inference
};
let (_table, ufcs_diags) = ufcs::resolve(&hir_inputs, index, resolutions, inference);
diagnostics.extend(ufcs_diags);
}
(diagnostics, symbol_meta)
}
#[must_use]
pub fn ufcs_resolution(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
inference: &InferenceResult,
) -> (UfcsTable, Vec<Diagnostic>) {
ufcs::resolve(files, index, resolutions, inference)
}
#[must_use]
pub fn coalesce_types(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
inference: &InferenceResult,
resolutions: &ResolutionMap,
) -> (CoalesceTable, Vec<Diagnostic>) {
coalesce::resolve(files, index, inference, resolutions)
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzerTablesOwned {
pub ufcs: brink_ir::lir::UfcsLookup,
pub coalesce: brink_ir::lir::CoalesceLookup,
}
impl AnalyzerTablesOwned {
#[must_use]
pub fn as_tables(&self) -> brink_ir::lir::AnalyzerTables<'_> {
brink_ir::lir::AnalyzerTables {
ufcs: &self.ufcs,
coalesce: &self.coalesce,
}
}
}
#[must_use]
pub fn assemble_analyzer_tables(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
host_manifest: Option<&HostManifest>,
inline_docs: &BTreeMap<(SymbolKind, String), DocBlock>,
) -> AnalyzerTablesOwned {
let needs_ufcs = files
.iter()
.any(|&(_, hir)| ufcs::project_has_ufcs_call(hir));
let needs_coalesce = files
.iter()
.any(|&(_, hir)| coalesce::project_has_coalesce(hir));
let inference = if needs_ufcs || needs_coalesce {
Some(infer::infer_project(
files,
index,
resolutions,
host_manifest,
inline_docs,
))
} else {
None
};
let ufcs = match (&inference, needs_ufcs) {
(Some(inference), true) => {
let (table, _ufcs_diagnostics) = ufcs_resolution(files, index, resolutions, inference);
ufcs_lir_lookup(&table)
}
_ => brink_ir::lir::UfcsLookup::new(),
};
let coalesce = match (&inference, needs_coalesce) {
(Some(inference), true) => {
let (table, _e066_diagnostics) = coalesce_types(files, index, inference, resolutions);
coalesce_lir_lookup(&table)
}
_ => brink_ir::lir::CoalesceLookup::new(),
};
AnalyzerTablesOwned { ufcs, coalesce }
}
fn hir_has_effects_assertion(hir: &HirFile) -> bool {
hir.knots.iter().any(|k| {
k.effects_assertion.is_some() || k.stitches.iter().any(|s| s.effects_assertion.is_some())
})
}
#[expect(
clippy::too_many_arguments,
reason = "each parameter is an independently-necessary layer-2 input this function \
assembles into the final AnalysisResult (issue #2272 added `scopes`, mirroring \
`per_file_diagnostics`'s own new `scope` parameter) — bundling them would just \
move the count into a struct with no consumer of its own"
)]
pub fn finish_analysis(
files: &[(FileId, &HirFile, &SymbolManifest)],
index: Arc<SymbolIndex>,
resolutions: ResolutionMap,
mut diagnostics: Vec<Diagnostic>,
opts: &AnalysisOptions,
is_native: bool,
strict_inference: Option<&infer::InferenceResult>,
scopes: &BTreeMap<FileId, ImportScope>,
) -> AnalysisResult {
let default_scope = ImportScope::default();
for &(file_id, hir, _manifest) in files {
let file_resolutions: ResolutionMap = resolutions
.iter()
.filter(|r| r.file == file_id)
.cloned()
.collect();
let scope = scopes.get(&file_id).unwrap_or(&default_scope);
diagnostics.extend(per_file_diagnostics(
file_id,
hir,
&file_resolutions,
&index,
opts.dialect,
is_native,
opts.host_manifest.as_ref(),
scope,
));
if is_native {
diagnostics.extend(native_strict_only_error(file_id, opts.types));
}
}
let (whole_diagnostics, symbol_meta) = whole_project_diagnostics(
files,
&index,
&resolutions,
opts,
is_native,
strict_inference,
);
diagnostics.extend(whole_diagnostics);
AnalysisResult {
index,
resolutions,
diagnostics,
symbol_meta,
}
}
fn collect_inline_docs(
files: &[(FileId, &SymbolManifest)],
) -> BTreeMap<(SymbolKind, String), DocBlock> {
let mut out = BTreeMap::new();
for &(_id, manifest) in files {
for (key, doc) in &manifest.docs {
out.insert(key.clone(), doc.clone());
}
}
out
}
fn manifest_maps(
manifest: Option<&HostManifest>,
) -> (
BTreeMap<String, SemanticTypeDef>,
BTreeMap<String, &ManifestExternal>,
) {
let mut types = BTreeMap::new();
let mut registered = BTreeMap::new();
if let Some(manifest) = manifest {
for ty in &manifest.types {
types.insert(ty.name.clone(), ty.clone());
}
for ext in &manifest.externals {
registered.insert(ext.name.clone(), ext);
}
}
(types, registered)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use brink_ir::{BaseType, HostManifest, SemanticTypeDef};
use super::{
AnalysisOptions, Dialect, FileId, ImportScope, LintLevel, LintPolicy, ModuleMap,
ProjectConfig, SemanticTypeDiagnosticSeverity, TypePolicy, analyze, analyze_with_options,
per_file_diagnostics, resolve, symbol_index, validate_conventions_preset,
};
fn analyze_composed(
files: &[(FileId, &super::HirFile, &super::SymbolManifest)],
modules: &ModuleMap,
opts: &AnalysisOptions,
is_native: bool,
) -> super::AnalysisResult {
let manifest_inputs: Vec<_> = files.iter().map(|&(id, _hir, m)| (id, m)).collect();
let (index, mut diagnostics) =
super::symbol_index_with_modules(&manifest_inputs, modules, opts.dialect, is_native);
let mut resolutions = brink_ir::ResolutionMap::new();
let mut scopes = std::collections::BTreeMap::new();
for &(file_id, hir, manifest) in files {
let declared_module = match modules.get(&file_id) {
Some(resolved) => resolved.declared.then(|| resolved.name.clone()),
None => hir.module.as_ref().map(|m| m.name.clone()),
};
let scope = ImportScope::new(declared_module, &hir.imports);
let (file_map, file_diags) = resolve(file_id, manifest, &index, &scope);
resolutions.extend(std::sync::Arc::unwrap_or_clone(file_map));
diagnostics.extend(file_diags);
scopes.insert(file_id, scope);
}
super::finish_analysis(
files,
index,
resolutions,
diagnostics,
opts,
is_native,
None,
&scopes,
)
}
const SRC: &str = "\
/// @param who {actor_id}
EXTERNAL add_state(who)
";
fn lower(src: &str) -> (brink_ir::hir::HirFile, brink_ir::SymbolManifest) {
let parsed = brink_syntax::parse(src);
let tree = parsed.tree();
let (hir, manifest, diags) = brink_ir::hir::lower(FileId(0), &tree);
assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
(hir, manifest)
}
#[test]
fn host_semantic_type_compiles_host_free_with_no_manifest() {
let (hir, manifest) = lower(SRC);
let result = analyze(&[(FileId(0), &hir, &manifest)]);
assert!(
result.diagnostics.is_empty(),
"host-free compile must not error on unknown semantic types: {:?}",
result.diagnostics
);
}
#[test]
fn host_semantic_type_still_checked_once_manifest_registered() {
let (hir, manifest) = lower(SRC);
let host_manifest = HostManifest {
markup: Vec::new(),
externals: Vec::new(),
types: vec![SemanticTypeDef {
name: "actor_id".to_string(),
base: BaseType::String,
constraint: None,
values: None,
widget: None,
}],
};
let opts = AnalysisOptions {
host_manifest: Some(host_manifest),
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result.diagnostics.is_empty(),
"known semantic type resolves cleanly: {:?}",
result.diagnostics
);
}
#[test]
fn genuinely_unknown_type_still_errors_when_manifest_registered() {
let (hir, manifest) = lower(SRC);
let opts = AnalysisOptions {
host_manifest: Some(HostManifest::default()),
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert_eq!(
result
.diagnostics
.iter()
.filter(|d| d.code == brink_ir::DiagnosticCode::E040)
.count(),
1,
"manifest registered but type unknown: E040 still fires: {:?}",
result.diagnostics
);
}
#[test]
fn semantic_type_check_default_is_tolerant() {
let (hir, manifest) = lower(SRC);
let opts = AnalysisOptions {
semantic_type_check: SemanticTypeDiagnosticSeverity::Tolerant,
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result.diagnostics.is_empty(),
"Tolerant (default) with no manifest: no E040: {:?}",
result.diagnostics
);
}
#[test]
fn semantic_type_check_error_diagnoses_with_no_manifest() {
let (hir, manifest) = lower(SRC);
let opts = AnalysisOptions {
semantic_type_check: SemanticTypeDiagnosticSeverity::Error,
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert_eq!(
result
.diagnostics
.iter()
.filter(|d| d.code == brink_ir::DiagnosticCode::E040)
.count(),
1,
"Error with no manifest: E040 still fires: {:?}",
result.diagnostics
);
}
#[test]
fn semantic_type_check_error_with_known_type_in_manifest_is_clean() {
let (hir, manifest) = lower(SRC);
let host_manifest = HostManifest {
markup: Vec::new(),
externals: Vec::new(),
types: vec![SemanticTypeDef {
name: "actor_id".to_string(),
base: BaseType::String,
constraint: None,
values: None,
widget: None,
}],
};
let opts = AnalysisOptions {
host_manifest: Some(host_manifest),
semantic_type_check: SemanticTypeDiagnosticSeverity::Error,
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result.diagnostics.is_empty(),
"known type resolves cleanly regardless of severity: {:?}",
result.diagnostics
);
}
fn lower_one(src: &str) -> (brink_ir::hir::HirFile, brink_ir::SymbolManifest) {
let parsed = brink_syntax::parse(src);
let (hir, manifest, diags) = brink_ir::hir::lower(FileId(0), &parsed.tree());
assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
(hir, manifest)
}
#[test]
fn types_default_is_dialect_keyed() {
let src = "=== noop(x) ===\nHello.\n-> DONE\n";
let (hir, manifest) = lower_one(src);
let opts = AnalysisOptions {
dialect: Dialect::StrictInk,
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result.diagnostics.is_empty(),
"strict-ink (default types = gradual) must stay silent: {:?}",
result.diagnostics
);
let opts = AnalysisOptions {
dialect: Dialect::Brink,
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E065),
"brink (default types = strict) must flag the Unknown escape: {:?}",
result.diagnostics
);
let opts = AnalysisOptions {
dialect: Dialect::Brink,
types: Some(TypePolicy::Gradual),
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result.diagnostics.is_empty(),
"brink + explicit gradual opt-out must stay silent: {:?}",
result.diagnostics
);
}
#[test]
fn strict_with_strict_ink_dialect_is_a_config_error_and_nothing_else_runs() {
let src = "=== noop(x) ===\nHello.\n-> DONE\n";
let (hir, manifest) = lower_one(src);
let opts = AnalysisOptions {
dialect: Dialect::StrictInk,
types: Some(TypePolicy::Strict),
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
let strict_diags: Vec<_> = result
.diagnostics
.iter()
.filter(|d| {
matches!(
d.code,
brink_ir::DiagnosticCode::E064
| brink_ir::DiagnosticCode::E065
| brink_ir::DiagnosticCode::E066
)
})
.collect();
assert_eq!(
strict_diags.len(),
1,
"exactly the one config error, nothing else: {:?}",
result.diagnostics
);
assert_eq!(strict_diags[0].code, brink_ir::DiagnosticCode::E064);
}
#[test]
fn strict_with_brink_dialect_surfaces_unknown_escape_as_a_compile_error() {
let src = "=== noop(x) ===\nHello.\n-> DONE\n";
let (hir, manifest) = lower_one(src);
let opts = AnalysisOptions {
dialect: Dialect::Brink,
types: Some(TypePolicy::Strict),
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E065),
"{:?}",
result.diagnostics
);
assert_eq!(
result
.diagnostics
.iter()
.find(|d| d.code == brink_ir::DiagnosticCode::E065)
.expect("checked above")
.code
.severity(),
brink_ir::Severity::Error,
"Unknown-escape is a compile error under strict, not a warning"
);
}
#[test]
fn strict_clean_project_compiles_with_no_diagnostics() {
let src =
"=== function heal(hp: int): int ===\n~ temp bonus: int = 5\n~ return hp + bonus\n";
let (hir, manifest) = lower_one(src);
let opts = AnalysisOptions {
dialect: Dialect::Brink,
types: Some(TypePolicy::Strict),
..AnalysisOptions::default()
};
let result = analyze_with_options(&[(FileId(0), &hir, &manifest)], &opts);
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
}
#[test]
fn per_file_diagnostics_is_native_true_skips_the_dialect_gate() {
let (hir, manifest) = lower_one("~ x = a[0]\n");
let (index, _diags) = symbol_index(&[(FileId(0), &manifest)]);
let (resolutions, _diags) = resolve(FileId(0), &manifest, &index, &ImportScope::default());
let diags = per_file_diagnostics(
FileId(0),
&hir,
&resolutions,
&index,
Dialect::StrictInk,
true,
None,
&ImportScope::default(),
);
assert!(
!diags
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E051),
"native must never see the ink-only dialect gate: {diags:?}"
);
}
#[test]
fn per_file_diagnostics_is_native_false_unaffected_still_flags_extension_syntax() {
let (hir, manifest) = lower_one("~ x = a[0]\n");
let (index, _diags) = symbol_index(&[(FileId(0), &manifest)]);
let (resolutions, _diags) = resolve(FileId(0), &manifest, &index, &ImportScope::default());
let diags = per_file_diagnostics(
FileId(0),
&hir,
&resolutions,
&index,
Dialect::StrictInk,
false,
None,
&ImportScope::default(),
);
assert!(
diags
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E051),
"ink must still see the dialect gate: {diags:?}"
);
}
#[test]
fn composed_is_native_true_skips_the_dialect_gate() {
let (hir, manifest) = lower_one("~ x = a[0]\n");
let result = analyze_composed(
&[(FileId(0), &hir, &manifest)],
&ModuleMap::new(),
&AnalysisOptions::default(),
true,
);
assert!(
!result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E051),
"native must never see the ink-only dialect gate: {:?}",
result.diagnostics
);
}
#[test]
fn composed_is_native_false_unaffected_still_flags_extension_syntax() {
let (hir, manifest) = lower_one("~ x = a[0]\n");
let result = analyze_composed(
&[(FileId(0), &hir, &manifest)],
&ModuleMap::new(),
&AnalysisOptions::default(),
false,
);
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E051),
"ink must still see the dialect gate: {:?}",
result.diagnostics
);
}
#[test]
fn composed_is_native_true_skips_the_ink_only_config_error() {
let (hir, manifest) = lower_one("=== start ===\nHello.\n-> DONE\n");
let opts = AnalysisOptions {
types: Some(TypePolicy::Strict),
..AnalysisOptions::default()
};
let result = analyze_composed(
&[(FileId(0), &hir, &manifest)],
&ModuleMap::new(),
&opts,
true,
);
assert!(
!result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E064),
"native has no dialect to be wrong about: {:?}",
result.diagnostics
);
}
#[test]
fn composed_is_native_false_unaffected_still_fires_config_error() {
let (hir, manifest) = lower_one("=== start ===\nHello.\n-> DONE\n");
let opts = AnalysisOptions {
types: Some(TypePolicy::Strict),
..AnalysisOptions::default()
};
let result = analyze_composed(
&[(FileId(0), &hir, &manifest)],
&ModuleMap::new(),
&opts,
false,
);
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E064),
"ink must still get the config error: {:?}",
result.diagnostics
);
}
#[test]
fn composed_is_native_true_reports_the_native_strict_only_error() {
let (hir, manifest) = lower_one("=== start ===\nHello.\n-> DONE\n");
let opts = AnalysisOptions {
types: Some(TypePolicy::Gradual),
..AnalysisOptions::default()
};
let result = analyze_composed(
&[(FileId(0), &hir, &manifest)],
&ModuleMap::new(),
&opts,
true,
);
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E137),
"explicit `types = gradual` is a native config error: {:?}",
result.diagnostics
);
}
#[test]
fn composed_is_native_false_never_reports_the_native_strict_only_error() {
let (hir, manifest) = lower_one("=== start ===\nHello.\n-> DONE\n");
let opts = AnalysisOptions {
types: Some(TypePolicy::Gradual),
..AnalysisOptions::default()
};
let result = analyze_composed(
&[(FileId(0), &hir, &manifest)],
&ModuleMap::new(),
&opts,
false,
);
assert!(
!result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E137),
"`E137` is native-only: {:?}",
result.diagnostics
);
}
#[test]
fn analyze_with_options_stays_the_ink_arm() {
let (hir, manifest) = lower_one("~ x = a[0]\n");
let result =
analyze_with_options(&[(FileId(0), &hir, &manifest)], &AnalysisOptions::default());
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == brink_ir::DiagnosticCode::E051),
"{:?}",
result.diagnostics
);
}
#[test]
fn apply_sets_unset_fields_from_config() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig {
dialect: Some(Dialect::Brink),
types: Some(TypePolicy::Strict),
..ProjectConfig::default()
};
options.apply_project_config(&config, false, false);
assert_eq!(options.dialect, Dialect::Brink);
assert_eq!(options.types, Some(TypePolicy::Strict));
}
#[test]
fn apply_leaves_overridden_fields_alone() {
let mut options = AnalysisOptions {
dialect: Dialect::StrictInk,
types: Some(TypePolicy::Gradual),
..AnalysisOptions::default()
};
let config = ProjectConfig {
dialect: Some(Dialect::Brink),
types: Some(TypePolicy::Strict),
..ProjectConfig::default()
};
options.apply_project_config(&config, true, true);
assert_eq!(options.dialect, Dialect::StrictInk);
assert_eq!(options.types, Some(TypePolicy::Gradual));
}
#[test]
fn apply_mixed_override_only_touches_non_overridden_field() {
let mut options = AnalysisOptions {
dialect: Dialect::StrictInk,
types: Some(TypePolicy::Gradual),
..AnalysisOptions::default()
};
let config = ProjectConfig {
dialect: Some(Dialect::Brink),
types: Some(TypePolicy::Strict),
..ProjectConfig::default()
};
options.apply_project_config(&config, true, false);
assert_eq!(options.dialect, Dialect::StrictInk);
assert_eq!(options.types, Some(TypePolicy::Strict));
}
#[test]
fn apply_with_no_config_values_leaves_options_untouched() {
let mut options = AnalysisOptions {
dialect: Dialect::Brink,
types: Some(TypePolicy::Strict),
..AnalysisOptions::default()
};
options.apply_project_config(&ProjectConfig::default(), false, false);
assert_eq!(options.dialect, Dialect::Brink);
assert_eq!(options.types, Some(TypePolicy::Strict));
}
#[test]
fn apply_project_config_applies_lint_overrides() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config.lints.insert("E014".to_owned(), LintLevel::Deny);
config.lints.insert("E022".to_owned(), LintLevel::Allow);
options.apply_project_config(&config, false, false);
assert_eq!(options.lints.overrides.get("E014"), Some(&LintLevel::Deny));
assert_eq!(options.lints.overrides.get("E022"), Some(&LintLevel::Allow));
}
#[test]
fn apply_project_config_sets_deny_warnings() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig {
deny_warnings: Some(true),
..ProjectConfig::default()
};
options.apply_project_config(&config, false, false);
assert!(options.lints.deny_warnings);
}
#[test]
fn apply_project_config_absent_lints_clears_lint_policy() {
let mut options = AnalysisOptions {
lints: LintPolicy {
overrides: BTreeMap::from([("E014".to_owned(), LintLevel::Deny)]),
deny_warnings: true,
},
..AnalysisOptions::default()
};
options.apply_project_config(&ProjectConfig::default(), false, false);
assert!(
options.lints.overrides.is_empty(),
"an absent [lints] table must clear previously-resolved overrides"
);
assert!(!options.lints.deny_warnings);
}
#[test]
fn apply_project_config_omitted_code_reverts_to_base_severity() {
let mut options = AnalysisOptions {
lints: LintPolicy {
overrides: BTreeMap::from([
("E014".to_owned(), LintLevel::Deny),
("E022".to_owned(), LintLevel::Allow),
]),
deny_warnings: true,
},
..AnalysisOptions::default()
};
let mut config = ProjectConfig::default();
config.lints.insert("E014".to_owned(), LintLevel::Deny);
options.apply_project_config(&config, false, false);
assert_eq!(
options.lints.overrides.get("E014"),
Some(&LintLevel::Deny),
"a code still present in the re-applied config keeps its override"
);
assert!(
!options.lints.overrides.contains_key("E022"),
"a code omitted from the re-applied config must revert to its \
base severity, not stick"
);
assert!(
!options.lints.deny_warnings,
"deny-warnings omitted from the re-applied config must revert \
to false, not stick"
);
}
#[test]
fn apply_project_config_rejects_unknown_lint_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config.lints.insert("E9999".to_owned(), LintLevel::Deny);
let warnings = options.apply_project_config(&config, false, false);
assert!(
options.lints.overrides.is_empty(),
"an unknown code must never be merged into the policy"
);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].0.contains("E9999"));
}
#[test]
fn apply_project_config_rejects_misspelled_lint_code_case() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config.lints.insert("e014".to_owned(), LintLevel::Deny);
let warnings = options.apply_project_config(&config, false, false);
assert!(options.lints.overrides.is_empty());
assert_eq!(warnings.len(), 1);
assert!(warnings[0].0.contains("e014"));
}
#[test]
fn apply_project_config_rejects_non_overridable_lint_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
assert_eq!(
brink_ir::DiagnosticCode::E001.severity(),
brink_ir::Severity::Error
);
config.lints.insert("E001".to_owned(), LintLevel::Deny);
let warnings = options.apply_project_config(&config, false, false);
assert!(options.lints.overrides.is_empty());
assert_eq!(warnings.len(), 1);
assert!(warnings[0].0.contains("E001"));
}
#[test]
fn apply_project_config_reports_no_warnings_for_valid_overridable_codes() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config.lints.insert("E014".to_owned(), LintLevel::Deny);
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty());
}
#[test]
fn apply_project_config_accepts_info_base_lint_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
assert_eq!(
brink_ir::DiagnosticCode::E157.severity(),
brink_ir::Severity::Info
);
config.lints.insert("E157".to_owned(), LintLevel::Warn);
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty());
assert_eq!(options.lints.overrides.get("E157"), Some(&LintLevel::Warn));
}
#[test]
fn apply_project_config_rejects_unknown_fix_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config
.fix
.insert("E9999".to_owned(), brink_project_config::FixPolicy::Auto);
let warnings = options.apply_project_config(&config, false, false);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].0.contains("E9999"), "{warnings:?}");
assert!(
warnings[0].0.contains("[fix]"),
"the warning must name the table it came from, not `[lints]`'s \
wording: {warnings:?}"
);
}
#[test]
fn apply_project_config_rejects_misspelled_fix_code_case() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config
.fix
.insert("e014".to_owned(), brink_project_config::FixPolicy::Off);
let warnings = options.apply_project_config(&config, false, false);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].0.contains("e014"), "{warnings:?}");
}
#[test]
fn apply_project_config_accepts_an_error_default_fix_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
assert_eq!(
brink_ir::DiagnosticCode::E001.severity(),
brink_ir::Severity::Error
);
config
.fix
.insert("E001".to_owned(), brink_project_config::FixPolicy::Auto);
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty(), "{warnings:?}");
}
#[test]
fn apply_project_config_reports_no_warnings_for_a_valid_fix_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config
.fix
.insert("E014".to_owned(), brink_project_config::FixPolicy::Off);
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty(), "{warnings:?}");
}
#[test]
fn apply_project_config_rejects_an_unrecognized_bare_preset_name() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig {
conventions: Some("screnplay".to_owned()),
..ProjectConfig::default()
};
let warnings = options.apply_project_config(&config, false, false);
assert_eq!(
options.conventions, None,
"an unrecognized preset name must never be carried onto \
`AnalysisOptions::conventions`"
);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].0.contains("screnplay"));
}
#[test]
fn apply_project_config_accepts_screenplay_preset_name_now_that_it_shipped() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig {
conventions: Some("screenplay".to_owned()),
..ProjectConfig::default()
};
let warnings = options.apply_project_config(&config, false, false);
assert_eq!(options.conventions.as_deref(), Some("screenplay"));
assert_eq!(warnings.len(), 1, "unexpected warnings: {warnings:?}");
assert!(warnings[0].0.contains("screenplay"));
assert!(warnings[0].0.contains("not injectable yet"));
assert!(warnings[0].0.contains("#2080"));
assert!(warnings[0].0.contains("#1840"));
}
#[test]
fn apply_project_config_accepts_a_bare_path_shaped_conventions_pointer() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig {
conventions: Some("conventions.brink".to_owned()),
..ProjectConfig::default()
};
let warnings = options.apply_project_config(&config, false, false);
assert!(
warnings.is_empty(),
"a path-shaped pointer (`.brink` extension) must never be \
rejected by the preset-name closed set — that would break the \
custom-conventions-module case #1844's confinement rule is \
built around"
);
assert_eq!(options.conventions.as_deref(), Some("conventions.brink"));
}
#[test]
fn apply_project_config_accepts_a_directory_path_shaped_conventions_pointer() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig {
conventions: Some("scenes/conventions.brink".to_owned()),
..ProjectConfig::default()
};
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty());
assert_eq!(
options.conventions.as_deref(),
Some("scenes/conventions.brink")
);
}
#[test]
fn apply_project_config_leaves_conventions_unset_when_absent() {
let mut options = AnalysisOptions::default();
let config = ProjectConfig::default();
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty());
assert_eq!(options.conventions, None);
}
#[test]
fn apply_project_config_carries_a_conventions_value_reconciled_from_the_deprecated_alias() {
let mut options = AnalysisOptions::default();
let (config, parse_warnings) =
brink_project_config::parse_str("[project]\nelements = \"conventions.brink\"\n")
.expect("deprecated `elements` key must still parse");
assert_eq!(parse_warnings.len(), 1, "{parse_warnings:?}");
let warnings = options.apply_project_config(&config, false, false);
assert!(warnings.is_empty(), "{warnings:?}");
assert_eq!(options.conventions.as_deref(), Some("conventions.brink"));
}
#[test]
fn validate_conventions_preset_accepts_a_name_present_in_the_registry() {
assert!(validate_conventions_preset("screenplay", &["screenplay"]).is_ok());
}
#[test]
fn validate_conventions_preset_rejects_a_name_outside_the_registry() {
assert!(validate_conventions_preset("screnplay", &["screenplay"]).is_err());
}
#[test]
fn apply_lint_overrides_merges_per_code_overrides() {
let mut options = AnalysisOptions::default();
let mut overrides = BTreeMap::new();
overrides.insert("E014".to_owned(), LintLevel::Deny);
let warnings = options.apply_lint_overrides(&overrides, None);
assert!(warnings.is_empty());
assert_eq!(options.lints.overrides.get("E014"), Some(&LintLevel::Deny));
}
#[test]
fn apply_lint_overrides_sets_deny_warnings() {
let mut options = AnalysisOptions::default();
let warnings = options.apply_lint_overrides(&BTreeMap::new(), Some(true));
assert!(warnings.is_empty());
assert!(options.lints.deny_warnings);
}
#[test]
fn apply_lint_overrides_none_deny_warnings_leaves_it_untouched() {
let mut options = AnalysisOptions::default();
options.lints.deny_warnings = true;
options.apply_lint_overrides(&BTreeMap::new(), None);
assert!(options.lints.deny_warnings);
}
#[test]
fn apply_lint_overrides_rejects_unknown_code() {
let mut options = AnalysisOptions::default();
let mut overrides = BTreeMap::new();
overrides.insert("E9999".to_owned(), LintLevel::Deny);
let warnings = options.apply_lint_overrides(&overrides, None);
assert!(options.lints.overrides.is_empty());
assert_eq!(warnings.len(), 1);
assert!(warnings[0].0.contains("E9999"));
}
#[test]
fn apply_lint_overrides_rejects_non_overridable_code() {
let mut options = AnalysisOptions::default();
let mut overrides = BTreeMap::new();
overrides.insert("E001".to_owned(), LintLevel::Deny);
let warnings = options.apply_lint_overrides(&overrides, None);
assert!(options.lints.overrides.is_empty());
assert_eq!(warnings.len(), 1);
assert!(warnings[0].0.contains("E001"));
}
#[test]
fn apply_lint_overrides_wins_over_a_prior_apply_project_config_for_the_same_code() {
let mut options = AnalysisOptions::default();
let mut config = ProjectConfig::default();
config.lints.insert("E014".to_owned(), LintLevel::Deny);
options.apply_project_config(&config, false, false);
assert_eq!(options.lints.overrides.get("E014"), Some(&LintLevel::Deny));
let mut overrides = BTreeMap::new();
overrides.insert("E014".to_owned(), LintLevel::Allow);
options.apply_lint_overrides(&overrides, None);
assert_eq!(options.lints.overrides.get("E014"), Some(&LintLevel::Allow));
}
}
#[cfg(test)]
mod overridability_agreement {
use super::{AnalysisOptions, LintLevel};
use brink_ir::DiagnosticCode;
use std::collections::BTreeMap;
#[test]
fn agrees_with_the_analyzers_own_gate() {
let mut disagreed = Vec::new();
for code in DiagnosticCode::ALL {
let mut options = AnalysisOptions::default();
let overrides = BTreeMap::from([(code.as_str().to_owned(), LintLevel::Allow)]);
let warnings = options.apply_lint_overrides(&overrides, None);
let accepted =
warnings.is_empty() && options.lints.overrides.contains_key(code.as_str());
if accepted != code.is_overridable() {
disagreed.push((code.as_str(), code.is_overridable(), accepted));
}
}
assert!(
disagreed.is_empty(),
"is_overridable disagrees with apply_lint_overrides for \
(code, predicate, analyzer): {disagreed:?}"
);
}
#[test]
fn the_todo_note_can_be_configured() {
let mut options = AnalysisOptions::default();
let overrides = BTreeMap::from([("E189".to_owned(), LintLevel::Allow)]);
let warnings = options.apply_lint_overrides(&overrides, None);
assert!(warnings.is_empty(), "{warnings:?}");
assert_eq!(options.lints.overrides.get("E189"), Some(&LintLevel::Allow));
assert!(DiagnosticCode::E189.is_overridable());
}
}