use std::{fmt, path::Path, sync::Arc};
use apollo_compiler::{
database::{AstStorage, HirStorage, InputStorage},
AstDatabase, FileId, HirDatabase, InputDatabase, Source,
};
use miette::{Diagnostic, Report, SourceSpan};
use thiserror::Error;
#[derive(Default)]
pub struct Linter {
pub db: LinterDatabase,
}
impl Linter {
pub fn new() -> Self {
Default::default()
}
pub fn document(&mut self, input: &str, path: impl AsRef<Path>) -> FileId {
let id = FileId::new();
self.db.set_input(
id,
Source::document(path.as_ref().to_owned(), input.to_string()),
);
let mut source_files = self.db.source_files();
source_files.push(id);
self.db.set_source_files(source_files);
id
}
pub fn lint(&self) -> Vec<LintDiagnostic> {
self.db.lint()
}
}
#[salsa::database(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 HirDatabase> for LinterDatabase {
fn upcast(&self) -> &(dyn HirDatabase + 'static) {
self
}
}
#[salsa::query_group(LintValidationStorage)]
pub trait LintValidation:
Upcast<dyn HirDatabase> + 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> {
db.all_operations()
.iter()
.filter_map(|def| def.name_src())
.chain(
db.types_definitions_by_name()
.values()
.map(|def| def.name_src()),
)
.chain(db.all_fragments().values().map(|def| def.name_src()))
.chain(
db.directive_definitions()
.values()
.map(|def| def.name_src()),
)
.filter_map(|name| {
if !name.src().chars().next()?.is_uppercase() {
if let Some(loc) = name.loc() {
let offset = loc.offset();
let len = loc.node_len();
Some(LintDiagnostic::CapitalisedDefinitions(
CapitalisedDefinitions {
src: db.source_code(loc.file_id()),
definition: (offset, len).into(),
},
))
} else {
None
}
} else {
None
}
})
.collect()
}
#[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: Arc<str>,
#[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 mut linter = Linter::new();
linter.document(input, "document.graphql");
let lints = linter.lint();
for lint in &lints {
println!("{}", lint)
}
}