use std::fmt;
use apollo_compiler::{
database::{AstStorage, DocumentStorage, HirStorage, InputStorage},
AstDatabase, DocumentDatabase, HirDatabase, InputDatabase,
};
use miette::{Diagnostic, Report, SourceSpan};
use thiserror::Error;
pub struct Linter {
pub db: LinterDatabase,
}
impl Linter {
pub fn new(input: &str) -> Self {
let mut db = LinterDatabase::default();
let input = input.to_string();
db.set_input(input);
Self { db }
}
pub fn lint(&self) -> Vec<LintDiagnostic> {
self.db.lint()
}
}
#[salsa::database(
DocumentStorage,
InputStorage,
AstStorage,
HirStorage,
LintValidationStorage
)]
#[derive(Default)]
pub struct LinterDatabase {
pub storage: salsa::Storage<LinterDatabase>,
}
impl salsa::Database for LinterDatabase {}
impl salsa::ParallelDatabase for LinterDatabase {
fn snapshot(&self) -> salsa::Snapshot<LinterDatabase> {
salsa::Snapshot::new(LinterDatabase {
storage: self.storage.snapshot(),
})
}
}
pub trait Upcast<T: ?Sized> {
fn upcast(&self) -> &T;
}
impl Upcast<dyn DocumentDatabase> for LinterDatabase {
fn upcast(&self) -> &(dyn DocumentDatabase + 'static) {
self
}
}
#[salsa::query_group(LintValidationStorage)]
pub trait LintValidation:
Upcast<dyn DocumentDatabase> + InputDatabase + AstDatabase + HirDatabase
{
fn lint(&self) -> Vec<LintDiagnostic>;
fn capitalised_definitions(&self) -> Vec<LintDiagnostic>;
}
fn lint(db: &dyn LintValidation) -> Vec<LintDiagnostic> {
let mut lints = Vec::new();
lints.extend(db.capitalised_definitions());
lints
}
fn capitalised_definitions(db: &dyn LintValidation) -> Vec<LintDiagnostic> {
let lints: Vec<LintDiagnostic> = db
.db_definitions()
.iter()
.filter_map(|def| {
if !def.name()?.chars().next()?.is_uppercase() {
if let Some(node) = def.name_src()?.ast_node(db.upcast()) {
let offset: usize = node.text_range().start().into();
let len: usize = node.text_range().len().into();
Some(LintDiagnostic::CapitalisedDefinitions(
CapitalisedDefinitions {
src: db.input(),
definition: (offset, len).into(),
},
))
} else {
None
}
} else {
None
}
})
.collect();
lints
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum LintDiagnostic {
CapitalisedDefinitions(CapitalisedDefinitions),
}
impl LintDiagnostic {
pub fn report(&self) -> Report {
match self {
LintDiagnostic::CapitalisedDefinitions(lint) => Report::new(lint.clone()),
}
}
}
impl fmt::Display for LintDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{:?}", self.report())
}
}
#[derive(Diagnostic, Debug, Error, Clone, Hash, PartialEq, Eq)]
#[error("definitions should be capitalised")]
#[diagnostic(code("graphql linter diagnostic"))]
pub struct CapitalisedDefinitions {
#[source_code]
pub src: String,
#[label = "capitalise this definition"]
pub definition: SourceSpan,
}
fn main() {
let input = r#"
type query {
topProducts: Product
customer: User
}
type product {
type: String
price(setPrice: Int): Int
}
type user {
id: ID
name: String
profilePic(size: Int): URL
}
scalar url @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
"#;
let linter = Linter::new(input);
let lints = linter.lint();
for lint in &lints {
println!("{}", lint)
}
}