/-
Copyright (c) 2022 Mario Carneiro. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mario Carneiro
-/
prelude
import Lean.Meta.Tactic.Simp.RegisterCommand
import Lean.Elab.Command
import Lean.Elab.SetOption
import Lean.Linter.Util

namespace Lean.Linter
open Elab.Command Parser.Command
open Parser.Term hiding «set_option»

register_builtin_option linter.missingDocs : Bool := {
  defValue := false
  descr := "enable the 'missing documentation' linter"
}

def getLinterMissingDocs (o : Options) : Bool := getLinterValue linter.missingDocs o


namespace MissingDocs

abbrev SimpleHandler := Syntax → CommandElabM Unit
abbrev Handler := Bool → SimpleHandler

def SimpleHandler.toHandler (h : SimpleHandler) : Handler :=
  fun enabled stx => if enabled then h stx else pure ()

unsafe def mkHandlerUnsafe (constName : Name) : ImportM Handler := do
  let env  := (← read).env
  let opts := (← read).opts
  match env.find? constName with
  | none      => throw ↑s!"unknown constant '{constName}'"
  | some info => match info.type with
    | Expr.const ``SimpleHandler _ => do
      let h ← IO.ofExcept $ env.evalConst SimpleHandler opts constName
      pure h.toHandler
    | Expr.const ``Handler _ =>
      IO.ofExcept $ env.evalConst Handler opts constName
    | _ => throw ↑s!"unexpected missing docs handler at '{constName}', `MissingDocs.Handler` or `MissingDocs.SimpleHandler` expected"

@[implemented_by mkHandlerUnsafe]
opaque mkHandler (constName : Name) : ImportM Handler

builtin_initialize builtinHandlersRef : IO.Ref (NameMap Handler) ← IO.mkRef {}

builtin_initialize missingDocsExt :
  PersistentEnvExtension (Name × Name) (Name × Name × Handler) (List (Name × Name) × NameMap Handler) ←
  registerPersistentEnvExtension {
    mkInitial       := return ([], ← builtinHandlersRef.get)
    addImportedFn   := fun as => do
      ([], ·) <$> as.foldlM (init := ← builtinHandlersRef.get) fun s as =>
        as.foldlM (init := s) fun s (n, k) => s.insert k <$> mkHandler n
    addEntryFn      := fun (entries, s) (n, k, h) => ((n, k)::entries, s.insert k h)
    exportEntriesFn := fun s => s.1.reverse.toArray
    statsFn := fun s => format "number of local entries: " ++ format s.1.length
  }

def addHandler (env : Environment) (declName key : Name) (h : Handler) : Environment :=
  missingDocsExt.addEntry env (declName, key, h)

def getHandlers (env : Environment) : NameMap Handler := (missingDocsExt.getState env).2

partial def missingDocs : Linter where
  run stx := do
    if let some h := (getHandlers (← getEnv)).find? stx.getKind then
      h (getLinterMissingDocs (← getOptions)) stx

builtin_initialize addLinter missingDocs

def addBuiltinHandler (key : Name) (h : Handler) : IO Unit :=
  builtinHandlersRef.modify (·.insert key h)

