Closed6

Erg用のLinterの関数を実装する

slugslug

ref to https://github.com/erg-lang/erg/issues/501

とりあえずはこれを作る

  • erg/crates/erg_linter/lint.rs
    pub fn lint(&mut self, hir: &HIR) -> CompileWarnings {
        log!(info "Start linting");
        for chunk in hir.module.iter() {
            self.lint_too_many_params(chunk);
            self.lint_bool_comparison(chunk)
            self.lint_too_many_instance_attributes(chunk); // <- here it is
        }
        log!(info "Finished linting");
        self.warns.take()
    }
    fn lint_too_many_instance_attributes(&mut self, expr: &Expr) {
        // errなのは意図的に赤くするため、見にくい人はinfo(緑色)
        log!(err {:#?}, expr)
    }
slugslug

erg_linterを実行するコマンドがなかったため、lintコマンドを追加する

  • erg/crates/erg_common/config.rs
        pub fn parse() -> Self {
        let mut args = env::args();
        // --- 省略 ---
               "--check" => {
                    cfg.mode = ErgMode::FullCheck;
                }
                "--lint" => {
                    cfg.mode = ErgMode::Lint;
                }
                "--compile" | "--dump-as-pyc" => {
                    cfg.mode = ErgMode::Compile;
                }
        // --- 省略 ---
    }

ついでに今回のインスタンス属性ありすぎ問題用のスクリプトを用意する

C = Class {
    foo_01 = Int;
    foo_02 = Int;
    foo_03 = Int;
    foo_04 = Int;
    foo_05 = Int;
    foo_06 = Int;
    foo_07 = Int;
    foo_08 = Int;
    foo_09 = Int;
    foo_10 = Int;
    foo_11 = Int;
    foo_12 = Int;
    foo_13 = Int;
    foo_14 = Int;
    foo_15 = Int;
    foo_16 = Int;
    foo_17 = Int;
    foo_18 = Int;
    foo_19 = Int;
    foo_20 = Int;
    foo_21 = Int;
    foo_22 = Int;
    foo_23 = Int;
    foo_24 = Int;
    foo_25 = Int;
    foo_26 = Int;
    foo_27 = Int;
    foo_28 = Int;
    foo_29 = Int;
    foo_30 = Int;
    foo_31 = Int;
    foo_32 = Int;
    foo_33 = Int;
    foo_34 = Int;
    foo_35 = Int;
    foo_36 = Int;
}

動作確認をする

cargo rd -- --lint too_many.er
slugslug

行が多いので抜き出した抽象構文木(AST)が以下の通り

    ClassDef {
        obj: Class(
            ClassTypeObj {
                t: Mono(
                    Rc(
                        "<module>::C",
                    ),
                ),
                base: Some(
                    Builtin {
                        t: Record(
                            Dict {
                                dict: {
                                    Field {
                                        vis: Private,
                                        symbol: Rc(
                                            "foo_15",
...
                ],
                var_params: None,
                default_params: [],
                kw_var_params: None,
                return_t: Mono(
                    Rc(
                        "<module>::C",
                    ),
                ),
            },
        ),
        methods_list: [],
    },

ClassDefobj: Classので進んでいき、t: Recordまでくれば良い

ergにおいてClassのインスタンス属性はレコードとして定義されている(参考)ため

これをパターンマッチで取り出していく

    fn lint_too_many_instance_attributes(&mut self, expr: &Expr) {
        const MAX_INSTANCE_ATTRIBUTES: usize = 36;
        if let Expr::ClassDef(ClassDef { obj, .. }) = expr {
            if let Some(TypeObj::Builtin {
                t: Type::Record(record),
                ..
            }) = obj.base_or_sup()
            {
                if record.len() >= MAX_INSTANCE_ATTRIBUTES {
                    // ワーニングの処理
                }
            }
        }
    }
slugslug

warningを定義する

  • crates/erg_linter/warn.rs

同じモジュール内の関数をコピーして名前とメッセージを変えてみる

pub(crate) fn too_many_instance_attributes(
    input: Input,
    caused_by: String,
    loc: Location,
) -> CompileWarning {
    CompileWarning::new(
        ErrorCore::new(
            vec![SubMessage::ambiguous_new(loc, vec![], Some("hint".to_string()))],
            "msg here".to_string(),
            0,
            ErrorKind::AttributeWarning,
            loc,
        ),
        input,
        caused_by,
    )
}
  • crates/erg_linter/lint.rs

先程のlinterメソッドの中で呼び出す

    fn lint_too_many_instance_attributes(&mut self, expr: &Expr) {
        const MAX_INSTANCE_ATTRIBUTES: usize = 36;
        if let Expr::ClassDef(ClassDef { obj, .. }) = expr {
            if let Some(TypeObj::Builtin {
                t: Type::Record(record),
                ..
            }) = obj.base_or_sup()
            {
                if record.len() >= MAX_INSTANCE_ATTRIBUTES {
                    // --- from ---
                    self.warns.push(too_many_instance_attributes(
                        self.input(),
                        self.caused_by(),
                        expr.loc(),
                    ));
                    // --- here ---
                }
            }
        }
    }

再度動作チェックをしてみる

cargo rd -- --lint too_many.er
Warning[#0000]: File .\tests\linter\too_many.er, line 1, <module>

1 | C = Class {
  : -
  : `- hint

AttributeWarning: msg here

こういったときにTDD開発みたいなやり方ができると良いなあと思う

TODOとして以下のようなのを考える(注意: TDDのやり方ではない)

  • hintメッセージを書く
  • warningメッセージを書く
  • メッセージを多言語対応する(switch_lang)
    • english
    • japanese
    • simplified_chinese
    • traditional_chinese
slugslug
  • crates/erg_linter/lint.rs

メインのメッセージは今回の実装目的の内容をかく

ヒントはちょっとめんどくさく、現状当たり障りのないことしか書けない

pub(crate) fn too_many_instance_attributes(
    input: Input,
    caused_by: String,
    loc: Location,
) -> CompileWarning {
    let msg = switch_lang!(
            "japanese" => "インスタンス属性が多すぎます",
            "simplified_chinese" => "实例属性过多",
            "traditional_chinese" => "實例屬性過多",
            "english" => "too many instance attributes",
    )
    .to_string();
    let hint = switch_lang!(
            "japanese" => "サブクラスやデータクラスを活用してください",
            "simplified_chinese" => "利用子类和数据类",
            "traditional_chinese" => "利用子類和資料類",
            "english" => "take advantage of subclasses or data classes",
    )
    .to_string();
    CompileWarning::new(
        ErrorCore::new(
            vec![SubMessage::ambiguous_new(loc, vec![], Some(hint))],
            msg,
            0,
            ErrorKind::AttributeWarning,
            loc,
        ),
        input,
        caused_by,
    )

実行して試してみる

# [alias]
# r = "run"
# r_ja = "run --features japanese"
# r_zh_cn = "run --features simplified_chinese"
# r_zh_tw = "run --features traditional_chinese"
cargo r_ja -- --lint .\tests\linter\too_many.er

Warning[#000]: File .\tests\linter\too_many.er, line 1, <module>

1 | C = Class {
  : -
  : `- サブクラスやデータクラスを活用してください

AttributeWarning: インスタンス属性が多すぎます

cargo r_zh_cn -- --lint .\tests\linter\too_many.er

Warning[#0000]: File .\tests\linter\too_many.er, line 1, <module>

1 | C = Class {
  : -
  : `- 利用子类和数据类

AttributeWarning: 实例属性过多
  • line!()

line!()を使い実行時のファイルの行を入れている

  • crates/erg_linter/lint.rs
                    self.warns.push(too_many_instance_attributes(
                        self.input(),
                        line!() as usize, <- here
                        self.caused_by(),
                        expr.loc(),
                    ));
  • crates/erg_linter/warn.rs
pub(crate) fn too_many_instance_attributes(
    input: Input,
    errno: usize,
    caused_by: String,
    loc: Location,
) -> CompileWarning {
// 略
        ErrorCore::new(
            vec![SubMessage::ambiguous_new(loc, vec![], Some(hint))],
            msg,
            errno, <- here
            ErrorKind::AttributeWarning,
            loc,
        ),

slugslug

最終出力結果が以下のようになる

Warning[#0233]: File .\tests\linter\too_many.er, line 1, <module>

1 | C = Class {
  : -
  : `- サブクラスやデータクラスを活用してください

AttributeWarning: インスタンス属性が多すぎます
このスクラップは3ヶ月前にクローズされました