use crate::TypeDatabase;
use crate::judge::JudgeConfig;
use crate::subtype::{SubtypeChecker, TypeEnvironment};
use crate::types::{TypeData, TypeId};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum SoundDiagnosticCode {
ExcessPropertyStickyFreshness = 9001,
MutableArrayCovariance = 9002,
MethodBivariance = 9003,
AnyEscape = 9004,
EnumNumberAssignment = 9005,
MissingIndexSignature = 9006,
UnsafeTypeAssertion = 9007,
UncheckedIndexedAccess = 9008,
}
impl SoundDiagnosticCode {
pub const fn code(self) -> u32 {
self as u32
}
pub const fn message(self) -> &'static str {
match self {
Self::ExcessPropertyStickyFreshness => {
"Object literal has excess property '{0}' which will be silently lost when assigned to type '{1}'."
}
Self::MutableArrayCovariance => {
"Type '{0}[]' is not safely assignable to type '{1}[]'. Array is mutable and may receive incompatible elements."
}
Self::MethodBivariance => {
"Method parameter type '{0}' is not contravariant with '{1}'. Methods should use strict parameter checking."
}
Self::AnyEscape => {
"Type 'any' is being used to bypass type checking. Consider using a more specific type or 'unknown'."
}
Self::EnumNumberAssignment => {
"Enum '{0}' should not be assigned to/from number without explicit conversion."
}
Self::MissingIndexSignature => {
"Type '{0}' is being used as a map but lacks an index signature. Add '[key: string]: {1}' to the type."
}
Self::UnsafeTypeAssertion => {
"Type assertion from '{0}' to '{1}' may be unsafe. The types do not overlap sufficiently."
}
Self::UncheckedIndexedAccess => {
"Indexed access '{0}[{1}]' may return undefined. Add a null check or enable noUncheckedIndexedAccess."
}
}
}
}
#[derive(Clone, Debug)]
pub struct SoundDiagnostic {
pub code: SoundDiagnosticCode,
pub args: Vec<String>,
pub location: Option<(u32, u32, u32)>,
}
impl SoundDiagnostic {
pub const fn new(code: SoundDiagnosticCode) -> Self {
Self {
code,
args: Vec::new(),
location: None,
}
}
pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub const fn with_location(mut self, file_id: u32, start: u32, end: u32) -> Self {
self.location = Some((file_id, start, end));
self
}
pub fn format_message(&self) -> String {
let mut msg = self.code.message().to_string();
for (i, arg) in self.args.iter().enumerate() {
let placeholder = format!("{{{i}}}");
msg = msg.replace(&placeholder, arg);
}
msg
}
}
pub struct SoundLawyer<'a> {
db: &'a dyn TypeDatabase,
env: &'a TypeEnvironment,
config: JudgeConfig,
}
impl<'a> SoundLawyer<'a> {
pub fn new(db: &'a dyn TypeDatabase, env: &'a TypeEnvironment, config: JudgeConfig) -> Self {
SoundLawyer { db, env, config }
}
pub fn is_assignable(&mut self, source: TypeId, target: TypeId) -> bool {
if source == target {
return true;
}
if target == TypeId::UNKNOWN {
return true;
}
if source == TypeId::NEVER {
return true;
}
if target == TypeId::ANY {
return true;
}
if source.is_any() {
return target.is_any_or_unknown();
}
if source.is_error() || target.is_error() {
return source == target;
}
let mut checker = SubtypeChecker::with_resolver(self.db, self.env);
checker.strict_function_types = true; checker.allow_void_return = false; checker.allow_bivariant_rest = false; checker.disable_method_bivariance = true; checker.strict_null_checks = self.config.strict_null_checks;
checker.exact_optional_property_types = self.config.exact_optional_property_types;
checker.no_unchecked_indexed_access = self.config.no_unchecked_indexed_access;
checker.is_subtype_of(source, target)
}
pub fn check_assignment(
&mut self,
source: TypeId,
target: TypeId,
diagnostics: &mut Vec<SoundDiagnostic>,
) -> bool {
if self.is_any_escape(source, target) {
diagnostics.push(SoundDiagnostic::new(SoundDiagnosticCode::AnyEscape));
return false;
}
if let Some(diag) = self.check_array_covariance(source, target) {
diagnostics.push(diag);
return false;
}
self.is_assignable(source, target)
}
fn is_any_escape(&self, source: TypeId, target: TypeId) -> bool {
source == TypeId::ANY && target != TypeId::ANY && target != TypeId::UNKNOWN
}
fn check_array_covariance(&self, source: TypeId, target: TypeId) -> Option<SoundDiagnostic> {
let source_key = self.db.lookup(source)?;
let target_key = self.db.lookup(target)?;
if let (TypeData::Array(s_elem), TypeData::Array(t_elem)) = (&source_key, &target_key)
&& s_elem != t_elem
{
let mut checker = SubtypeChecker::with_resolver(self.db, self.env);
checker.strict_function_types = true;
if checker.is_subtype_of(*s_elem, *t_elem) && !checker.is_subtype_of(*t_elem, *s_elem) {
return Some(
SoundDiagnostic::new(SoundDiagnosticCode::MutableArrayCovariance)
.with_arg(format!("{s_elem:?}"))
.with_arg(format!("{t_elem:?}")),
);
}
}
None
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SoundModeConfig {
pub sticky_freshness: bool,
pub strict_any: bool,
pub strict_array_covariance: bool,
pub strict_method_bivariance: bool,
pub strict_enums: bool,
}
impl Default for SoundModeConfig {
fn default() -> Self {
Self {
sticky_freshness: true,
strict_any: true,
strict_array_covariance: true,
strict_method_bivariance: true,
strict_enums: true,
}
}
}
impl SoundModeConfig {
pub fn all() -> Self {
Self::default()
}
pub const fn minimal() -> Self {
Self {
sticky_freshness: true,
strict_any: false,
strict_array_covariance: false,
strict_method_bivariance: false,
strict_enums: false,
}
}
}
#[cfg(test)]
#[path = "../tests/sound_tests.rs"]
mod tests;