builtin_initialize
  let mkAttr (builtin : Bool) (name : Name) := registerBuiltinAttribute {
    name
    descr           := (if builtin then "(builtin) " else "") ++
      "adds a syntax traversal for the missing docs linter"
    applicationTime := .afterCompilation
    add             := fun declName stx kind => do
      unless kind == AttributeKind.global do throwError "invalid attribute '{name}', must be global"
      let env ← getEnv
      unless builtin || (env.getModuleIdxFor? declName).isNone do
        throwError "invalid attribute '{name}', declaration is in an imported module"
      let decl ← getConstInfo declName
      let fnNameStx ← Attribute.Builtin.getIdent stx
      let key ← Elab.realizeGlobalConstNoOverloadWithInfo fnNameStx
      unless decl.levelParams.isEmpty && (decl.type == .const ``Handler [] || decl.type == .const ``SimpleHandler []) do
        throwError "unexpected missing docs handler at '{declName}', `MissingDocs.Handler` or `MissingDocs.SimpleHandler` expected"
      if builtin then
        let h := if decl.type == .const ``SimpleHandler [] then
          mkApp (mkConst ``SimpleHandler.toHandler) (mkConst declName)
        else mkConst declName
        declareBuiltin declName <| mkApp2 (mkConst ``addBuiltinHandler) (toExpr key) h
      else
        setEnv <| missingDocsExt.addEntry env (declName, key, ← mkHandler declName)
  }
  mkAttr true `builtin_missing_docs_handler
  mkAttr false `missing_docs_handler

def lint (stx : Syntax) (msg : String) : CommandElabM Unit :=
  logLint linter.missingDocs stx m!"missing doc string for {msg}"

def lintNamed (stx : Syntax) (msg : String) : CommandElabM Unit :=
  lint stx s!"{msg} {stx.getId}"

def lintField (parent stx : Syntax) (msg : String) : CommandElabM Unit :=
  lint stx s!"{msg} {parent.getId}.{stx.getId}"

def lintStructField (parent stx : Syntax) (msg : String) : CommandElabM Unit :=
  lint stx s!"{msg} {parent.getId}.{stx.getId}"

