Repository: denoland/deno
Pull request: fix(unstable): add missing decorators nodes in lint ast

Support for decorators in the lint plugin AST was a bit half baked. This PR goes through all the places where decorators can be set in TS and supports these.

Fixes https://github.com/denoland/deno/issues/28830

Changed files:
filename: cli/tools/lint/ast_buffer/buffer.rs
patch: @@ -534,6 +534,31 @@ impl SerializeCtx {
     }
   }
 
+  pub fn write_maybe_ref_vec_skip<P>(
+    &mut self,
+    prop: P,
+    parent_ref: &PendingRef,
+    value: Option<Vec<NodeRef>>,
+  ) where
+    P: Into<u8> + Display + Clone,
+  {
+    if let Some(value) = value {
+      self.write_ref_vec(prop, parent_ref, value);
+    }
+  }
+
+  pub fn write_ref_vec_or_empty<P>(
+    &mut self,
+    prop: P,
+    parent_ref: &PendingRef,
+    value: Option<Vec<NodeRef>>,
+  ) where
+    P: Into<u8> + Display + Clone,
+  {
+    let actual = value.unwrap_or_default();
+    self.write_ref_vec(prop, parent_ref, actual)
+  }
+
   /// Serialize all information we have into a buffer that can be sent to JS.
   /// It has the following structure:
   ///

