use std::collections::{BTreeMap, BTreeSet};
use super::{
ident::{is_valid_kotlin_package, is_writable_kotlin_ident},
model::{
KtBody, KtClass, KtClassKind, KtClassModifier, KtCompanion, KtCtorParam, KtDecl, KtFile,
KtFun, KtParam,
},
slot::KtPropertyValue,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Check {
InvalidIdentifier,
InvalidPackage,
DuplicateType,
DuplicateValue,
DuplicateFunction,
DuplicateRaw,
PropertyWithoutTypeOrValue,
EnumEntryMissingArguments,
FunctionWithoutBody,
ImportCollision,
}
impl Check {
pub const ALL: &'static [Check] = &[
Check::InvalidIdentifier,
Check::InvalidPackage,
Check::DuplicateType,
Check::DuplicateValue,
Check::DuplicateFunction,
Check::DuplicateRaw,
Check::PropertyWithoutTypeOrValue,
Check::EnumEntryMissingArguments,
Check::FunctionWithoutBody,
Check::ImportCollision,
];
pub fn name(self) -> &'static str {
match self {
Check::InvalidIdentifier => "invalid-identifier",
Check::InvalidPackage => "invalid-package",
Check::DuplicateType => "duplicate-type",
Check::DuplicateValue => "duplicate-value",
Check::DuplicateFunction => "duplicate-function",
Check::DuplicateRaw => "duplicate-raw",
Check::PropertyWithoutTypeOrValue => "property-without-type-or-value",
Check::EnumEntryMissingArguments => "enum-entry-missing-arguments",
Check::FunctionWithoutBody => "function-without-body",
Check::ImportCollision => "import-collision",
}
}
}
impl std::fmt::Display for Check {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Warning,
Error,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Severity::Warning => "warning",
Severity::Error => "error",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub check: Check,
pub severity: Severity,
pub scope: String,
pub message: String,
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} [{}] in `{}`: {}",
self.severity, self.check, self.scope, self.message
)
}
}
#[derive(Clone, Debug, Default)]
pub struct ValidationPolicy {
overrides: BTreeMap<Check, Option<Severity>>,
}
impl ValidationPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn warn(mut self, check: Check) -> Self {
self.overrides.insert(check, Some(Severity::Warning));
self
}
pub fn deny(mut self, check: Check) -> Self {
self.overrides.insert(check, Some(Severity::Error));
self
}
pub fn warn_all() -> Self {
let mut policy = Self::new();
for check in Check::ALL {
policy = policy.warn(*check);
}
policy
}
pub fn allow(mut self, check: Check) -> Self {
self.overrides.insert(check, None);
self
}
pub fn severity_of(&self, check: Check) -> Option<Severity> {
self.overrides
.get(&check)
.copied()
.unwrap_or(Some(Severity::Error))
}
}
fn scope_join(scope: &str, child: &str) -> String {
if scope.is_empty() {
child.to_string()
} else {
format!("{scope}/{child}")
}
}
pub(crate) struct Diagnostics<'a> {
policy: &'a ValidationPolicy,
out: Vec<Diagnostic>,
}
impl<'a> Diagnostics<'a> {
pub(crate) fn new(policy: &'a ValidationPolicy) -> Self {
Self {
policy,
out: Vec::new(),
}
}
pub(crate) fn push(&mut self, check: Check, scope: &str, message: String) {
if let Some(severity) = self.policy.severity_of(check) {
self.out.push(Diagnostic {
check,
severity,
scope: scope.to_string(),
message,
});
}
}
pub(crate) fn finish(self) -> Vec<Diagnostic> {
self.out
}
}
impl KtFile {
pub fn validate(&self) -> Vec<Diagnostic> {
self.validate_with(&ValidationPolicy::new())
}
pub fn validate_with(&self, policy: &ValidationPolicy) -> Vec<Diagnostic> {
let mut d = Diagnostics::new(policy);
check_identifiers(self, &mut d);
check_scope(&self.decls, &[], None, &self.package, &mut d);
check_shapes(&self.decls, Container::File, &self.package, &mut d);
check_extra_imports(self, &mut d);
d.finish()
}
}
fn check_scope<'a>(
decls: &'a [KtDecl],
ctor_params: &'a [KtCtorParam],
companion: Option<&'a KtCompanion>,
scope: &str,
d: &mut Diagnostics<'_>,
) {
let mut types: BTreeSet<&str> = BTreeSet::new();
let mut values: BTreeSet<&str> = BTreeSet::new();
let mut funs: BTreeSet<String> = BTreeSet::new();
let mut raws: BTreeSet<&str> = BTreeSet::new();
for p in ctor_params.iter().filter(|p| p.prop.is_some()) {
if !p.name.is_empty() && !values.insert(&p.name) {
d.push(
Check::DuplicateValue,
scope,
format!("duplicate value `{}` (constructor property)", p.name),
);
}
}
for decl in decls {
match decl {
KtDecl::Class(c) => {
declare_name(&mut types, &c.name, Check::DuplicateType, "type", scope, d);
check_class_scope(c, scope, d);
}
KtDecl::FunInterface(i) => {
declare_name(&mut types, &i.name, Check::DuplicateType, "type", scope, d);
}
KtDecl::TypeAlias { name, .. } => {
declare_name(&mut types, name, Check::DuplicateType, "type", scope, d);
}
KtDecl::Property(p) => {
declare_name(
&mut values,
&p.name,
Check::DuplicateValue,
"value",
scope,
d,
);
}
KtDecl::Fun(f) => {
if f.name.is_empty() {
continue;
}
let sig = fun_signature(f);
if !funs.insert(sig.clone()) {
d.push(
Check::DuplicateFunction,
scope,
format!("duplicate function `{sig}`"),
);
}
}
KtDecl::Raw { name, .. } => {
declare_name(&mut raws, name, Check::DuplicateRaw, "raw block", scope, d);
}
}
}
if let Some(name) = companion.and_then(|c| c.name.as_deref()) {
if !name.is_empty() && !types.insert(name) {
d.push(
Check::DuplicateType,
scope,
format!("companion object `{name}` collides with another type of that name"),
);
}
}
}
fn fun_signature(f: &KtFun) -> String {
let params = param_signature(&f.params);
match &f.receiver {
Some(r) if r.needs_receiver_parens() => format!("({r}).{}({params})", f.name),
Some(r) => format!("{r}.{}({params})", f.name),
None => format!("{}({params})", f.name),
}
}
fn param_signature(params: &[KtParam]) -> String {
params
.iter()
.map(|p| p.ty.to_string())
.collect::<Vec<_>>()
.join(", ")
}
fn declare_name<'a>(
set: &mut BTreeSet<&'a str>,
name: &'a str,
check: Check,
what: &str,
scope: &str,
d: &mut Diagnostics<'_>,
) {
if name.is_empty() {
return;
}
if !set.insert(name) {
d.push(check, scope, format!("duplicate {what} `{name}`"));
}
}
fn check_class_scope(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
let inner = scope_join(scope, &c.name);
check_scope(
&c.members,
c.ctor_params(),
c.companion.as_deref(),
&inner,
d,
);
if let Some(comp) = &c.companion {
let cscope = scope_join(&inner, comp.name.as_deref().unwrap_or("Companion"));
check_scope(&comp.members, &[], None, &cscope, d);
}
}
fn check_extra_imports(file: &KtFile, d: &mut Diagnostics<'_>) {
let mut by_simple: BTreeMap<&str, &str> = BTreeMap::new();
for imp in &file.extra_imports {
let simple = imp.rsplit_once('.').map(|(_, s)| s).unwrap_or(imp.as_str());
if simple.chars().next().is_some_and(|c| c.is_lowercase()) {
continue;
}
let owner = *by_simple.entry(simple).or_insert(imp.as_str());
if owner != imp.as_str() {
d.push(
Check::ImportCollision,
&file.package,
format!("import simple-name collision: `{owner}` and `{imp}`"),
);
}
}
}
fn check_identifiers(file: &KtFile, d: &mut Diagnostics<'_>) {
if !is_valid_kotlin_package(&file.package) {
d.push(
Check::InvalidPackage,
&file.package,
format!("`{}` is not a valid Kotlin package path", file.package),
);
}
for decl in &file.decls {
check_decl_identifiers(decl, &file.package, d);
}
}
fn check_ident(name: &str, what: &str, scope: &str, d: &mut Diagnostics<'_>) {
if !is_writable_kotlin_ident(name) {
d.push(
Check::InvalidIdentifier,
scope,
format!("{what} name `{name}` is not a valid Kotlin identifier"),
);
}
}
fn check_decl_identifiers(decl: &KtDecl, scope: &str, d: &mut Diagnostics<'_>) {
match decl {
KtDecl::Class(c) => check_class_identifiers(c, scope, d),
KtDecl::Fun(f) => {
check_ident(&f.name, "function", scope, d);
let inner = scope_join(scope, &f.name);
for p in &f.params {
check_ident(&p.name, "parameter", &inner, d);
}
}
KtDecl::FunInterface(i) => {
check_ident(&i.name, "fun interface", scope, d);
let inner = scope_join(scope, &i.name);
check_ident(&i.method.name, "function", &inner, d);
let method = scope_join(&inner, &i.method.name);
for p in &i.method.params {
check_ident(&p.name, "parameter", &method, d);
}
}
KtDecl::Property(p) => check_ident(&p.name, "property", scope, d),
KtDecl::TypeAlias { name, .. } => check_ident(name, "type alias", scope, d),
KtDecl::Raw { .. } => {}
}
}
fn check_class_identifiers(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
check_ident(&c.name, "class", scope, d);
let inner = scope_join(scope, &c.name);
for p in c.ctor_params() {
check_ident(&p.name, "constructor parameter", &inner, d);
}
for e in c.kind.entries() {
check_ident(&e.name, "enum entry", &inner, d);
}
for m in &c.members {
check_decl_identifiers(m, &inner, d);
}
if let Some(comp) = &c.companion {
if let Some(name) = &comp.name {
check_ident(name, "companion object", &inner, d);
}
let cscope = scope_join(&inner, comp.name.as_deref().unwrap_or("Companion"));
for m in &comp.members {
check_decl_identifiers(m, &cscope, d);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Container {
File,
Interface,
Abstract,
Concrete,
}
impl Container {
fn of(c: &KtClass) -> Self {
match &c.kind {
KtClassKind::Interface | KtClassKind::SealedInterface => Container::Interface,
KtClassKind::Class {
modifier: Some(KtClassModifier::Abstract) | Some(KtClassModifier::Sealed),
..
} => Container::Abstract,
_ => Container::Concrete,
}
}
}
fn check_shapes(decls: &[KtDecl], container: Container, scope: &str, d: &mut Diagnostics<'_>) {
for decl in decls {
match decl {
KtDecl::Property(p) => {
if p.ty.is_none()
&& matches!(p.value, KtPropertyValue::None)
&& p.accessors.is_none()
{
d.push(
Check::PropertyWithoutTypeOrValue,
scope,
format!(
"property `{}` has no type, no value and no accessors",
p.name
),
);
}
}
KtDecl::Fun(f) => {
if !matches!(f.body, KtBody::None) {
continue;
}
let allowed = match container {
Container::Interface => true,
Container::Abstract => f
.modifiers
.iter()
.any(|m| m.split(' ').any(|w| w == "abstract")),
Container::File | Container::Concrete => false,
};
if !allowed {
d.push(
Check::FunctionWithoutBody,
scope,
format!(
"function `{}` has no body; only an interface member, or an \
`abstract` member of an abstract class, may omit one",
f.name
),
);
}
}
KtDecl::Class(c) => check_class_shapes(c, scope, d),
KtDecl::FunInterface(_) | KtDecl::TypeAlias { .. } | KtDecl::Raw { .. } => {}
}
}
}
fn check_class_shapes(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
let inner = scope_join(scope, &c.name);
if let KtClassKind::Enum { ctor, entries } = &c.kind {
if !ctor.is_empty() {
for e in entries.iter().filter(|e| e.args.is_none()) {
d.push(
Check::EnumEntryMissingArguments,
&inner,
format!(
"entry `{}` passes no arguments, but the enum declares {} \
constructor parameter(s)",
e.name,
ctor.len()
),
);
}
}
}
check_shapes(&c.members, Container::of(c), &inner, d);
if let Some(comp) = &c.companion {
let cscope = scope_join(&inner, comp.name.as_deref().unwrap_or("Companion"));
check_shapes(&comp.members, Container::Concrete, &cscope, d);
}
}