Changelog
All notable changes to transformers-mlinter will be documented in this file.
The format is based on Keep a Changelog, and this project uses Semantic Versioning.
[Unreleased]
Fixed
- A retired rule keeps its page on the docs site instead of disappearing from it.
TRF054vanished entirely when it was deprecated, so anyone meeting the id in an old CI log or an existing# trf-ignore: TRF054comment got a 404 rather than an explanation. Deprecated rules now appear in aRemoved rulestable on the rule index and get a generated page that gives the removal reason, says the id no longer runs and that asking for it is an error, and notes that leftover suppression comments are harmless. They stay out of the headline rule count and out of the filterable table of live rules. The tombstone’sdescriptioninrules.tomlis the published prose, and is exposed asmlinter.DEPRECATED_TRF_RULE_SPECSfor the generator to read; a tombstone with nodescriptionfalls back to a generic line.
[0.1.4] - 2026-08-17
Improved
- Rules can now be retired. A
[rules.TRFXXX]table markeddeprecated = trueis a tombstone: the rule module is deleted, and mlinter ignores the id everywhere — it is absent from--list-rules, fromTRF_RULES/TRF_RULE_CHECKS/ the package’s publicTRFXXXconstants, from the generated rule pages, and from the default set, so a project that still passes# trf-ignore: TRFXXXor has the rule in its own config is simply unaffected. Asking for a deprecated rule (--enable-rules TRFXXX,--rule TRFXXX) is an error. A rules TOML — including one passed with--rules-toml— that still describes a retired rule as active fails the run with exit code 2 and names the rule instead of silently linting nothing under that id..ai/skills/remove-mlinter-rule/walks an agent through a removal. mlinternow accepts files and directories to check as positional arguments, so a standalone model repository — model code shipped on the Hub withtrust_remote_code, which does not mirrorsrc/transformers/models/<model>/— can be linted without rearranging it into the transformers layout. A directory is searched recursively for model integration files, a file named explicitly is checked as given, and--changed-onlynarrows the git diff to the paths passed. With no path argument, discovery is unchanged:src/transformers/modelsandtests/modelsrelative to the current directory. Rules that resolve other models (sibling configs, files undertests/models/, other model directories) find nothing outside a transformers checkout and stay quiet, as do per-model allowlists and cutoff dates. A run that matched no model integration file now says so instead of reportingOK.- Lint cache entries are keyed on the absolute file path, so the same relative path in two different model repositories no longer shares a cache entry. Existing entries are re-checked once.
Removed
- Removed
TRF054, which flaggedself.image_token_id/self.video_token_id/self.audio_token_idassignments in processor__init__methods and asked for a property reading the tokenizer instead. It fired on legitimate processor code often enough that the noise outweighed what it caught.[rules.TRF054]stays inrules.tomlas adeprecated = truetombstone and the number is never reused. Nothing to do on the transformers side: mlinter ignores the id, and leftover# trf-ignore: TRF054comments are harmless. A project shipping its own rules TOML must markTRF054deprecated = trueor drop the table — leaving it active now fails the run with exit code 2.
[0.1.3] - 2026-08-13
Added
- Added
TRF020, which enforces that Multi-head Latent Attention (MLA) models — those whose configuration declareskv_lora_rank— isolate the KV LoRA expansion (conventionallykv_b_proj, or anynn.Linear(config.kv_lora_rank, ...)) in a dedicated method (e.g.expand_kv) thatforward()calls, rather than applying it inline insideforward(). This gives external backends (vLLM/SGLang) a single method to override so they can store and consume the compressed KV cache directly instead of materializing the full key/value states. The MLA gate reads companionconfiguration_*.pyfiles to detect thekv_lora_rankfield; models that intentionally deviate can suppress with# trf-ignore: TRF020. - Added
TRF021, which flagstorch.tensor(<scalar>, ..., device=<non-cpu>)inmodeling_*.pyandmodular_*.py. Building a 0-d tensor that way materializes the value on the host and then issues a host-to-device copy, which CUDA graph capture forbids;torch.full((), <scalar>, dtype=..., device=...)fills the same tensor directly on-device. The rule only fires when the value provably resolves to a Python scalar — numeric literals and arithmetic over them,torch.finfo/torch.iinfofields, scalar builtins andmath.*calls, locals bound exactly once,self.<attr>assigned in the class body, andself.config.<field>/config.<field>annotatedint/float/boolin the companionconfiguration_*.py. Fields that may also be sequences (e.g.eos_token_id: int | list[int] | None) and unresolvable expressions are left alone, as are construction-time methods (__init__,_init_weights,__post_init__,post_init). Suppress with# trf-ignore: TRF021. - Added
TRF022, which flags_no_split_modulesentries naming a class that does not exist in the model. Names are resolved against the classes defined or imported in themodeling_*.py/modular_*.pyfile and those defined by sibling modules of the same model directory. Entries naming another model’s classes are flagged too:post_initalready collects_no_split_modulesfrom child submodels. ComplementsTRF005, which only validates the shape of the value. Suppress with# trf-ignore: TRF022. - Added
TRF023, which flags config fields declared under an upstream paper’s abbreviation instead of the library’s canonical name:d_model/n_embd(→hidden_size),d_ff/d_inner/ffn_dim/ffn_hidden_size/expansion_ratio(→intermediate_size),d_head(→head_dim),n_head/n_heads(→num_attention_heads),n_layer/n_layers/num_blocks(→num_hidden_layers). Fields are collected from the class body and from__init__/__post_init__assignments and signature defaults, and each legacy name is reported once per class. Names that remain idiomatic in parts of the library (num_heads,num_layers,embed_dim,mlp_ratio) are deliberately not flagged.cutoff_dategrandfathers the ~78 models that predate the convention;kosmos2andopenaiare allowlisted because their doc pages (kosmos-2.md,openai-gpt.md) cannot be derived from the directory name, andqwen3_asrbecause its encoder config mirrors Whisper’s publicd_model. - Added
TRF024, which flagstorch.nnlayer constructors built with an integer literal greater than 8 in a dimension position — positionally or by keyword — inmodeling_*.pyandmodular_*.py. CoversLinear,LazyLinear,Bilinear,Embedding,EmbeddingBag,LayerNorm,RMSNorm,GroupNorm,BatchNorm*,InstanceNorm*,Conv*d,ConvTranspose*dandMultiheadAttention. Operator-shape arguments (kernel_size,stride,padding,num_groups) are ignored and literals up to 8 are allowed, so scalar heads, binary classifiers and RGB channel counts stay clean. A hardcoded width pins the module to one checkpoint size and splits the source of truth away from the config. - Added
TRF025, which flags mask factories (create_causal_mask,create_bidirectional_mask,create_sliding_window_causal_mask,create_chunked_causal_mask,create_masks_for_generate, and anycreate_*_maskhelper) called from a class whose name ends inLayer,AttentionorBlock. Mask construction does not vary per layer, so building it inside the layer repeats quadratic work and leaves each layer owning its own mask. Models and encoders that build the mask once and pass it down are not in scope. - Added
TRF026, which flags a non-PreTrainedModelclass that defines only__init__andforward, assigns exactly oneself.<attr>in__init__, and whoseforwardbody is exactlyreturn self.<attr>(...)for that attribute. The wrapper adds a level to every weight name, to_no_split_modules, to the parallelism plans and to every conversion mapping while contributing no computation.PreTrainedModelsubclasses are exempt because they exist forfrom_pretrainedand the auto classes even when the forward only delegates. - Added
TRF027, which flags bareassertinmodeling_*.py,modular_*.pyandconfiguration_*.py.python -Ostrips asserts, so a shape or config check written that way silently disappears, and anAssertionErrortells the user nothing actionable. - Added
TRF028, which requires a complete license header in the first 25 lines ofmodeling_*.py,modular_*.py,configuration_*.py,processing_*.py,image_processing_*.pyandvideo_processing_*.py. Every clause of the warranty paragraph is matched, not just the wordsApache License, because that is what the real defects look like:bitnetdrops the closinglimitations under the License.,tvpandbridgetowercarry a stray=before every comma from a bad search-and-replace, andminimax_m3_vlstops after the URL. The license name is not checked —blipis BSD-3-clause andsapiens2uses Meta’s own license — so a deliberate license choice is not reported. - Added
TRF029, which flags an__init__acceptingconfigalongside an argument that is unambiguously a config field (hidden_size,num_attention_heads,intermediate_size,head_dim,embed_dim,dropout,eps,patch_size,rope_theta, …). The value then has two sources of truth and the caller decides which wins.kosmos2is allowlisted because its doc page (kosmos-2.md) is not derivable from the directory name. - Added
TRF030, which flags attribute chains rooted atconfig/self.configthat go three or more levels deep. One hop (config.hidden_size) and two (config.text_config.hidden_size) are the normal sub-config accesses; deeper means the module should have been handed a sub-config. Reported once per line. - Added
TRF031, which flags a top-level@dataclassin a modeling file whose bases carry noOutputname. A plain dataclass does not index like a tuple, does not survivereturn_dict=False, and is invisible to@auto_docstring. AnyBaseModelOutputWith*base counts as satisfying the rule. - Added
TRF032, which flagsmasked_fill,masked_fill_,full,full_likeandnew_fullcalled with a negated literal of magnitude 1e3 or more. A hardcoded-1e9overflows to-infin float16 and is not the float32 minimum, so the mask behaves differently per dtype;torch.finfo(dtype).minis correct in all of them. - Added
TRF033, which flagsset_*methods other than thePreTrainedModelcontract ones (set_input_embeddings,set_output_embeddings,set_decoder,set_encoder,set_attn_implementation,set_default_language). A hyperparameter behind a setter is not in the config, so it is not saved, not restored byfrom_pretrained, and invisible to device-map and parallelism planning. - Added
TRF034, which flags a locally-defined class ending inLayer/Block, instantiated inside annn.ModuleList(...), that does not reachGradientCheckpointingLayerthrough its local base chain.gradient_checkpointing_enable()skips plainnn.Modulelayers silently, so training appears to checkpoint and still allocates full activations. ModuleLists of experts, heads or projections are out of scope. Ten models are allowlisted; the list is in the TOML. - Added
TRF035, which flags# noqainmodeling_*.py,modular_*.pyandconfiguration_*.py, reporting the suppressed codes when they are given. Three models are allowlisted. - Added
TRF036, which flagsnn.Sequential(...)in modeling files. Sequential names its children by position, so weights land atmlp.0.weight, and every conversion mapping and parallelism plan has to reference indices.x_clipis allowlisted. - Added
TRF037, which flagseinsumin modeling files and reports the equation when it is a literal. Disabled by default — einsum is occasionally the clearest way to write a contraction, so this is opt-in via--enable-rules TRF037rather than a hard convention.x_clipis allowlisted. - Added
TRF038, which checks that everymodeling_*.py,processing_*.py,image_processing_*.py,video_processing_*.pyandfeature_extraction_*.pyfile has a matchingtests/models/<model>/test_*.pyfile (e.g.modeling_acme.py->tests/models/acme/test_modeling_acme.py).configuration_*.pyis exempt, since config classes are conventionally exercised throughConfigTesterinsidetest_modeling_*.py.modular_*.pyfiles are handled by inspecting the classes they define rather than the filename, since one modular file can mix modeling, processing, image/video-processor and config classes. This rule has no# trf-ignore: TRF038suppression: every model can ship at least a minimal test built on a dummy config and randomly initialized weights, so exemptions must go throughallowlist_modelsinstead, where they are visible in review. - Added
TRF039, which flags imports insideif is_*_available(): ...guards (e.g.if is_vision_available(): from PIL import Image) that are never referenced anywhere else in the file.ruffdoes not clean these up on its own, so a leftover import from a refactor silently lingers insrc/transformers. Suppress with# trf-ignore: TRF039for genuine false positives (e.g. names only used dynamically). - Added
TRF040, which flags methods inmodeling_*.py/modular_*.pydecorated with both@capture_outputsand@can_return_tuple. Both decorators popreturn_dict, so only the outermost one sees the value the caller actually passed while the inner one silently falls back toself.config.return_dict.@capture_outputsalready handles theto_tupleconversion, which makes@can_return_tupleredundant. ComplementsTRF003, which covers manualreturn_dictbranching. Suppress with# trf-ignore: TRF040. - Added
TRF041, which requires a# CODEPATH:comment on everyif/elifstatement and conditional expression inmodeling_*.py/modular_*.pywhose condition reads aconfig.*orself.config.*attribute. The comment is accepted on the branch line or anywhere in the contiguous comment block above it, so it can head a multi-line explanation. Modelled on Rust’s// SAFETY:convention: the branch stays legal, but the author has to write down which checkpoints take which path. Deliberately broad — a branch on a numeric or optional config field forks the graph exactly as much as one on a boolean flag, and the library has 1 838 such branches across 330 models today.cutoff_dategrandfathers all of them; the eleven post-cutoff models are allowlisted in the TOML. Default coalescing is exempt by shape, not by name:X if X is not None else fallback, where the tested field is itself one of the results, isgetattr(config, x, default)spelled long and cannot fork the graph, so it needs no note (79 of the 2 674 firings in the library today). Mentioning None is not enough to qualify —config.vision_config is not Nonegates a whole extra tower and still has to explain itself. Fields that gate no checkpoint divergence —problem_typepicking a loss,hidden_actpicking an activation — can be exempted for a whole file with a module-level# trf-ignore: TRF041 config.problem_type, config.hidden_actdirective, instead of repeating a per-branch suppression.self.config.x,config.xandxall name the same field, and the directive has to name at least one, so a bare# trf-ignore: TRF041still means only its own line. Exemption is per field: a branch reading several config fields is skipped only when every one of them is exempt. - Added
TRF042, which requires atest_tokenization_*.pyfile to define a test class inheritingTokenizerTesterMixin.TokenizerTesterMixinis where encode/decode round-tripping, padding and truncation, special-token handling and save/load equivalence are actually checked, so a file that only asserts a couple of hand-written id lists looks tested while the tokenizer is broken in every one of those dimensions. Files whose only classes are helpers are skipped — only classes the runner collects count, so a helper mixing in the suite does not satisfy the rule for a real test class — and inheritance is followed through local base classes and into another model’s tokenizer test —DistilBertTokenizationTest(test_tokenization_bert.BertTokenizationTest)counts as satisfied because the class it derives from carries the mixin. A base the tests tree cannot resolve never counts. Five of the six tokenizer tests missing the mixin predate 2026 and are grandfathered bycutoff_date;autois allowlisted becausetest_tokenization_auto.pytestsAutoTokenizerresolution rather than one model’s tokenizer. - Added
TRF043–TRF054, twelve rules mined from the transformers deep-review dimension registry (recurring maintainer review comments across ~300 PRs, nine reviewers). Across the twelve, a current transformers checkout reports three violations after allowlisting the legacy tail —TRF043oncohere_compassandTRF054twice onmuse_glimmer, both models added after these rules were written.TRF045andTRF054additionally usecutoff_date = "2026-06-20"so they guard new models without opening a backlog.TRF043: attention classes must not declareposition_idsin theirforwardsignature — it flows through**kwargsso padding-free flash-attention can consume it.TRF044: nocache_positionparameter anywhere in modeling code; it is removed framework surface and the cache update call carries no position threading.TRF045:forwardmust not declareoutput_attentions/output_hidden_states/return_dict; the@capture_outputs/@can_return_tupledecorator stack owns them.TRF046:forwardmust not writeself.<attr>; modules are stateless in forward and carried state is passed explicitly.TRF047: image/video processorpreprocess/_preprocess/__call__/post_process*must not writeself.<attr>; carried state breaks preprocess-many-then-postprocess batching.TRF048:_tied_weights_keysmust be the v5 dict form mapping target to source, not a list.TRF049: no weight-value initialization in__init__(nn.init.*,init.*, or in-place ops on own parameters); meta-device instantiation discards it — allocate withtorch.emptyand initialize in_init_weights.TRF050: attention classes must not instantiate their own*RotaryEmbedding; the Model owns a singlerotary_emband passes cos/sin down asposition_embeddings.TRF051: no comparisons against_attn_implementationin modeling code; dispatch belongs toALL_ATTENTION_FUNCTIONS.get_interfaceand backend-conditional tensor handling tointegrations/.TRF052: no module-level*_ATTENTION_CLASSESdispatch dicts, even propagated from a legacy parent.TRF053: no manualshift_logits/shift_labelsconstruction;self.loss_functionowns label shifting vialabels=None, shift_labels=labels.TRF054: processor media token ids (image_token_id/video_token_id/audio_token_id) are properties, never instance attributes set in__init__— instance attributes serialize intoprocessor_config.json.
- Added
TRF055, which flagsconfig = SomeConfigonPreTrainedModelsubclasses inmodeling_*.pyandmodular_*.py; the correct form is the annotationconfig: SomeConfig.PreTrainedModel.__init_subclass__derivesconfig_classfrom aconfigannotation viainspect.get_annotations(cls), so an assignment is invisible to it: the class gets a stray attribute whileconfig_classsilently keeps the parent’s, which is howGemma4VisionModel.config_classresolved toGemma4Configinstead ofGemma4VisionConfig. A pure annotation has no runtime value, so it setsconfig_classcorrectly. Suppress with# trf-ignore: TRF055. - Added
TRF056, which flags.item()and.tolist()inside aforwardinmodeling_*.pyandmodular_*.py. Both read a tensor back to the host, so the dynamo graph breaks at the use site. A.tolist()whose result is the split-size argument ofsplit(...), passed directly or through a local, is exempt astorch.splitneeds Python ints. - Added
TRF057, which flags a missing@auto_docstringon the classes that need it: publicPreTrainedModelsubclasses (<Model>PreTrainedModel,<Model>Model,<Model>For<Task>, backbones),PreTrainedConfigsubclasses,ModelOutputsubclasses, image processors andProcessorMixinsubclasses, and on their public methods:forward,get_image_features,get_video_features,get_audio_features,get_text_features,preprocessand__call__. A class or method in amodular_*.pyfile is checked against the files generated from it.
Improved
- Added
--output-json FILE, which writes every finding toFILEas JSON alongside the normal output: afindingslist of{"path", "line", "rule", "message"}objects plus arulesmap carrying the description,why_badand diff of each rule that fired. The file is always written, so a consumer never has to distinguish “clean run” from “no file produced”, and the exit code is unaffected. The transformers CI uses it to upload findings as an artifact and post them as inline review comments. - Documentation site published at https://huggingface.github.io/transformers-mlinter/, built
with Jekyll + just-the-docs from
docs/by.github/workflows/pages.yml. The per-rule reference is generated frommlinter/rules.tomlbyscripts/build_docs.py, so a rule’sdescriptionandexplanationfields are now published prose and adding a rule documents it with no page to write.docs/rules/is git-ignored and rebuilt on every build. Build locally withmake docs(ormake docs-serveto preview); theREADME.mdlong-form content moved todocs/index.mdand the CLI reference todocs/usage.md. - Project logo, used in the docs sidebar, as the site favicon (cropped to the mark, since a
tab icon cannot render the wordmark), as the
og:imagesocial preview, and at the top ofREADME.md. - Widened file discovery to the tests tree.
iter_modeling_filesnow also walkstests/models/**/test_tokenization_*.pyvia a new sharedTESTS_ROOT,_model_dir_nameresolves a model name from either root, and--changed-onlyaccepts those paths. This changes which files the linter walks for every rule, so it is worth noting even though no existing rule is affected: they all gate on the file-name prefix, and a full scan confirms none ofTRF001-TRF041fires on a test file. File count on a current checkout goes from 1 132 to 1 222. - Shared the companion-config resolution helpers (
_find_config_file,_parse_config_classes,_resolve_config_class_name_from_modeling_class,_resolve_target_config_class_name) by moving them fromTRF015intomlinter/_helpers.py, so cross-file rules resolve a modeling class to its target config class the same way. - Moved
model_contribution_date(andDOCS_ROOT) fromTRF019intomlinter/_helpers.pyand addedis_exempt_by_cutoff, so every cutoff-gated rule resolves a model’s contribution date the same way. The lookup now also tries the hyphenated spelling of the model directory (blenderbot_small→blenderbot-small.md), which grandfathers models whose doc page uses hyphens instead of leaving them permanently unexempt. - Moved the modular generation banner into
mlinter/_helpers.pyasGENERATED_FILE_MARKER, next to a newread_file_headhelper, so rules that read a generated file recognise it the same way_is_generated_filedoes.
Fixed
- Removed a stale
# type: ignore[union-attr]inTRF019thattyreported as an unused suppression, somake typecheckis clean. - Fixed the reviewer context script computing the next free rule ID from the PR’s own checkout, which made it report the rule ID the PR adds as already taken and ask for a needless renumbering.
[0.1.2] - 2026-07-08
Added
- Added
TRF016, which flagsdo_*boolean flags declared on image/video processor classes that are not referenced by an overriddenpreprocess/_preprocessmethod. - Expanded the set of files the linter targets to include
image_processing_*.pyandvideo_processing_*.pyin addition tomodeling_*.py,modular_*.py, andconfiguration_*.py. This affects file discovery for every rule, not justTRF016. - Added
TRF017, which flags model output classes decorated with both@auto_docstringand@dataclasswhere@dataclassis listed above@auto_docstring. Bottom-up decorator application means@auto_docstringthen runs before@dataclasssynthesizes__init__, and ends up modifying the parent class’s__init__.__doc__instead of the subclass’s. Mirrors the upstream fix in huggingface/transformers#45702. - Added
TRF018, which flags_init_weightsoverrides onPreTrainedModelsubclasses that do not chain viasuper()._init_weights(...)(or the modular-file equivalent<Class>._init_weights(self, ...)). Models that intentionally fully override initialization can suppress with# trf-ignore: TRF018. Modular files using theraise AttributeError(...)delete-sentinel are skipped. See https://github.com/huggingface/transformers/pull/45597 for the bug class this catches. - Added
TRF019, which flags non-empty_defaultsdictionaries on*ProcessorKwargsTypedDict classes inprocessing_*.pyfiles for models contributed on or after the rule cutoff date. Processor defaults should live inprocessor_config.jsonon the Hub instead of being hardcoded in Python. - Expanded the set of files the linter targets to include
processing_*.pyfiles in addition to modeling, configuration, modular, image-processing, and video-processing files.
[0.1.1] - 2026-04-22
Added
- Added
--rules-tomlso the CLI can load rule metadata from a custom TOML file instead of the bundledmlinter/rules.toml. - Added schema version validation for rule-spec TOML files and included the active rule-spec hash in the lint cache so custom rule sets do not reuse stale cache entries.
Fixed
- Fixed
TRF005so modular files may useAttributeError()as the sentinel for removing_no_split_modulesduring generated-code cleanup, whilemodeling_*.pyfiles still require a list or tuple of non-empty strings.
[0.1.0] - 2026-04-21
Added
- Initial release of
transformers-mlinter.