def hasInheritDoc (attrs : Syntax) : Bool :=
  attrs[0][1].getSepArgs.any fun attr =>
    attr[1].isOfKind ``Parser.Attr.simple &&
    attr[1][0].getId.eraseMacroScopes == `inherit_doc

def declModifiersPubNoDoc (mods : Syntax) : Bool :=
  mods[2][0].getKind != ``«private» && mods[0].isNone && !hasInheritDoc mods[1]

def lintDeclHead (k : SyntaxNodeKind) (id : Syntax) : CommandElabM Unit := do
  if k == ``«abbrev» then lintNamed id "public abbrev"
  else if k == ``definition then lintNamed id "public def"
  else if k == ``«opaque» then lintNamed id "public opaque"
  else if k == ``«axiom» then lintNamed id "public axiom"
  else if k == ``«inductive» then lintNamed id "public inductive"
  else if k == ``classInductive then lintNamed id "public inductive"
  else if k == ``«structure» then lintNamed id "public structure"

@[builtin_missing_docs_handler declaration]
def checkDecl : SimpleHandler := fun stx => do
  let head := stx[0]; let rest := stx[1]
  if head[2][0].getKind == ``«private» then return -- not private
  let k := rest.getKind
  if declModifiersPubNoDoc head then -- no doc string
    lintDeclHead k rest[1][0]
  if k == ``«inductive» || k == ``classInductive then
    for stx in rest[4].getArgs do
      let head := stx[2]
      if stx[0].isNone && declModifiersPubNoDoc head then
        lintField rest[1][0] stx[3] "public constructor"
    unless rest[5].isNone do
      for stx in rest[5][0][1].getArgs do
        let head := stx[0]
        if declModifiersPubNoDoc head then -- no doc string
          lintField rest[1][0] stx[1] "computed field"
  else if rest.getKind == ``«structure» then
    unless rest[5][2].isNone do
      let redecls : HashSet String.Pos :=
        (← get).infoState.trees.foldl (init := {}) fun s tree =>
          tree.foldInfo (init := s) fun _ info s =>
            if let .ofFieldRedeclInfo info := info then
              if let some range := info.stx.getRange? then
                s.insert range.start
              else s
            else s
      let parent := rest[1][0]
      let lint1 stx := do
        if let some range := stx.getRange? then
          if redecls.contains range.start then return
        lintField parent stx "public field"
      for stx in rest[5][2][0].getArgs do
        let head := stx[0]
        if declModifiersPubNoDoc head then
          if stx.getKind == ``structSimpleBinder then
            lint1 stx[1]
          else
            for stx in stx[2].getArgs do
              lint1 stx

@[builtin_missing_docs_handler «initialize»]
def checkInit : SimpleHandler := fun stx => do
  if !stx[2].isNone && declModifiersPubNoDoc stx[0] then
    lintNamed stx[2][0] "initializer"

@[builtin_missing_docs_handler «notation»]
def checkNotation : SimpleHandler := fun stx => do
  if stx[0].isNone && stx[2][0][0].getKind != ``«local» && !hasInheritDoc stx[1] then
    if stx[5].isNone then lint stx[3] "notation"
    else lintNamed stx[5][0][3] "notation"

@[builtin_missing_docs_handler «mixfix»]
def checkMixfix : SimpleHandler := fun stx => do
  if stx[0].isNone && stx[2][0][0].getKind != ``«local» && !hasInheritDoc stx[1] then
    if stx[5].isNone then lint stx[3] stx[3][0].getAtomVal
    else lintNamed stx[5][0][3] stx[3][0].getAtomVal

@[builtin_missing_docs_handler «syntax»]
def checkSyntax : SimpleHandler := fun stx => do
  if stx[0].isNone && stx[2][0][0].getKind != ``«local» && !hasInheritDoc stx[1] then
    if stx[5].isNone then lint stx[3] "syntax"
    else lintNamed stx[5][0][3] "syntax"

def mkSimpleHandler (name : String) : SimpleHandler := fun stx => do
  if stx[0].isNone then
    lintNamed stx[2] name

@[builtin_missing_docs_handler syntaxAbbrev]
def checkSyntaxAbbrev : SimpleHandler := mkSimpleHandler "syntax"

@[builtin_missing_docs_handler syntaxCat]
def checkSyntaxCat : SimpleHandler := mkSimpleHandler "syntax category"

@[builtin_missing_docs_handler «macro»]
def checkMacro : SimpleHandler := fun stx => do
  if stx[0].isNone && stx[2][0][0].getKind != ``«local» && !hasInheritDoc stx[1] then
    if stx[5].isNone then lint stx[3] "macro"
    else lintNamed stx[5][0][3] "macro"

@[builtin_missing_docs_handler «elab»]
def checkElab : SimpleHandler := fun stx => do
  if stx[0].isNone && stx[2][0][0].getKind != ``«local» && !hasInheritDoc stx[1] then
    if stx[5].isNone then lint stx[3] "elab"
    else lintNamed stx[5][0][3] "elab"

@[builtin_missing_docs_handler classAbbrev]
def checkClassAbbrev : SimpleHandler := fun stx => do
  if declModifiersPubNoDoc stx[0] then
    lintNamed stx[3] "class abbrev"

@[builtin_missing_docs_handler Parser.Tactic.declareSimpLikeTactic]
def checkSimpLike : SimpleHandler := mkSimpleHandler "simp-like tactic"

@[builtin_missing_docs_handler Option.registerBuiltinOption]
def checkRegisterBuiltinOption : SimpleHandler := mkSimpleHandler "option"

@[builtin_missing_docs_handler Option.registerOption]
def checkRegisterOption : SimpleHandler := mkSimpleHandler "option"

@[builtin_missing_docs_handler registerSimpAttr]
def checkRegisterSimpAttr : SimpleHandler := mkSimpleHandler "simp attr"

@[builtin_missing_docs_handler «in»]
def handleIn : Handler := fun _ stx => do
  if stx[0].getKind == ``«set_option» then
    let opts ← Elab.elabSetOption stx[0][1] stx[0][3]
    withScope (fun scope => { scope with opts }) do
      missingDocs.run stx[2]
  else
    missingDocs.run stx[2]

@[builtin_missing_docs_handler «mutual»]
def handleMutual : Handler := fun _ stx => do
  stx[1].getArgs.forM missingDocs.run