filename: cli/tools/lint/ast_buffer/swc.rs
patch: @@ -294,6 +294,13 @@ fn serialize_module_decl(
 
           let body = ctx.write_class_body(&node.class.span, members);
 
+          let decorators = node
+            .class
+            .decorators
+            .iter()
+            .map(|deco| serialize_decorator(ctx, deco))
+            .collect::<Vec<_>>();
+
           let decl = ctx.write_class_decl(
             &node.class.span,
             false,
@@ -302,6 +309,7 @@ fn serialize_module_decl(
             super_class,
             implements,
             body,
+            decorators,
           );
 
           (false, decl)
@@ -320,7 +328,15 @@ fn serialize_module_decl(
           let params = fn_obj
             .params
             .iter()
-            .map(|param| serialize_pat(ctx, &param.pat))
+            .map(|param| {
+              let decorators = param
+                .decorators
+                .iter()
+                .map(|deco| serialize_decorator(ctx, deco))
+                .collect::<Vec<_>>();
+
+              serialize_pat(ctx, &param.pat, Some(decorators))
+            })
             .collect::<Vec<_>>();
 
           let return_type =
@@ -564,7 +580,10 @@ fn serialize_stmt(ctx: &mut TsEsTreeBuilder, stmt: &Stmt) -> NodeRef {
       let block = serialize_stmt(ctx, &Stmt::Block(node.block.clone()));
 
       let handler = node.handler.as_ref().map(|catch| {
-        let param = catch.param.as_ref().map(|param| serialize_pat(ctx, param));
+        let param = catch
+          .param
+          .as_ref()
+          .map(|param| serialize_pat(ctx, param, None));
 
         let body = serialize_stmt(ctx, &Stmt::Block(catch.body.clone()));
 
@@ -665,7 +684,15 @@ fn serialize_expr(ctx: &mut TsEsTreeBuilder, expr: &Expr) -> NodeRef {
       let params = fn_obj
         .params
         .iter()
-        .map(|param| serialize_pat(ctx, &param.pat))
+        .map(|param| {
+          let decorators = param
+            .decorators
+            .iter()
+            .map(|deco| serialize_decorator(ctx, deco))
+            .collect::<Vec<_>>();
+
+          serialize_pat(ctx, &param.pat, Some(decorators))
+        })
         .collect::<Vec<_>>();
 
       let return_id = maybe_serialize_ts_type_ann(ctx, &fn_obj.return_type);
@@ -755,7 +782,7 @@ fn serialize_expr(ctx: &mut TsEsTreeBuilder, expr: &Expr) -> NodeRef {
         AssignTarget::Simple(simple_assign_target) => {
           match simple_assign_target {
             SimpleAssignTarget::Ident(target) => {
-              serialize_binding_ident(ctx, target)
+              serialize_binding_ident(ctx, target, None)
             }
             SimpleAssignTarget::Member(target) => {
               serialize_expr(ctx, &Expr::Member(target.clone()))
@@ -792,10 +819,10 @@ fn serialize_expr(ctx: &mut TsEsTreeBuilder, expr: &Expr) -> NodeRef {
         }
         AssignTarget::Pat(target) => match target {
           AssignTargetPat::Array(array_pat) => {
-            serialize_pat(ctx, &Pat::Array(array_pat.clone()))
+            serialize_pat(ctx, &Pat::Array(array_pat.clone()), None)
           }
           AssignTargetPat::Object(object_pat) => {
-            serialize_pat(ctx, &Pat::Object(object_pat.clone()))
+            serialize_pat(ctx, &Pat::Object(object_pat.clone()), None)
           }
           AssignTargetPat::Invalid(_) => {
             // Ignore syntax errors
@@ -953,7 +980,7 @@ fn serialize_expr(ctx: &mut TsEsTreeBuilder, expr: &Expr) -> NodeRef {
       let params = node
         .params
         .iter()
-        .map(|param| serialize_pat(ctx, param))
+        .map(|param| serialize_pat(ctx, param, None))
         .collect::<Vec<_>>();
 
       let body = match node.body.as_ref() {
@@ -1035,12 +1062,12 @@ fn serialize_expr(ctx: &mut TsEsTreeBuilder, expr: &Expr) -> NodeRef {
     Expr::MetaProp(node) => {
       let (meta, prop) = match node.kind {
         MetaPropKind::NewTarget => (
-          ctx.write_identifier(&node.span, "new", false, None),
-          ctx.write_identifier(&node.span, "target", false, None),
+          ctx.write_identifier(&node.span, "new", false, None, None),
+          ctx.write_identifier(&node.span, "target", false, None, None),
         ),
         MetaPropKind::ImportMeta => (
-          ctx.write_identifier(&node.span, "import", false, None),
-          ctx.write_identifier(&node.span, "meta", false, None),
+          ctx.write_identifier(&node.span, "import", false, None, None),
+          ctx.write_identifier(&node.span, "meta", false, None, None),
         ),
       };
       ctx.write_meta_prop(&node.span, meta, prop)
@@ -1068,7 +1095,8 @@ fn serialize_expr(ctx: &mut TsEsTreeBuilder, expr: &Expr) -> NodeRef {
     Expr::TsConstAssertion(node) => {
       let expr = serialize_expr(ctx, node.expr.as_ref());
 
-      let type_name = ctx.write_identifier(&node.span, "const", false, None);
+      let type_name =
+        ctx.write_identifier(&node.span, "const", false, None, None);
       let type_ann = ctx.write_ts_type_ref(&node.span, type_name, None);
 
       ctx.write_ts_as_expr(&node.span, expr, type_ann)
@@ -1165,7 +1193,8 @@ fn serialize_prop_or_spread(
           let left = serialize_ident(ctx, &assign_prop.key, None);
           let right = serialize_expr(ctx, assign_prop.value.as_ref());
 
-          let child_pos = ctx.write_assign_pat(&assign_prop.span, left, right);
+          let child_pos =
+            ctx.write_assign_pat(&assign_prop.span, left, right, None);
 
           (left, child_pos)
         }
@@ -1294,6 +1323,7 @@ fn serialize_ident(
     ident.sym.as_str(),
     ident.optional,
     type_ann,
+    None,
   )
 }
 
@@ -1334,6 +1364,13 @@ fn serialize_decl(ctx: &mut TsEsTreeBuilder, decl: &Decl) -> NodeRef {
 
       let body = ctx.write_class_body(&node.class.span, members);
 
+      let decorators = node
+        .class
+        .decorators
+        .iter()
+        .map(|deco| serialize_decorator(ctx, deco))
+        .collect::<Vec<_>>();
+
       ctx.write_class_decl(
         &node.class.span,
         false,
@@ -1342,6 +1379,7 @@ fn serialize_decl(ctx: &mut TsEsTreeBuilder, decl: &Decl) -> NodeRef {
         super_class,
         implements,
         body,
+        decorators,
       )
     }
     Decl::Fn(node) => {
@@ -1361,7 +1399,7 @@ fn serialize_decl(ctx: &mut TsEsTreeBuilder, decl: &Decl) -> NodeRef {
         .function
         .params
         .iter()
-        .map(|param| serialize_pat(ctx, &param.pat))
+        .map(|param| serialize_pat(ctx, &param.pat, None))
         .collect::<Vec<_>>();
 
       if let Some(body) = body {
@@ -1394,7 +1432,7 @@ fn serialize_decl(ctx: &mut TsEsTreeBuilder, decl: &Decl) -> NodeRef {
         .decls
         .iter()
         .map(|decl| {
-          let ident = serialize_pat(ctx, &decl.name);
+          let ident = serialize_pat(ctx, &decl.name, None);
           let init = decl
             .init
             .as_ref()
@@ -1423,7 +1461,7 @@ fn serialize_decl(ctx: &mut TsEsTreeBuilder, decl: &Decl) -> NodeRef {
         .decls
         .iter()
         .map(|decl| {
-          let ident = serialize_pat(ctx, &decl.name);
+          let ident = serialize_pat(ctx, &decl.name, None);
           let init = decl
             .init
             .as_ref()
@@ -1854,25 +1892,39 @@ fn serialize_jsx_identifier(
   ctx.write_jsx_identifier(&node.span, &node.sym)
 }
 
-fn serialize_pat(ctx: &mut TsEsTreeBuilder, pat: &Pat) -> NodeRef {
+fn serialize_pat(
+  ctx: &mut TsEsTreeBuilder,
+  pat: &Pat,
+  decorators: Option<Vec<NodeRef>>,
+) -> NodeRef {
   match pat {
-    Pat::Ident(node) => serialize_binding_ident(ctx, node),
+    Pat::Ident(node) => serialize_binding_ident(ctx, node, decorators),
     Pat::Array(node) => {
       let type_ann = maybe_serialize_ts_type_ann(ctx, &node.type_ann);
 
       let children = node
         .elems
         .iter()
-        .map(|pat| pat.as_ref().map_or(NodeRef(0), |v| serialize_pat(ctx, v)))
+        .map(|pat| {
+          pat
+            .as_ref()
+            .map_or(NodeRef(0), |v| serialize_pat(ctx, v, None))
+        })
         .collect::<Vec<_>>();
 
-      ctx.write_arr_pat(&node.span, node.optional, type_ann, children)
+      ctx.write_arr_pat(
+        &node.span,
+        node.optional,
+        type_ann,
+        children,
+        decorators,
+      )
     }
     Pat::Rest(node) => {
       let type_ann = maybe_serialize_ts_type_ann(ctx, &node.type_ann);
-      let arg = serialize_pat(ctx, &node.arg);
+      let arg = serialize_pat(ctx, &node.arg, None);
 
-      ctx.write_rest_elem(&node.span, type_ann, arg)
+      ctx.write_rest_elem(&node.span, type_ann, arg, decorators)
     }
     Pat::Object(node) => {
       let type_ann = maybe_serialize_ts_type_ann(ctx, &node.type_ann);
@@ -1885,7 +1937,7 @@ fn serialize_pat(ctx: &mut TsEsTreeBuilder, pat: &Pat) -> NodeRef {
             let computed = matches!(key_value_prop.key, PropName::Computed(_));
 
             let key = serialize_prop_name(ctx, &key_value_prop.key);
-            let value = serialize_pat(ctx, key_value_prop.value.as_ref());
+            let value = serialize_pat(ctx, key_value_prop.value.as_ref(), None);
 
             ctx.write_property(
               &key_value_prop.span(),
@@ -1898,14 +1950,16 @@ fn serialize_pat(ctx: &mut TsEsTreeBuilder, pat: &Pat) -> NodeRef {
             )
           }
           ObjectPatProp::Assign(assign_pat_prop) => {
-            let key = serialize_binding_ident(ctx, &assign_pat_prop.key);
-            let mut value = serialize_binding_ident(ctx, &assign_pat_prop.key);
+            let key = serialize_binding_ident(ctx, &assign_pat_prop.key, None);
+            let mut value =
+              serialize_binding_ident(ctx, &assign_pat_prop.key, None);
 
             let shorthand = assign_pat_prop.value.is_none();
 
             if let Some(assign) = &assign_pat_prop.value {
               let expr = serialize_expr(ctx, assign);
-              value = ctx.write_assign_pat(&assign_pat_prop.span, value, expr);
+              value =
+                ctx.write_assign_pat(&assign_pat_prop.span, value, expr, None);
             }
 
             ctx.write_property(
@@ -1919,18 +1973,24 @@ fn serialize_pat(ctx: &mut TsEsTreeBuilder, pat: &Pat) -> NodeRef {
             )
           }
           ObjectPatProp::Rest(rest_pat) => {
-            serialize_pat(ctx, &Pat::Rest(rest_pat.clone()))
+            serialize_pat(ctx, &Pat::Rest(rest_pat.clone()), None)
           }
         })
         .collect::<Vec<_>>();
 
-      ctx.write_obj_pat(&node.span, node.optional, type_ann, children)
+      ctx.write_obj_pat(
+        &node.span,
+        node.optional,
+        type_ann,
+        children,
+        decorators,
+      )
     }
     Pat::Assign(node) => {
-      let left = serialize_pat(ctx, &node.left);
+      let left = serialize_pat(ctx, &node.left, None);
       let right = serialize_expr(ctx, &node.right);
 
-      ctx.write_assign_pat(&node.span, left, right)
+      ctx.write_assign_pat(&node.span, left, right, decorators)
     }
     Pat::Invalid(_) => {
       // Ignore syntax errors
@@ -1951,7 +2011,7 @@ fn serialize_for_head(
     ForHead::UsingDecl(using_decl) => {
       serialize_decl(ctx, &Decl::Using(using_decl.clone()))
     }
-    ForHead::Pat(pat) => serialize_pat(ctx, pat),
+    ForHead::Pat(pat) => serialize_pat(ctx, pat, None),
   }
 }
 
@@ -1968,7 +2028,13 @@ fn serialize_ident_name(
   ctx: &mut TsEsTreeBuilder,
   ident_name: &IdentName,
 ) -> NodeRef {
-  ctx.write_identifier(&ident_name.span, ident_name.sym.as_str(), false, None)
+  ctx.write_identifier(
+    &ident_name.span,
+    ident_name.sym.as_str(),
+    false,
+    None,
+    None,
+  )
 }
 
 fn serialize_prop_name(
@@ -2072,10 +2138,10 @@ fn serialize_class_member(
 
             let paramter = match &prop.param {
               TsParamPropParam::Ident(binding_ident) => {
-                serialize_binding_ident(ctx, binding_ident)
+                serialize_binding_ident(ctx, binding_ident, None)
               }
               TsParamPropParam::Assign(assign_pat) => {
-                serialize_pat(ctx, &Pat::Assign(assign_pat.clone()))
+                serialize_pat(ctx, &Pat::Assign(assign_pat.clone()), None)
               }
             };
 
@@ -2088,7 +2154,9 @@ fn serialize_class_member(
               paramter,
             )
           }
-          ParamOrTsParamProp::Param(param) => serialize_pat(ctx, &param.pat),
+          ParamOrTsParamProp::Param(param) => {
+            serialize_pat(ctx, &param.pat, None)
+          }
         })
         .collect::<Vec<_>>();
 
@@ -2112,6 +2180,7 @@ fn serialize_class_member(
         a11y,
         key,
         value,
+        vec![],
       ))
     }
     ClassMember::Method(node) => {
@@ -2288,7 +2357,15 @@ fn serialize_class_method(
   let params = function
     .params
     .iter()
-    .map(|param| serialize_pat(ctx, &param.pat))
+    .map(|param| {
+      let decorators = param
+        .decorators
+        .iter()
+        .map(|deco| serialize_decorator(ctx, deco))
+        .collect::<Vec<_>>();
+
+      serialize_pat(ctx, &param.pat, Some(decorators))
+    })
     .collect::<Vec<_>>();
 
   let return_type = maybe_serialize_ts_type_ann(ctx, &function.return_type);
@@ -2336,6 +2413,12 @@ fn serialize_class_method(
       value,
     )
   } else {
+    let decorators = function
+      .decorators
+      .iter()
+      .map(|deco| serialize_decorator(ctx, deco))
+      .collect::<Vec<_>>();
+
     ctx.write_class_method(
       span,
       false,
@@ -2347,6 +2430,7 @@ fn serialize_class_method(
       a11y,
       key,
       value,
+      decorators,
     )
   }
 }
@@ -2372,9 +2456,16 @@ fn serialize_decorator(ctx: &mut TsEsTreeBuilder, node: &Decorator) -> NodeRef {
 fn serialize_binding_ident(
   ctx: &mut TsEsTreeBuilder,
   node: &BindingIdent,
+  decorators: Option<Vec<NodeRef>>,
 ) -> NodeRef {
   let type_ann = maybe_serialize_ts_type_ann(ctx, &node.type_ann);
-  ctx.write_identifier(&node.span, &node.sym, node.optional, type_ann)
+  ctx.write_identifier(
+    &node.span,
+    &node.sym,
+    node.optional,
+    type_ann,
+    decorators,
+  )
 }
 
 fn serialize_ts_param_inst(
@@ -2503,7 +2594,7 @@ fn serialize_ts_type(ctx: &mut TsEsTreeBuilder, node: &TsType) -> NodeRef {
               Pat::Object(object_pat) => object_pat.optional,
               _ => false,
             };
-            let label = serialize_pat(ctx, label);
+            let label = serialize_pat(ctx, label, None);
             let type_id = serialize_ts_type(ctx, elem.ty.as_ref());
 
             ctx
@@ -2772,9 +2863,11 @@ fn serialize_ts_fn_param(
   node: &TsFnParam,
 ) -> NodeRef {
   match node {
-    TsFnParam::Ident(ident) => serialize_binding_ident(ctx, ident),
-    TsFnParam::Array(pat) => serialize_pat(ctx, &Pat::Array(pat.clone())),
-    TsFnParam::Rest(pat) => serialize_pat(ctx, &Pat::Rest(pat.clone())),
-    TsFnParam::Object(pat) => serialize_pat(ctx, &Pat::Object(pat.clone())),
+    TsFnParam::Ident(ident) => serialize_binding_ident(ctx, ident, None),
+    TsFnParam::Array(pat) => serialize_pat(ctx, &Pat::Array(pat.clone()), None),
+    TsFnParam::Rest(pat) => serialize_pat(ctx, &Pat::Rest(pat.clone()), None),
+    TsFnParam::Object(pat) => {
+      serialize_pat(ctx, &Pat::Object(pat.clone()), None)
+    }
   }
 }

filename: cli/tools/lint/ast_buffer/ts_estree.rs
patch: @@ -821,6 +821,7 @@ impl TsEsTreeBuilder {
     super_class: Option<NodeRef>,
     implements: Vec<NodeRef>,
     body: NodeRef,
+    decorators: Vec<NodeRef>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::ClassDeclaration, span);
     self.ctx.write_bool(AstProp::Declare, is_declare);
@@ -831,6 +832,7 @@ impl TsEsTreeBuilder {
       .write_maybe_ref(AstProp::SuperClass, &id, super_class);
     self.ctx.write_ref_vec(AstProp::Implements, &id, implements);
     self.ctx.write_ref(AstProp::Body, &id, body);
+    self.ctx.write_ref_vec(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }
@@ -966,6 +968,7 @@ impl TsEsTreeBuilder {
     accessibility: Option<String>,
     key: NodeRef,
     value: NodeRef,
+    decorators: Vec<NodeRef>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::MethodDefinition, span);
 
@@ -984,7 +987,7 @@ impl TsEsTreeBuilder {
     self.write_accessibility(accessibility);
     self.ctx.write_ref(AstProp::Key, &id, key);
     self.ctx.write_ref(AstProp::Value, &id, value);
-    self.ctx.write_ref_vec(AstProp::Decorators, &id, vec![]);
+    self.ctx.write_ref_vec(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }
@@ -1582,6 +1585,7 @@ impl TsEsTreeBuilder {
     name: &str,
     optional: bool,
     type_annotation: Option<NodeRef>,
+    decorators: Option<Vec<NodeRef>>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::Identifier, span);
 
@@ -1592,6 +1596,9 @@ impl TsEsTreeBuilder {
       &id,
       type_annotation,
     );
+    self
+      .ctx
+      .write_maybe_ref_vec_skip(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }
@@ -1611,11 +1618,15 @@ impl TsEsTreeBuilder {
     span: &Span,
     left: NodeRef,
     right: NodeRef,
+    decorators: Option<Vec<NodeRef>>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::AssignmentPattern, span);
 
     self.ctx.write_ref(AstProp::Left, &id, left);
     self.ctx.write_ref(AstProp::Right, &id, right);
+    self
+      .ctx
+      .write_ref_vec_or_empty(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }
@@ -1626,6 +1637,7 @@ impl TsEsTreeBuilder {
     optional: bool,
     type_ann: Option<NodeRef>,
     elems: Vec<NodeRef>,
+    decorators: Option<Vec<NodeRef>>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::ArrayPattern, span);
 
@@ -1634,6 +1646,9 @@ impl TsEsTreeBuilder {
       .ctx
       .write_maybe_undef_ref(AstProp::TypeAnnotation, &id, type_ann);
     self.ctx.write_ref_vec(AstProp::Elements, &id, elems);
+    self
+      .ctx
+      .write_ref_vec_or_empty(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }
@@ -1644,6 +1659,7 @@ impl TsEsTreeBuilder {
     optional: bool,
     type_ann: Option<NodeRef>,
     props: Vec<NodeRef>,
+    decorators: Option<Vec<NodeRef>>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::ObjectPattern, span);
 
@@ -1652,6 +1668,9 @@ impl TsEsTreeBuilder {
       .ctx
       .write_maybe_undef_ref(AstProp::TypeAnnotation, &id, type_ann);
     self.ctx.write_ref_vec(AstProp::Properties, &id, props);
+    self
+      .ctx
+      .write_ref_vec_or_empty(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }
@@ -1661,13 +1680,17 @@ impl TsEsTreeBuilder {
     span: &Span,
     type_ann: Option<NodeRef>,
     arg: NodeRef,
+    decorators: Option<Vec<NodeRef>>,
   ) -> NodeRef {
     let id = self.ctx.append_node(AstNode::RestElement, span);
 
     self
       .ctx
       .write_maybe_undef_ref(AstProp::TypeAnnotation, &id, type_ann);
     self.ctx.write_ref(AstProp::Argument, &id, arg);
+    self
+      .ctx
+      .write_ref_vec_or_empty(AstProp::Decorators, &id, decorators);
 
     self.ctx.commit_node(id)
   }

filename: tests/unit/__snapshots__/lint_plugin_test.ts.snap
patch: @@ -91,6 +91,7 @@ snapshot[`Plugin - FunctionDeclaration 2`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         16,
         20,
@@ -134,6 +135,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
   },
   params: [
     {
+      decorators: [],
       left: {
         name: "a",
         optional: false,
@@ -160,6 +162,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
       type: "AssignmentPattern",
     },
     {
+      decorators: [],
       optional: false,
       properties: [
         {
@@ -183,6 +186,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
           shorthand: false,
           type: "Property",
           value: {
+            decorators: [],
             left: {
               name: "a",
               optional: false,
@@ -251,6 +255,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
             type: "Identifier",
             typeAnnotation: undefined,
           },
+          decorators: [],
           range: [
             32,
             36,
@@ -267,6 +272,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
       typeAnnotation: undefined,
     },
     {
+      decorators: [],
       elements: [
         {
           name: "d",
@@ -289,6 +295,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
             type: "Identifier",
             typeAnnotation: undefined,
           },
+          decorators: [],
           range: [
             43,
             47,
@@ -316,6 +323,7 @@ snapshot[`Plugin - FunctionDeclaration 3`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         50,
         54,
@@ -503,6 +511,7 @@ snapshot[`Plugin - FunctionDeclaration 7`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         23,
         34,
@@ -1103,6 +1112,7 @@ snapshot[`Plugin - ExportDefaultDeclaration 3`] = `
       type: "ClassBody",
     },
     declare: false,
+    decorators: [],
     id: {
       name: "Foo",
       optional: false,
@@ -1143,6 +1153,7 @@ snapshot[`Plugin - ExportDefaultDeclaration 4`] = `
       type: "ClassBody",
     },
     declare: false,
+    decorators: [],
     id: null,
     implements: [],
     range: [
@@ -2267,6 +2278,7 @@ snapshot[`Plugin - ArrowFunctionExpression 3`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         12,
         23,
@@ -4303,6 +4315,7 @@ snapshot[`Plugin - FunctionExpression 3`] = `
   id: null,
   params: [
     {
+      decorators: [],
       name: "a",
       optional: true,
       range: [
@@ -4336,6 +4349,7 @@ snapshot[`Plugin - FunctionExpression 3`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         26,
         37,
@@ -5451,6 +5465,7 @@ snapshot[`Plugin - YieldExpression 1`] = `
 
 snapshot[`Plugin - ObjectPattern 1`] = `
 {
+  decorators: [],
   optional: false,
   properties: [
     {
@@ -5496,6 +5511,7 @@ snapshot[`Plugin - ObjectPattern 1`] = `
 
 snapshot[`Plugin - ObjectPattern 2`] = `
 {
+  decorators: [],
   optional: false,
   properties: [
     {
@@ -5541,6 +5557,7 @@ snapshot[`Plugin - ObjectPattern 2`] = `
 
 snapshot[`Plugin - ObjectPattern 3`] = `
 {
+  decorators: [],
   optional: false,
   properties: [
     {
@@ -5585,6 +5602,7 @@ snapshot[`Plugin - ObjectPattern 3`] = `
 
 snapshot[`Plugin - ObjectPattern 4`] = `
 {
+  decorators: [],
   optional: false,
   properties: [
     {
@@ -5608,6 +5626,7 @@ snapshot[`Plugin - ObjectPattern 4`] = `
       shorthand: false,
       type: "Property",
       value: {
+        decorators: [],
         left: {
           name: "prop",
           optional: false,
@@ -5646,6 +5665,7 @@ snapshot[`Plugin - ObjectPattern 4`] = `
 
 snapshot[`Plugin - ObjectPattern 5`] = `
 {
+  decorators: [],
   optional: false,
   properties: [
     {
@@ -5669,6 +5689,7 @@ snapshot[`Plugin - ObjectPattern 5`] = `
       shorthand: false,
       type: "Property",
       value: {
+        decorators: [],
         left: {
           name: "prop",
           optional: false,
@@ -5706,6 +5727,7 @@ snapshot[`Plugin - ObjectPattern 5`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         18,
         22,
@@ -5725,6 +5747,7 @@ snapshot[`Plugin - ObjectPattern 5`] = `
 
 snapshot[`Plugin - ObjectPattern 6`] = `
 {
+  decorators: [],
   optional: false,
   properties: [
     {
@@ -5748,6 +5771,7 @@ snapshot[`Plugin - ObjectPattern 6`] = `
       shorthand: false,
       type: "Property",
       value: {
+        decorators: [],
         left: {
           name: "a",
           optional: false,
@@ -5787,6 +5811,7 @@ snapshot[`Plugin - ObjectPattern 6`] = `
 
 snapshot[`Plugin - ArrayPattern 1`] = `
 {
+  decorators: [],
   elements: [
     {
       name: "a",
@@ -5821,8 +5846,10 @@ snapshot[`Plugin - ArrayPattern 1`] = `
 
 snapshot[`Plugin - ArrayPattern 2`] = `
 {
+  decorators: [],
   elements: [
     {
+      decorators: [],
       left: {
         name: "a",
         optional: false,
@@ -5861,6 +5888,7 @@ snapshot[`Plugin - ArrayPattern 2`] = `
 
 snapshot[`Plugin - ArrayPattern 3`] = `
 {
+  decorators: [],
   elements: [
     {
       name: "a",
@@ -5883,6 +5911,7 @@ snapshot[`Plugin - ArrayPattern 3`] = `
         type: "Identifier",
         typeAnnotation: undefined,
       },
+      decorators: [],
       range: [
         10,
         14,
@@ -5992,104 +6021,703 @@ snapshot[`Plugin - Literal 8`] = `
     0,
     6,
   ],
-  raw: "/foo/g",
-  regex: {
-    flags: "g",
-    pattern: "foo",
+  raw: "/foo/g",
+  regex: {
+    flags: "g",
+    pattern: "foo",
+  },
+  type: "Literal",
+  value: /foo/g,
+}
+`;
+
+snapshot[`Plugin - Abstract class 1`] = `
+{
+  abstract: true,
+  body: {
+    body: [
+      {
+        accessibility: undefined,
+        computed: false,
+        declare: false,
+        decorators: [],
+        definite: false,
+        key: {
+          name: "prop",
+          optional: false,
+          range: [
+            36,
+            40,
+          ],
+          type: "Identifier",
+          typeAnnotation: undefined,
+        },
+        optional: false,
+        override: false,
+        range: [
+          27,
+          49,
+        ],
+        readonly: false,
+        static: false,
+        type: "TSAbstractPropertyDefinition",
+        typeAnnotation: {
+          range: [
+            40,
+            48,
+          ],
+          type: "TSTypeAnnotation",
+          typeAnnotation: {
+            range: [
+              42,
+              48,
+            ],
+            type: "TSStringKeyword",
+          },
+        },
+        value: null,
+      },
+    ],
+    range: [
+      0,
+      51,
+    ],
+    type: "ClassBody",
+  },
+  declare: false,
+  decorators: [],
+  id: {
+    name: "SomeClass",
+    optional: false,
+    range: [
+      15,
+      24,
+    ],
+    type: "Identifier",
+    typeAnnotation: undefined,
+  },
+  implements: [],
+  range: [
+    0,
+    51,
+  ],
+  superClass: null,
+  type: "ClassDeclaration",
+}
+`;
+
+snapshot[`Plugin - Abstract class 2`] = `
+{
+  abstract: true,
+  body: {
+    body: [
+      {
+        accessibility: undefined,
+        computed: false,
+        key: {
+          name: "method",
+          optional: false,
+          range: [
+            36,
+            42,
+          ],
+          type: "Identifier",
+          typeAnnotation: undefined,
+        },
+        kind: "method",
+        optional: false,
+        override: false,
+        range: [
+          27,
+          53,
+        ],
+        static: false,
+        type: "TSAbstractMethodDefinition",
+        value: {
+          async: false,
+          body: null,
+          declare: false,
+          expression: false,
+          generator: false,
+          id: null,
+          params: [],
+          range: [
+            27,
+            53,
+          ],
+          returnType: {
+            range: [
+              44,
+              52,
+            ],
+            type: "TSTypeAnnotation",
+            typeAnnotation: {
+              range: [
+                46,
+                52,
+              ],
+              type: "TSStringKeyword",
+            },
+          },
+          type: "TSEmptyBodyFunctionExpression",
+          typeParameters: undefined,
+        },
+      },
+    ],
+    range: [
+      0,
+      55,
+    ],
+    type: "ClassBody",
+  },
+  declare: false,
+  decorators: [],
+  id: {
+    name: "SomeClass",
+    optional: false,
+    range: [
+      15,
+      24,
+    ],
+    type: "Identifier",
+    typeAnnotation: undefined,
+  },
+  implements: [],
+  range: [
+    0,
+    55,
+  ],
+  superClass: null,
+  type: "ClassDeclaration",
+}
+`;
+
+snapshot[`Plugin - Decorators 1`] = `
+{
+  abstract: false,
+  body: {
+    body: [],
+    range: [
+      0,
+      18,
+    ],
+    type: "ClassBody",
+  },
+  declare: false,
+  decorators: [
+    {
+      expression: {
+        name: "deco",
+        optional: false,
+        range: [
+          1,
+          5,
+        ],
+        type: "Identifier",
+        typeAnnotation: undefined,
+      },
+      range: [
+        0,
+        5,
+      ],
+      type: "Decorator",
+    },
+  ],
+  id: {
+    name: "Foo",
+    optional: false,
+    range: [
+      12,
+      15,
+    ],
+    type: "Identifier",
+    typeAnnotation: undefined,
+  },
+  implements: [],
+  range: [
+    0,
+    18,
+  ],
+  superClass: null,
+  type: "ClassDeclaration",
+}
+`;
+
+snapshot[`Plugin - Decorators 2`] = `
+{
+  abstract: false,
+  body: {
+    body: [
+      {
+        accessibility: undefined,
+        computed: false,
+        declare: false,
+        decorators: [
+          {
+            expression: {
+              name: "deco",
+              optional: false,
+              range: [
+                23,
+                27,
+              ],
+              type: "Identifier",
+              typeAnnotation: undefined,
+            },
+            range: [
+              22,
+              27,
+            ],
+            type: "Decorator",
+          },
+        ],
+        key: {
+          name: "foo",
+          optional: false,
+          range: [
+            28,
+            31,
+          ],
+          type: "Identifier",
+          typeAnnotation: undefined,
+        },
+        kind: "method",
+        optional: false,
+        override: false,
+        range: [
+          22,
+          36,
+        ],
+        static: false,
+        type: "MethodDefinition",
+        value: {
+          async: false,
+          body: {
+            body: [],
+            range: [
+              34,
+              36,
+            ],
+            type: "BlockStatement",
+          },
+          generator: false,
+          id: null,
+          params: [],
+          range: [
+            22,
+            36,
+          ],
+          returnType: undefined,
+          type: "FunctionExpression",
+          typeParameters: undefined,
+        },
+      },
+    ],
+    range: [
+      10,
+      38,
+    ],
+    type: "ClassBody",
+  },
+  declare: false,
+  id: {
+    name: "Foo",
+    optional: false,
+    range: [
+      16,
+      19,
+    ],
+    type: "Identifier",
+    typeAnnotation: undefined,
+  },
+  implements: [],
+  range: [
+    10,
+    38,
+  ],
+  superClass: null,
+  superTypeArguments: undefined,
+  type: "ClassExpression",
+  typeParameters: undefined,
+}
+`;
+
+snapshot[`Plugin - Decorators 3`] = `
+{
+  accessibility: undefined,
+  computed: false,
+  declare: false,
+  decorators: [
+    {
+      expression: {
+        name: "deco",
+        optional: false,
+        range: [
+          13,
+          17,
+        ],
+        type: "Identifier",
+        typeAnnotation: undefined,
+      },
+      range: [
+        12,
+        17,
+      ],
+      type: "Decorator",
+    },
+  ],
+  key: {
+    name: "foobar",
+    optional: false,
+    range: [
+      18,
+      24,
+    ],
+    type: "Identifier",
+    typeAnnotation: undefined,
+  },
+  kind: "method",
+  optional: false,
+  override: false,
+  range: [
+    12,
+    29,
+  ],
+  static: false,
+  type: "MethodDefinition",
+  value: {
+    async: false,
+    body: {
+      body: [],
+      range: [
+        27,
+        29,
+      ],
+      type: "BlockStatement",
+    },
+    generator: false,
+    id: null,
+    params: [],
+    range: [
+      12,
+      29,
+    ],
+    returnType: undefined,
+    type: "FunctionExpression",
+    typeParameters: undefined,
+  },
+}
+`;
+
+snapshot[`Plugin - Decorators 4`] = `
+{
+  accessibility: undefined,
+  computed: false,
+  declare: false,
+  decorators: [
+    {
+      expression: {
+        name: "deco",
+        optional: false,
+        range: [
+          13,
+          17,
+        ],
+        type: "Identifier",
+        typeAnnotation: undefined,
+      },
+      range: [
+        12,
+        17,
+      ],
+      type: "Decorator",
+    },
+  ],
+  key: {
+    name: "foo",
+    optional: false,
+    range: [
+      22,
+      25,
+    ],
+    type: "Identifier",
+    typeAnnotation: undefined,
+  },
+  kind: "get",
+  optional: false,
+  override: false,
+  range: [
+    12,
+    40,
+  ],
+  static: false,
+  type: "MethodDefinition",
+  value: {
+    async: false,
+    body: {
+      body: [
+        {
+          argument: {
+            range: [
+              37,
+              38,
+            ],
+            raw: "2",
+            type: "Literal",
+            value: 2,
+          },
+          range: [
+            30,
+            38,
+          ],
+          type: "ReturnStatement",
+        },
+      ],
+      range: [
+        28,
+        40,
+      ],
+      type: "BlockStatement",
+    },
+    generator: false,
+    id: null,
+    params: [],
+    range: [
+      12,
+      40,
+    ],
+    returnType: undefined,
+    type: "FunctionExpression",
+    typeParameters: undefined,
   },
-  type: "Literal",
-  value: /foo/g,
 }
 `;
 
-snapshot[`Plugin - Abstract class 1`] = `
+snapshot[`Plugin - Decorators 5`] = `
 {
-  abstract: true,
+  abstract: false,
   body: {
     body: [
       {
         accessibility: undefined,
         computed: false,
         declare: false,
-        decorators: [],
-        definite: false,
+        decorators: [
+          {
+            expression: {
+              arguments: [
+                {
+                  range: [
+                    18,
+                    23,
+                  ],
+                  raw: '"arg"',
+                  type: "Literal",
+                  value: "arg",
+                },
+              ],
+              callee: {
+                name: "deco",
+                optional: false,
+                range: [
+                  13,
+                  17,
+                ],
+                type: "Identifier",
+                typeAnnotation: undefined,
+              },
+              optional: false,
+              range: [
+                13,
+                24,
+              ],
+              type: "CallExpression",
+              typeArguments: null,
+            },
+            range: [
+              12,
+              24,
+            ],
+            type: "Decorator",
+          },
+        ],
         key: {
-          name: "prop",
+          name: "foo",
           optional: false,
           range: [
-            36,
-            40,
+            25,
+            28,
           ],
           type: "Identifier",
           typeAnnotation: undefined,
         },
         optional: false,
         override: false,
         range: [
-          27,
-          49,
+          12,
+          37,
         ],
         readonly: false,
         static: false,
-        type: "TSAbstractPropertyDefinition",
+        type: "PropertyDefinition",
         typeAnnotation: {
           range: [
-            40,
-            48,
+            28,
+            36,
           ],
           type: "TSTypeAnnotation",
           typeAnnotation: {
             range: [
-              42,
-              48,
+              30,
+              36,
             ],
             type: "TSStringKeyword",
           },
         },
         value: null,
       },
+      {
+        accessibility: undefined,
+        computed: false,
+        declare: false,
+        decorators: [],
+        key: {
+          name: "constructor",
+          optional: false,
+          range: [
+            38,
+            49,
+          ],
+          type: "Identifier",
+          typeAnnotation: undefined,
+        },
+        kind: "constructor",
+        optional: false,
+        override: false,
+        range: [
+          38,
+          72,
+        ],
+        static: false,
+        type: "MethodDefinition",
+        value: {
+          async: false,
+          body: {
+            body: [
+              {
+                expression: {
+                  left: {
+                    computed: false,
+                    object: {
+                      range: [
+                        54,
+                        58,
+                      ],
+                      type: "ThisExpression",
+                    },
+                    optional: false,
+                    property: {
+                      name: "foo",
+                      optional: false,
+                      range: [
+                        59,
+                        62,
+                      ],
+                      type: "Identifier",
+                      typeAnnotation: undefined,
+                    },
+                    range: [
+                      54,
+                      62,
+                    ],
+                    type: "MemberExpression",
+                  },
+                  operator: "=",
+                  range: [
+                    54,
+                    70,
+                  ],
+                  right: {
+                    range: [
+                      65,
+                      70,
+                    ],
+                    raw: '"foo"',
+                    type: "Literal",
+                    value: "foo",
+                  },
+                  type: "AssignmentExpression",
+                },
+                range: [
+                  54,
+                  70,
+                ],
+                type: "ExpressionStatement",
+              },
+            ],
+            range: [
+              52,
+              72,
+            ],
+            type: "BlockStatement",
+          },
+          generator: false,
+          id: null,
+          params: [],
+          range: [
+            38,
+            72,
+          ],
+          returnType: undefined,
+          type: "FunctionExpression",
+          typeParameters: undefined,
+        },
+      },
     ],
     range: [
       0,
-      51,
+      74,
     ],
     type: "ClassBody",
   },
   declare: false,
+  decorators: [],
   id: {
-    name: "SomeClass",
+    name: "Foo",
     optional: false,
     range: [
-      15,
-      24,
+      6,
+      9,
     ],
     type: "Identifier",
     typeAnnotation: undefined,
   },
   implements: [],
   range: [
     0,
-    51,
+    74,
   ],
   superClass: null,
   type: "ClassDeclaration",
 }
 `;
 
-snapshot[`Plugin - Abstract class 2`] = `
+snapshot[`Plugin - Decorators 6`] = `
 {
-  abstract: true,
+  abstract: false,
   body: {
     body: [
       {
         accessibility: undefined,
         computed: false,
+        declare: false,
+        decorators: [],
         key: {
-          name: "method",
+          name: "foo",
           optional: false,
           range: [
-            36,
-            42,
+            12,
+            15,
           ],
           type: "Identifier",
           typeAnnotation: undefined,
@@ -6098,63 +6726,99 @@ snapshot[`Plugin - Abstract class 2`] = `
         optional: false,
         override: false,
         range: [
-          27,
-          53,
+          12,
+          37,
         ],
         static: false,
-        type: "TSAbstractMethodDefinition",
+        type: "MethodDefinition",
         value: {
           async: false,
-          body: null,
-          declare: false,
-          expression: false,
-          generator: false,
-          id: null,
-          params: [],
-          range: [
-            27,
-            53,
-          ],
-          returnType: {
+          body: {
+            body: [],
             range: [
-              44,
-              52,
+              35,
+              37,
             ],
-            type: "TSTypeAnnotation",
-            typeAnnotation: {
+            type: "BlockStatement",
+          },
+          generator: false,
+          id: null,
+          params: [
+            {
+              decorators: [
+                {
+                  expression: {
+                    name: "deco",
+                    optional: false,
+                    range: [
+                      17,
+                      21,
+                    ],
+                    type: "Identifier",
+                    typeAnnotation: undefined,
+                  },
+                  range: [
+                    16,
+                    21,
+                  ],
+                  type: "Decorator",
+                },
+              ],
+              name: "foo",
+              optional: false,
               range: [
-                46,
-                52,
+                22,
+                25,
               ],
-              type: "TSStringKeyword",
+              type: "Identifier",
+              typeAnnotation: {
+                range: [
+                  25,
+                  33,
+                ],
+                type: "TSTypeAnnotation",
+                typeAnnotation: {
+                  range: [
+                    27,
+                    33,
+                  ],
+                  type: "TSStringKeyword",
+                },
+              },
             },
-          },
-          type: "TSEmptyBodyFunctionExpression",
+          ],
+          range: [
+            12,
+            37,
+          ],
+          returnType: undefined,
+          type: "FunctionExpression",
           typeParameters: undefined,
         },
       },
     ],
     range: [
       0,
-      55,
+      39,
     ],
     type: "ClassBody",
   },
   declare: false,
+  decorators: [],
   id: {
-    name: "SomeClass",
+    name: "Foo",
     optional: false,
     range: [
-      15,
-      24,
+      6,
+      9,
     ],
     type: "Identifier",
     typeAnnotation: undefined,
   },
   implements: [],
   range: [
     0,
-    55,
+    39,
   ],
   superClass: null,
   type: "ClassDeclaration",
@@ -8220,6 +8884,7 @@ snapshot[`Plugin - TSInterfaceDeclaration 12`] = `
               type: "Identifier",
               typeAnnotation: undefined,
             },
+            decorators: [],
             range: [
               30,
               44,
@@ -10244,6 +10909,7 @@ snapshot[`Plugin - TSTupleType + TSArrayType 5`] = `
           type: "Identifier",
           typeAnnotation: undefined,
         },
+        decorators: [],
         range: [
           10,
           15,

filename: tests/unit/lint_plugin_test.ts
patch: @@ -921,6 +921,44 @@ Deno.test("Plugin - Abstract class", async (t) => {
   );
 });
 
+Deno.test("Plugin - Decorators", async (t) => {
+  // Class declaration
+  await testSnapshot(
+    t,
+    `@deco class Foo {}`,
+    "ClassDeclaration",
+  );
+
+  // Class expression
+  await testSnapshot(
+    t,
+    `let foo = class Foo { @deco foo() {} }`,
+    "ClassExpression",
+  );
+
+  // Other
+  await testSnapshot(
+    t,
+    `class Foo { @deco foobar() {} }`,
+    "MethodDefinition",
+  );
+  await testSnapshot(
+    t,
+    `class Foo { @deco get foo() { return 2 } }`,
+    "MethodDefinition",
+  );
+  await testSnapshot(
+    t,
+    `class Foo { @deco("arg") foo: string; constructor() { this.foo = "foo" } }`,
+    "ClassDeclaration",
+  );
+  await testSnapshot(
+    t,
+    `class Foo { foo(@deco foo: string) {} }`,
+    "ClassDeclaration",
+  );
+});
+
 Deno.test("Plugin - JSXElement + JSXOpeningElement + JSXClosingElement + JSXAttr", async (t) => {
   await testSnapshot(t, "<div />", "JSXElement");
   await testSnapshot(t, "<div></div>", "JSXElement");