moonhadolint

    Dockerfile parser and hadolint-style linter for MoonBit

    dockerfile
    hadolint
    lint
    docker
    parser
    cli
    Download zip
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    13 hours ago
    Downloads
    2

    #MoonHadolint

    MoonHadolint 是一个使用 MoonBit 编写的 Dockerfile 解析器与 hadolint 风格检查器。它读取 Dockerfile 文本,生成指令 AST,并按规则输出带行号的诊断。

    本项目检查的是 Dockerfile 写法,不是镜像构建,也不是 OCI 布局:

    • 不调用 Docker daemon,不构建镜像;
    • 和 oyjh0381/moonoci 不同,本项目解析 Dockerfile,不读取 image layout;
    • 和 Lfan-ke/moonctl 不同,本项目检查已有 Dockerfile,不生成脚手架;
    • 和 ignore 包不同,本项目不处理 .dockerignore glob。

    #边界与关系说明(规避重复)

    MoonHadolint 的目标是“Dockerfile 质量检查器”,而不是通用语法基础设施:

    • 不提供通用的语法树增量更新能力(如 Syntax tree / highlighter 套件);
    • 不提供编辑器级 tokenization、高亮、代码动作补全、语法树遍历 API;
    • 不提供语言服务器或构建编译流水线能力;
    • 不重建或依赖外部镜像布局,不替代 mooncakes.io 生态中的通用解析器/高亮器。

    它的输入是 Dockerfile 文本,输出是带规则码(DL/SC)、行号和严重级别的 linter 诊断;默认产物偏向 CI 检测与质量治理场景。mooncakes.io/mizchi/syntree(0.2.4)更偏通用语法树与高亮工具链,二者是互补关系而非替代关系。

    首版已经完成可运行 MVP:库 API、Native CLI、示例文件和自动化测试。

    #功能

    • 解析常见 Dockerfile 指令:FROM、RUN、COPY、ADD、WORKDIR、ENV、EXPOSE、USER、CMD、ENTRYPOINT 等;
    • 支持注释、空行和 \ 续行;
    • 识别 FROM 标签、digest、registry 端口、AS 别名和 COPY --from;
    • 支持 # hadolint ignore=、--ignore 和小型配置文件;
    • 检查未打标签、:latest、相对 WORKDIR、root USER、sudo、包版本未固定、ADD 误用、连续 RUN、未知 stage 等问题;
    • 输出 text / JSON / GitHub annotations 报告;
    • 存在 error 级诊断时,CLI 返回非 0 退出码。

    #快速开始

    作为 MoonBit 库依赖安装:

    moon add spectacleCase/moonhadolint

    使用 Native CLI:

    moon test moon run --target native cmd/main -- sample moon run --target native cmd/main -- lint - moon run --target native cmd/main -- json examples/bad.Dockerfile moon run --target native cmd/main -- annotate examples/bad.Dockerfile moon run --target native cmd/main -- lint --ignore DL3006 examples/bad.Dockerfile moon run --target native cmd/main -- parse examples/good.Dockerfile moon run --target native cmd/main -- lint examples/multistage.Dockerfile

    #命令

    moonhadolint lint [--ignore CODE] [--config FILE] <Dockerfile> moonhadolint json [--ignore CODE] [--config FILE] <Dockerfile> moonhadolint annotate [--ignore CODE] [--config FILE] <Dockerfile> moonhadolint parse <Dockerfile> moonhadolint sample moonhadolint bad-sample moonhadolint rules

    文件路径写成 - 时从标准输入读取。直接传入 Dockerfile 路径时,默认执行 lint。--threshold 可以是 error、warning 或 never。

    #库 API

    ///|
    test "lint clean sample" {
    let result = @moonhadolint.lint_source(@moonhadolint.sample_dockerfile()).unwrap()
    inspect(result.diagnostics.length(), content="0")
    inspect(result.instruction_count, content="6")
    }

    ///|
    test "parse FROM line" {
    let doc = @moonhadolint.parse_dockerfile("FROM alpine:3.19\n").unwrap()
    assert_true(doc.instructions[0].kind is From)
    inspect(doc.instructions[0].arguments, content="alpine:3.19")
    }

    #规则

    当前内置 45 条规则(含 4 条高频 shell 子集,编号对齐 ShellCheck),可用 moonhadolint rules 列出。覆盖镜像标签、多阶段 COPY --from、包管理器、USER/WORKDIR、LABEL、CMD/ENTRYPOINT,以及对 RUN 中未加引号的变量和未保护的 cd。

    ///|
    test "catalog includes FROM tag rule" {
    assert_true(@moonhadolint.has_rule("DL3006"))
    }

    #当前范围

    MoonHadolint 首版有意保持聚焦:

    • 只做本地 Dockerfile 文本的解析和规则检查;
    • 覆盖高频指令和最有用的一批 hadolint 规则;
    • 对 RUN 只做高频 shell 子集(SC2086 / SC2046 / SC2068 / SC2164),不是完整 ShellCheck;
    • 不替代 Docker build、BuildKit 或镜像扫描工具。

    规则编号与 hadolint 对齐,实现使用 MoonBit 重写,许可证为 Apache-2.0。

    #许可证

    Apache-2.0

    CopyNode

    pub(all) struct CopyNode {
    from_stage : String?
    chown : String?
    sources : Array[String]
    dest : String
    } derive(Eq,
    Debug
    )

    CopyNode::equal

    fn CopyNode::equal(CopyNode, CopyNode) -> Bool

    CopyNode::not_equal

    fn CopyNode::not_equal(x : CopyNode, y : CopyNode) -> Bool

    CopyNode::to_repr

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    level : String
    line : Int
    message : String
    stage : String?
    } derive(Eq,
    Debug
    )

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    Dockerfile

    pub(all) struct Dockerfile {
    instructions : Array[Instruction]
    } derive(
    Debug
    )

    ExecNode

    pub(all) struct ExecNode {
    form : InstructionForm
    argv : Array[String]
    raw : String
    } derive(Eq,
    Debug
    )

    ExecNode::equal

    fn ExecNode::equal(ExecNode, ExecNode) -> Bool

    ExecNode::not_equal

    fn ExecNode::not_equal(x : ExecNode, y : ExecNode) -> Bool

    ExecNode::to_repr

    FromNode

    pub(all) struct FromNode {
    image : String
    tag : String?
    digest : Bool
    platform : String?
    as_name : String?
    } derive(Eq,
    Debug
    )

    FromNode::equal

    fn FromNode::equal(FromNode, FromNode) -> Bool

    FromNode::not_equal

    fn FromNode::not_equal(x : FromNode, y : FromNode) -> Bool

    FromNode::to_repr

    Instruction

    pub(all) struct Instruction {
    kind : InstructionKind
    name : String
    arguments : String
    line : Int
    form : InstructionForm
    stage : String?
    copy_from : String?
    ignored_codes : Array[String]
    node : Node
    } derive(
    Debug
    )

    InstructionForm

    pub(all) enum InstructionForm {
    Shell
    Json
    Other
    } derive(Eq,
    Debug
    )

    InstructionForm::equal

    InstructionForm::not_equal

    fn InstructionForm::not_equal(x : InstructionForm, y : InstructionForm) -> Bool

    InstructionKind

    pub(all) enum InstructionKind {
    From
    Run
    Cmd
    Entrypoint
    Copy
    Add
    Workdir
    Env
    Expose
    User
    Arg
    Label
    Volume
    Healthcheck
    Shell
    Maintainer
    Stopsignal
    Onbuild
    Unknown
    } derive(Eq,
    Debug
    )

    InstructionKind::equal

    InstructionKind::not_equal

    fn InstructionKind::not_equal(x : InstructionKind, y : InstructionKind) -> Bool

    LintOptions

    pub(all) struct LintOptions {
    ignored : Array[String]
    trusted_registries : Array[String]
    required_labels : Array[String]
    } derive(
    Debug
    )

    LintResult

    pub(all) struct LintResult {
    diagnostics : Array[Diagnostic]
    instruction_count : Int
    } derive(
    Debug
    )

    LintResult::error_count

    fn LintResult::error_count(self : LintResult) -> Int

    LintResult::has_code

    fn LintResult::has_code(self : LintResult, code : String) -> Bool

    LintResult::has_errors

    fn LintResult::has_errors(self : LintResult) -> Bool

    LintResult::should_fail

    fn LintResult::should_fail(self : LintResult, threshold : String) -> Bool

    LintResult::warning_count

    fn LintResult::warning_count(self : LintResult) -> Int

    Node

    pub(all) enum Node {
    From(FromNode)
    Run(RunNode)
    Copy(CopyNode)
    Add(CopyNode)
    Env(Array[PairNode])
    Arg(PairNode)
    Label(Array[PairNode])
    Expose(Array[String])
    Cmd(ExecNode)
    Entrypoint(ExecNode)
    Workdir(String)
    User(String)
    Other
    } derive(
    Debug
    )

    Node::to_repr

    PairNode

    pub(all) struct PairNode {
    key : String
    value : String
    } derive(Eq,
    Debug
    )

    PairNode::equal

    fn PairNode::equal(PairNode, PairNode) -> Bool

    PairNode::not_equal

    fn PairNode::not_equal(x : PairNode, y : PairNode) -> Bool

    PairNode::to_repr

    ParseError

    pub(all) struct ParseError {
    line : Int
    message : String
    }

    impl Show for ParseError

    ParseError::output

    fn ParseError::output(self : ParseError, logger : &Logger) -> Unit

    ParseError::to_string

    fn ParseError::to_string(self : ParseError) -> String

    RuleInfo

    pub(all) struct RuleInfo {
    code : String
    level : String
    summary : String
    } derive(Eq,
    Debug
    )

    RuleInfo::equal

    fn RuleInfo::equal(RuleInfo, RuleInfo) -> Bool

    RuleInfo::not_equal

    fn RuleInfo::not_equal(x : RuleInfo, y : RuleInfo) -> Bool

    RuleInfo::to_repr

    RunNode

    pub(all) struct RunNode {
    form : InstructionForm
    script : String
    argv : Array[String]
    has_pipe : Bool
    has_heredoc : Bool
    } derive(Eq,
    Debug
    )

    RunNode::equal

    fn RunNode::equal(RunNode, RunNode) -> Bool

    RunNode::not_equal

    fn RunNode::not_equal(x : RunNode, y : RunNode) -> Bool

    RunNode::to_repr

    all_rules

    fn all_rules() -> Array[RuleInfo]

    default_options

    fn default_options() -> LintOptions

    has_rule

    fn has_rule(code : String) -> Bool

    lint_dockerfile

    fn lint_dockerfile(doc : Dockerfile) -> LintResult

    lint_dockerfile_with

    fn lint_dockerfile_with(doc : Dockerfile, options : LintOptions) -> LintResult

    lint_source

    fn lint_source(source : String) -> Result[LintResult, ParseError]

    lint_source_with

    fn lint_source_with(source : String, options : LintOptions) -> Result[LintResult, ParseError]

    parse_config

    fn parse_config(source : String) -> LintOptions

    parse_dockerfile

    fn parse_dockerfile(source : String) -> Result[Dockerfile, ParseError]

    report_github

    fn report_github(result : LintResult, path? : String) -> String

    report_json

    fn report_json(result : LintResult) -> String

    report_parse

    fn report_parse(doc : Dockerfile) -> String

    report_rules

    fn report_rules() -> String

    report_text

    fn report_text(result : LintResult) -> String

    sample_bad_dockerfile

    fn sample_bad_dockerfile() -> String

    sample_dockerfile

    fn sample_dockerfile() -> String

    version

    let version : String