use super::{Context, LintRule};
use crate::handler::{Handler, Traverse};
use crate::tags::{self, Tags};
use crate::Program;
use deno_ast::view::{
ArrayPat, BindingIdent, Expr, Lit, ObjectPat, Pat, TsAsExpr, TsLit, TsType,
TsTypeAnn, TsTypeAssertion, VarDecl,
};
use deno_ast::{SourceRange, SourceRanged};
use derive_more::Display;
const CODE: &str = "prefer-as-const";
#[derive(Display)]
enum PreferAsConstMessage {
#[display(
fmt = "Expected a `const` assertion instead of a literal type annotation"
)]
ExpectedConstAssertion,
}
#[derive(Display)]
enum PreferAsConstHint {
#[display(fmt = "Remove a literal type annotation and add `as const`")]
AddAsConst,
}
#[derive(Debug)]
pub struct PreferAsConst;
impl LintRule for PreferAsConst {
fn tags(&self) -> Tags {
&[tags::RECOMMENDED]
}
fn code(&self) -> &'static str {
CODE
}
fn lint_program_with_ast_view(
&self,
context: &mut Context,
program: Program,
) {
PreferAsConstHandler.traverse(program, context);
}
}
struct PreferAsConstHandler;
fn add_diagnostic_helper(range: SourceRange, ctx: &mut Context) {
ctx.add_diagnostic_with_hint(
range,
CODE,
PreferAsConstMessage::ExpectedConstAssertion,
PreferAsConstHint::AddAsConst,
);
}
fn compare(
type_ann: &TsType,
expr: &Expr,
range: SourceRange,
ctx: &mut Context,
) {
if let TsType::TsLitType(lit_type) = type_ann {
if let Expr::Lit(expr_lit) = expr {
match (expr_lit, &lit_type.lit) {
(Lit::Str(value_literal), TsLit::Str(type_literal)) => {
if value_literal.value() == type_literal.value() {
add_diagnostic_helper(range, ctx)
}
}
(Lit::Num(value_literal), TsLit::Number(type_literal)) => {
if (value_literal.value() - type_literal.value()).abs() < f64::EPSILON
{
add_diagnostic_helper(range, ctx)
}
}
_ => {}
}
}
}
}
impl Handler for PreferAsConstHandler {
fn ts_as_expr(&mut self, as_expr: &TsAsExpr, ctx: &mut Context) {
compare(
&as_expr.type_ann,
&as_expr.expr,
as_expr.type_ann.range(),
ctx,
);
}
fn ts_type_assertion(
&mut self,
type_assertion: &TsTypeAssertion,
ctx: &mut Context,
) {
compare(
&type_assertion.type_ann,
&type_assertion.expr,
type_assertion.type_ann.range(),
ctx,
);
}
fn var_decl(&mut self, var_decl: &VarDecl, ctx: &mut Context) {
for decl in var_decl.decls {
if let Some(init) = &decl.init {
if let Pat::Array(ArrayPat { type_ann, .. })
| Pat::Object(ObjectPat { type_ann, .. })
| Pat::Ident(BindingIdent { type_ann, .. }) = &decl.name
{
if let Some(TsTypeAnn { type_ann, .. }) = &type_ann {
compare(type_ann, init, type_ann.range(), ctx);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefer_as_const_valid() {
assert_lint_ok! {
PreferAsConst,
"let foo = 'baz' as const;",
"let foo = 1 as const;",
"let foo = { bar: 'baz' as const };",
"let foo = { bar: 1 as const };",
"let foo = { bar: 'baz' };",
"let foo = { bar: 2 };",
"let foo = <bar>'bar';",
"let foo = <string>'bar';",
"let foo = 'bar' as string;",
"let foo = `bar` as `bar`;",
"let foo = `bar` as `foo`;",
"let foo = `bar` as 'bar';",
"let foo: string = 'bar';",
"let foo: number = 1;",
"let foo: 'bar' = baz;",
"let foo = 'bar';",
"class foo { bar: 'baz' = 'baz'; }",
"class foo { bar = 'baz'; }",
"let foo: 'bar';",
"let foo = { bar };",
"let foo: 'baz' = 'baz' as const;",
};
}
#[test]
fn prefer_as_const_invalid() {
assert_lint_err! {
PreferAsConst,
"let foo = { bar: 'baz' as 'baz' };": [
{
col: 26,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo = { bar: 1 as 1 };": [
{
col: 22,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let [x]: 'bar' = 'bar';": [
{
col: 9,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let {x}: 'bar' = 'bar';": [
{
col: 9,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo: 'bar' = 'bar';": [
{
col: 9,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo: 2 = 2;": [
{
col: 9,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo: 'bar' = 'bar' as 'bar';": [
{
col: 26,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo = <'bar'>'bar';": [
{
col: 11,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo = <4>4;": [
{
col: 11,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo = 'bar' as 'bar';": [
{
col: 19,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo = 5 as 5;": [
{
col: 15,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo: 1.23456 = 1.23456;": [
{
col: 9,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo: 2 = 2, bar: 3 = 3;": [
{
col: 9,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
},
{
col: 21,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
"let foo = () => { let x: 'x' = 'x'; };": [
{
col: 25,
message: PreferAsConstMessage::ExpectedConstAssertion,
hint: PreferAsConstHint::AddAsConst,
}
],
};
}
}