{"id":51861637,"url":"https://github.com/juninmd/zplint","last_synced_at":"2026-07-24T08:02:15.446Z","repository":{"id":372564132,"uuid":"1297464122","full_name":"juninmd/zplint","owner":"juninmd","description":"Lightning-fast linter for ZP5.0 AMXX plugins (27 crash-pattern detectors)","archived":false,"fork":false,"pushed_at":"2026-07-22T01:40:17.000Z","size":107,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-22T03:29:54.075Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/juninmd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-07-11T13:25:02.000Z","updated_at":"2026-07-22T01:40:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/juninmd/zplint","commit_stats":null,"previous_names":["juninmd/zplint"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/juninmd/zplint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juninmd%2Fzplint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juninmd%2Fzplint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juninmd%2Fzplint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juninmd%2Fzplint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/juninmd","download_url":"https://codeload.github.com/juninmd/zplint/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/juninmd%2Fzplint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35832970,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-24T02:00:07.870Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-24T08:02:14.709Z","updated_at":"2026-07-24T08:02:15.439Z","avatar_url":"https://github.com/juninmd.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zplint 🔍\n\nLightning-fast linter for **Pawn / AMX Mod X** plugins (CS 1.6), with deep **Zombie Plague 5.0** support.  \nScans `.sma` files for **106 detectors**: compile errors before you compile, runtime-crash patterns\n(HLDS `svc_bad` / segfault / run time errors 3/4/10/11), engine limits (precache/edicts/netchan),\ntag-mismatch bugs, and ZP50 API contract violations.\n\nEvery rule is backed by a documented source (AMXX compiler sources, AlliedModders wiki/forums,\namxmodx.org API docs, official ZP 5.0 sources) — see [`docs/KNOWLEDGE.md`](docs/KNOWLEDGE.md).\n\nWritten in **Rust**, parallelized with rayon. Single binary, zero runtime dependencies.\n~1.7s for 542 files. Reads Windows-1252 legacy files, honors `#pragma ctrlchar`.\n\nValidated against two corpora: the official `alliedmodders/amxmodx` bundled plugins\n(**0 errors** — canonical code passes clean) and a 542-file real-world ZP plugin collection.\n\n## Install\n\n```bash\ngit clone https://github.com/your/zplint\ncd zplint\ncargo build --release\n./target/release/zplint --help\n```\n\n## Usage\n\n```bash\nzplint lint              # Lint all .sma files\nzplint lint file.sma     # Lint specific file\nzplint lint folder/      # Lint .sma files under a folder\nzplint watch             # Re-lint on file save\nzplint fix               # Apply auto-fixes\n```\n\n## Rules (106 detectors)\n\n### Player Validation (8)\n| Rule | Severity | Fix | Description |\n|------|----------|-----|-------------|\n| `client_disconnect_guard` | error | ❌ | Risky natives in client_disconnected without guard |\n| `dangerous_forward_guard` | error | ❌ | Risky natives in fw_Killed/Ham_Killed/fw_Spawn |\n| `message_begin_guard` | error | ❌ | message_begin(MSG_ONE) with unvalidated index -\u003e `svc_bad` |\n| `find_entity_in_sphere` | error | ❌ | FindEntityInSphere result used as player |\n| `loop_player_guard` | error | ❌ | 1-32 loop without is_user_connected guard |\n| `attacker_not_validated` | error | ❌ | TakeDamage handler uses attacker without is_user_alive (attacker = 0/world) |\n| `zp_infect_cure_guard` | error | ❌ | zp_core_infect/cure without is_zombie check |\n| `zp_force_no_guard` | error | ❌ | zp_core_force_infect/cure bypasses validation |\n\n### Entity \u0026 Memory (9)\n| Rule | Severity | Fix | Description |\n|------|----------|-----|-------------|\n| `create_entity_guard` | error | ❌ | create_entity without is_valid_ent check |\n| `nested_message` | error | ❌ | Nested message_begin without message_end (crashes server) |\n| `message_write_outside` | error | ❌ | write_* outside message_begin/message_end (crashes server) |\n| `message_end_without_begin` | error | ❌ | message_end without message_begin (crashes server) |\n| `message_hook_scope` | error | ❌ | get_msg_arg*/set_msg_arg* outside register_message callback |\n| `hardcoded_message_id` | error | ❌ | numeric message id in message_begin (use get_user_msgid/constant) |\n| `array_random_empty` | error | ❌ | ArrayGet* with random ArraySize()-1 without non-empty guard |\n| `touch_spam` | warning | ❌ | Touch handler spams client_print |\n| `precache_sound` | error | ❌ | emit_sound with custom sound never precached |\n\n### ZP API Misuse (5)\n| Rule | Severity | Fix | Description |\n|------|----------|-----|-------------|\n| `zp_gamemode_if` | error | ✅ | `if (mode)` should be `if (mode \u003e 0)` |\n| `zp_class_if` | error | ✅ | `if (class)` should be `if (class \u003e 0)`; also checks `zp_class_*_register()` in wrong scope |\n| `zp_items_register_check` | error | ❌ | zp_items_register() return value not checked |\n| `precache_outside_precache` | error | ❌ | precache_*() called in plugin_init() not plugin_precache() -\u003e crash |\n| `library_exists_hotpath` | warning | ❌ | LibraryExists() per-hit in TakeDamage (cache as bool) |\n\n### AMXX General (10)\n| Rule | Severity | Fix | Description |\n|------|----------|-----|-------------|\n| `set_task_public` | error | ❌ | set_task callback is non-public function |\n| `registered_callback_public` | error | ❌ | menu/register callback is non-public function |\n| `read_data_multi_context` | error | ❌ | read_data() in event + non-event callback |\n| `pev_oldbuttons` | warning | ❌ | pev_oldbuttons unreliable in PreThink |\n| `get_user_origin` | warning | ❌ | get_user_origin() loses float precision |\n| `task_interval_zero` | error | ❌ | set_task with interval 0.0 (minimum 0.1) |\n| `set_task_flags` | error | ❌ | set_task invalid flags/repeat usage |\n| `percent_n_player_name` | error | ❌ | `%n` player-name formatter can throw on invalid index |\n| `menu_handler_destroy` | error | ❌ | dynamic menu_create handler does not call menu_destroy |\n| `fopen_close` | error | ❌ | fopen handle is not closed with fclose in same function |\n\n### Code Smells (5)\n| Rule | Severity | Fix | Description |\n|------|----------|-----|-------------|\n| `abort_call` | error | ❌ | abort() causes run time error 1 |\n| `precache_sound_sprite` | error | ❌ | precache_sound on sprite-named variable (use precache_model) |\n| `buffer_size` | warning | ✅ | Hardcoded buffer \u003c 64 in get_user_* (use charsmax) |\n| `client_cmd_spk` | warning | ❌ | client_cmd(0, \"spk...\") instead of emit_sound |\n| `hardcoded_maxplayers` | warning | ❌ | loop uses `#define MAXPLAYERS 32` as runtime player count |\n\n## Research-driven detectors (69)\n\nAdded from internet research of real compile errors, crash reports and API docs\n(sources in [`docs/KNOWLEDGE.md`](docs/KNOWLEDGE.md)). All are on by default; turn any off\nvia `rules.disable` (see Config).\n\n### Compile structure (9)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `unbalanced_braces` | error | File ends with unmatched `{`/`}` (errors 030/054, cascades into 010/004) |\n| `unbalanced_preprocessor` | error | `#if`/`#else`/`#endif` stack errors (026/060/061) |\n| `unterminated_string` | error | Odd quote count on a line (error 037); escape-char and continuation aware |\n| `else_paren` | error | `else (cond)` instead of `else if (cond)` (error 029/010) |\n| `empty_statement` | error | `if (...)`/`while (...)` terminated by `;` detaches the block (error 036) |\n| `stacked_case` | error | `case A:` `case B:` — Pawn has no fallthrough; use `case A, B:` |\n| `line_too_long` | warning | Line \u003e 511 chars (error 075 on amxxpc 1.8.x) |\n| `array_index_oob` | error | Literal out-of-bounds array write, e.g. `Players[32] = 15` on a 32-slot array (valid 0-31) |\n| `array_compare_by_ref` | error | `arr1 == arr2` compares array references, not contents — doesn't compile in Pawn |\n\n### Correctness (14)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `string_literal_compare` | error | `== \"...\"` — strings need equal()/equali() (error 033) |\n| `string_assign` | warning | String literal larger than the destination array (error 047) |\n| `forward_arity` | error | Known forward defined with wrong parameter count (error 025) |\n| `formatex_self` | error | formatex() output buffer also used as input (corrupts output) |\n| `assignment_in_condition` | warning | `if (x = 1)` (warning 211); `((x = y))` idiom is allowed |\n| `comparison_as_statement` | warning | `x == 5;` as a statement does nothing (warning 215) |\n| `self_assignment` | warning | `x = x;` (warning 226) |\n| `constant_condition` | warning | `if (0)` / `if (1)` dead-codes a branch (warnings 205/206) |\n| `unreachable_code` | warning | Statements after an unconditional return (warning 225) |\n| `contain_truthy` | warning | contain() returns -1 on miss — bare truthiness inverts the logic |\n| `strcmp_truthy` | warning | strcmp() returns 0 on match — bare `if (strcmp(..))` means \"differs\" |\n| `sql_fieldname_truthy` | warning | `SQL_FieldNameToNum()` returns -1 on miss — bare truthiness misreads column 0 as failure |\n| `func_id_truthy` | warning | `get_func_id()`/`get_xvar_id()` return -1 on failure but id 0 is valid — bare truthiness (direct or via variable) misreads it as failure |\n| `global_shadowing` | warning | Local `new` shadows a global (warning 219) |\n\n### Tag mismatch — warning 213 that misbehaves at runtime (13)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `set_task_int_interval` | error | `set_task(10, ..)` → interval becomes ~1e-44s (runs every frame) |\n| `pev_float_int` | error | Integer into Float pev field (pev_health 100 → ~1.4e-43 = instant death) |\n| `int_native_float` | error | Float into int native (set_user_health(id, 100.0) → 1120403456 hp) |\n| `engfunc_int_float` | error | Float literal into an engfunc() int/entity slot, checked positionally across ~20 EngFunc_* signatures (WalkMove mode, hull, WriteByte value, ...) |\n| `engfunc_float_int` | error | Int literal into an engfunc() Float slot, same table, reverse direction (e.g. `EngFunc_WriteCoord(36)` instead of `36.0`) |\n| `entity_ev_type_mismatch` | error | Engine module `entity_set_int(id, EV_FL_gravity, ..)` — EV_* prefix names the real family (int/float/vector/edict/string/byte), function used doesn't match |\n| `ham_int_float` | error | Float literal into an ExecuteHam(B)/Ham_* int slot (e.g. Ham_Use's use_type) |\n| `ham_float_int` | error | Int literal into an ExecuteHam(B)/Ham_* Float slot (e.g. `Ham_TakeDamage` damage passed as `25` instead of `25.0`) |\n| `set_ham_param_mismatch` | error | `SetHamParamInteger(4, ..)` inside a `Ham_TakeDamage` hook — slot 4 is `Float:damage`, wrong setter used regardless of the value passed |\n| `cs_float_int` | error | Int literal into a cstrike.inc Float setter (`cs_set_c4_explode_time`, `cs_set_user_lastactivity`, `cs_set_hostage_lastuse/nextuse`) |\n| `fun_float_int` | error | Int literal into `set_user_maxspeed`/`set_user_gravity`'s Float parameter |\n| `amxmodx_int_float` | error | Float literal into a `set_hudmessage`/`set_dhudmessage`/`emit_sound`/`change_task` int slot |\n| `amxmodx_float_int` | error | Int literal into a `set_hudmessage`/`set_dhudmessage`/`emit_sound`/`change_task` Float slot (fxtime, holdtime, vol, att, newTime, ...) |\n\n### Runtime crashes (7)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `userid_as_index` | error | `arr[get_user_userid(id)]` — userid is a session counter, not an index (RTE 4) |\n| `player_array_32` | error | `new arr[32]` indexed by player id — overflows on full server (RTE 4) |\n| `find_ent_no_advance` | error | Entity-search loop restarting from a constant — infinite loop, server freeze |\n| `deathmsg_killer_guard` | error | DeathMsg `read_data(1)` can be 0 (world kills) — guard before use |\n| `div_by_runtime` | warning | Division by get_playersnum()/cvar that can be zero (RTE 11) |\n| `pragma_dynamic_stack` | warning | Local array ≥ 2048 cells without `#pragma dynamic` (RTE 3) |\n| `format_injection` | warning | User text (read_args/get_user_name) as format string — `%` in chat crashes |\n\n### Engine / HLDS limits (10)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `precache_mp3` | error | .mp3 through precache_sound/emit_sound — never plays; use precache_generic |\n| `sound_prefix` | error | `\"sound/...\"` prefix — paths are already relative to sound/ |\n| `model_not_precached` | error | Literal model set but never precached (fatal SV_ModelIndex) |\n| `mp3_loading_path` | warning | Client stufftext filter silently blocks paths containing \"loading\" |\n| `te_reliable` | warning | SVC_TEMPENTITY on MSG_ALL/MSG_ONE — reliable-channel overflow kicks |\n| `precache_in_loop` | warning | precache_* inside a loop risks the 512-entry engine limit |\n| `entity_leak` | warning | create_entity with no removal path — \"ED_Alloc: no free edicts\" |\n| `hud_channel_range` | warning | set_hudmessage channel outside 1-4/-1 |\n| `changelevel_cmd` | warning | server_cmd changelevel bypasses forwards and map validity check |\n| `geoip_code_overflow` | error | `geoip_code2/3()` overflow their result buffer by one cell on an unknown IP — use the `_ex` variant |\n\n### API contracts \u0026 modernization (8)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `callback_not_defined` | warning | Registered callback string has no function in the file (\"function not found\") |\n| `client_command_handled` | warning | PLUGIN_HANDLED in client_command starves other plugins (use _MAIN) |\n| `client_connect_actions` | warning | Client-affecting natives in client_connect (too early; use putinserver) |\n| `deprecated_symbols` | warning | client_disconnect / md5 / strbreak (AMXX 1.9 warning 233) |\n| `define_reserved_const` | warning | Redefining MAX_PLAYERS etc. from amxconst.inc (warning 201) |\n| `get_cvar_hotpath` | warning | get_cvar_* outside init — use cached pcvars (\"dozens of times faster\") |\n| `strlen_in_loop` | warning | strlen() in loop condition — O(n²) |\n| `buffer_in_loop` / `read_file_loop` | warning | Array re-zeroed per iteration / O(n²) file API in loops |\n\n### ZP 5.0 API (6)\n| Rule | Severity | Description |\n|------|----------|-------------|\n| `zp_fw_attacker_guard` | error | zp_fw_core_infect/cure `attacker` is 0 for gamemode/admin infections |\n| `zp_select_pre_filter` | warning | select_pre returns restrictive ZP_* without using itemid/classid — blocks ALL items |\n| `zp_select_pre_return` | warning | PLUGIN_HANDLED in select_pre (=NOT_AVAILABLE) / ZP_* in core _pre forwards |\n| `zp50_register_return` | warning | Registration id discarded — cannot filter forwards for your item/class |\n| `zp50_get_in_init` | warning | zp50 query natives in plugin_init (\"Invalid Array Handle\" load-order bug) |\n| `zp43_mixing` | warning | ZP 4.3 API (`\u003czombieplague\u003e`) mixed with zp50 includes |\n\n## Config\n\n`zplint.toml` in project root:\n\n```toml\n[lint]\npaths = [\"meus_plugins_organizados\"]\nexclude = [\"00-Old_Archive\"]\n\n[lint.rules]\n# original detectors: true = enable, false = disable\nclient_disconnect_guard = true\nzp_gamemode_if = true\nabort_call = false\n# research-driven detectors are on by default; turn off by id:\ndisable = [\"deprecated_symbols\", \"get_cvar_hotpath\"]\n\n[output]\ncolor = true\n```\n\nSeverity model: **errors** are crash/compile-failure patterns and set exit code 1;\n**warnings** are style/perf/modernization signals and never fail CI.\n\n## Performance\n\n542 real `.sma` files = **1.7s**; official amxmodx plugins (74 files) = **0.35s** (release build, rayon-parallel).\nNon-UTF8 (Windows-1252) legacy files are decoded, not skipped.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuninmd%2Fzplint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjuninmd%2Fzplint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuninmd%2Fzplint/lists"}