{"id":19466748,"url":"https://github.com/zulip/zulint","last_synced_at":"2025-10-07T08:31:20.531Z","repository":{"id":33929693,"uuid":"143569068","full_name":"zulip/zulint","owner":"zulip","description":"A lightweight linting framework designed for complex applications using a mix of third-party linters and custom rules.","archived":false,"fork":false,"pushed_at":"2025-07-19T01:23:18.000Z","size":1498,"stargazers_count":25,"open_issues_count":6,"forks_count":17,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-07-19T06:05:25.568Z","etag":null,"topics":["linter","python","zulip"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zulip.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","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},"funding":{"github":"zulip","patreon":"zulip","open_collective":"zulip"}},"created_at":"2018-08-04T23:13:09.000Z","updated_at":"2025-07-19T01:23:23.000Z","dependencies_parsed_at":"2022-09-13T22:40:19.757Z","dependency_job_id":"24a0d1a3-cacd-487e-bfac-bc287d489808","html_url":"https://github.com/zulip/zulint","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zulip/zulint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zulip%2Fzulint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zulip%2Fzulint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zulip%2Fzulint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zulip%2Fzulint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zulip","download_url":"https://codeload.github.com/zulip/zulint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zulip%2Fzulint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278743101,"owners_count":26037972,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-07T02:00:06.786Z","response_time":59,"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":["linter","python","zulip"],"created_at":"2024-11-10T18:30:00.197Z","updated_at":"2025-10-07T08:31:20.525Z","avatar_url":"https://github.com/zulip.png","language":"Python","funding_links":["https://github.com/sponsors/zulip","https://patreon.com/zulip","https://opencollective.com/zulip"],"categories":[],"sub_categories":[],"readme":"# zulint\n\n![CI](https://github.com/zulip/zulint/workflows/CI/badge.svg)\n\nzulint is a lightweight linting framework designed for complex\napplications using a mix of third-party linters and custom rules.\n\n## Why zulint\n\nModern full-stack web applications generally involve code written in\nseveral programming languages, each of which have their own standard\nlinter tools.  For example, [Zulip](https://zulip.com) uses Python\n(mypy, Ruff), JavaScript (eslint), CSS (stylelint),\npuppet (puppet-lint), shell (shellcheck), and several more.  For many\ncodebases, this results in linting being an unpleasantly slow\nexperience, resulting in even more unpleasant secondary problems like\ndevelopers merging code that doesn't pass lint, not enforcing linter\nrules, and debates about whether a useful linter is \"worth the time\".\n\nZulint is the linter framework we built for Zulip to create a\nreliable, lightning-fast linter experience to solve these problems.\nIt has the following features:\n\n- Integrates with `git` to only checks files in source control (not\n  automatically generated, untracked, or .gitignore files).\n- Runs the linters in parallel, so you only have to wait for the\n  slowest linter.  For Zulip, this is a ~4x performance improvement\n  over running our third-party linters in series.\n- Produduces easy-to-read, clear terminal output, with each\n  independent linter given its own color.\n- Can check just modified files, or even as a `pre-commit` hook, only\n  checking files that have changed (and only starting linters which\n  check files that have changed).\n- Handles all the annoying details of flushing stdout and managing\n  color codes.\n- Highly configurable.\n  - Integrate a third-party linter with just a couple lines of code.\n  - Every feature supports convenient include/exclude rules.\n  - Add custom lint rules with a powerful regular expression\n    framework.  E.g. in Zulip, we want all access to `Message` objects\n    in views code to be done via our `access_message_by_id` functions\n    (which do security checks to ensure the user the request is being\n    done on behalf of has access to the message), and that is enforced\n    in part by custom regular expression lint rules.  This system is\n    optimized Python: Zulip has a few hundred custom linter rules of\n    this type.\n  - Easily add custom options to check subsets of your codebase,\n    subsets of rules, etc.\n- Has a nice automated testing framework for custom lint rules, so you\n  can make sure your rules actually work.\n\nThis codebase has been in production use in Zulip for several years,\nbut only in 2019 was generalized for use by other projects.  Its API\nto be beta and may change (with notice in the release notes) if we\ndiscover a better API, and patches to further extend it for more use\ncases are encouraged.\n\n## Using zulint\n\nOnce a project is setup with zulint, you'll have a top-level linter\nscript with at least the following options:\n\n```\n$ ./example-lint --help\nusage: example-lint [-h] [--modified] [--verbose-timing] [--skip SKIP]\n                    [--only ONLY] [--list] [--list-groups] [--groups GROUPS]\n                    [--verbose] [--fix]\n                    [targets [targets ...]]\n\npositional arguments:\n  targets               Specify directories to check\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --modified, -m        Only check modified files\n  --verbose-timing, -vt\n                        Print verbose timing output\n  --skip SKIP           Specify linters to skip, eg: --skip=mypy,gitlint\n  --only ONLY           Specify linters to run, eg: --only=mypy,gitlint\n  --list, -l            List all the registered linters\n  --list-groups, -lg    List all the registered linter groups\n  --groups GROUPS, -g GROUPS\n                        Only run linter for languages in the group(s), e.g.:\n                        --groups=backend,frontend\n  --verbose, -v         Print verbose output where available\n  --fix                 Automatically fix problems where supported\n```\n\n\n**Example Output**\n\n```\n❯ ./tools/lint\njs        | Use channel module for AJAX calls at static/js/channel.js line 81:\njs        |                 const jqXHR = $.ajax(args);\npy        | avoid subject as a var at zerver/lib/email_mirror.py line 321:\npy        |     # strips RE and FWD from the subject\npy        | Please use access_message() to fetch Message objects at zerver/worker/queue_processors.py line 579:\npy        |         message = Message.objects.get(id=event['message_id'])\npy        | avoid subject as a var at zerver/lib/email_mirror.py line 327:\npy        | Use do_change_is_admin function rather than setting UserProfile's is_realm_admin attribute directly. at file.py line 28:\npy        |             user.is_realm_admin = True\npuppet    | /usr/lib/ruby/vendor_ruby/puppet/util.rb:461: warning: URI.escape is obsolete\nhbs       | Avoid using the `style=` attribute; we prefer styling in CSS files at static/templates/group_pms.hbs line 6:\nhbs       |         \u003cspan class=\"user_circle_fraction\" style=\"background:hsla(106, 74%, 44%, {{fraction_present}});\"\u003e\u003c/span\u003e\npep8      | tools/linter_lib/custom_check.py:499:13: E121 continuation line under-indented for hanging indent\npep8      | tools/linter_lib/custom_check.py:500:14: E131 continuation line unaligned for hanging indent\n```\n\nTo display `good_lines` and `bad_lines` along with errors, use `--verbose` option.\n\n```\n❯ ./tools/lint --verbose\npy        | Always pass update_fields when saving user_profile objects at zerver/lib/actions.py line 3160:\npy        |     user_profile.save()  # Can't use update_fields because of how the foreign key works.\npy        |   Good code: user_profile.save(update_fields=[\"pointer\"])\npy        |   Bad code:  user_profile.save()\npy        |\npy        | Missing whitespace after \":\" at zerver/tests/test_push_notifications.py line 535:\npy        |             'realm_counts': '[{\"id\":1,\"property\":\"invites_sent::day\",\"subgroup\":null,\"end_time\":574300800.0,\"value\":5,\"realm\":2}]',\npy        |   Good code: \"foo\": bar | \"some:string:with:colons\"\npy        |   Bad code:  \"foo\":bar  | \"foo\":1\npy        |\njs        | avoid subject in JS code at static/js/util.js line 279:\njs        |         message.topic = message.subject;\njs        |   Good code: topic_name\njs        |   Bad code:  subject=\"foo\" |  MAX_SUBJECT_LEN\njs        |\n```\n\n### pre-commit hook mode\n\nSee https://github.com/zulip/zulip/blob/master/tools/pre-commit for an\nexample pre-commit hook (Zulip's has some extra complexity because we\nuse Vagrant from our development environment, and want to be able to\nrun the hook from outside Vagrant).\n\n## Adding zulint to a codebase\n\nTODO: Make a pypi release\n\nAdd `zulint` to your codebase requirements file or just do:\n\n```\npip install zulint\n```\n\nWe recommend starting by copying [example-lint](./example-lint) into\nyour codebase and configuring it.  For a more advanced example, you\ncan look at [Zulip's\nlinter](https://github.com/zulip/zulip/blob/master/tools/lint).\n\n```bash\ncp -a example-lint /path/to/project/bin/lint\nchmod +x /path/to/project/bin/lint\ngit add /path/to/project/bin//lint\n```\n\n## Adding third-party linters\n\nFirst import the `LinterConfig` and initialize it with default arguments.\nYou can then use the `external_linter` method to register the linter.\n\neg:\n\n```python\n\nlinter_config.external_linter('eslint', ['node', 'node_modules/.bin/eslint',\n                                          '--quiet', '--cache', '--ext', '.js,.ts'], ['js', 'ts'],\n                              fix_arg='--fix',\n                              description=\"Standard JavaScript style and formatting linter\"\n                              \"(config: .eslintrc).\")\n```\n\nThe `external_linter` method takes the following arguments:\n\n* name: Name of the linter. It will be printer before the failed code to show\n        which linter is failing. | `REQUIRED`\n* command: The terminal command to execute your linter in \"shell-like syntax\".\n           You can use `shlex.split(\"SHELL COMMAND TO RUN LINTER\")` to split your\n           command. | `REQUIRED`\n* target_langs: The language files this linter should run on. Leave this argument\n                empty (= `[]`) to run on all the files. | `RECOMMENDED`\n* pass_targets: Pass target files (aka files in the specified `target_langs`) to\n                the linter command when executing it. Default: `True` | `OPTIONAL`\n* fix_arg: Some linters support fixing the errors automatically. Set it to the flag\n           used by the linter to fix the errors. | `OPTIONAL`\n* description: The description of your linter to be printed with `--list` argument. | `RECOMMENDED`\n\neg:\n\n```\n❯ ./tools/lint --list\nLinter          Description\ncss             Standard CSS style and formatting linter (config: .stylelintrc)\neslint          Standard JavaScript style and formatting linter(config: .eslintrc).\npuppet          Runs the puppet parser validator, checking for syntax errors.\npuppet-lint     Standard puppet linter(config: tools/linter_lib/exclude.py)\ntemplates       Custom linter checks whitespace formattingof HTML templates.\nopenapi         Validates our OpenAPI/Swagger API documentation(zerver/openapi/zulip.yaml)\nshellcheck      Standard shell script linter.\nmypy            Static type checker for Python (config: mypy.ini)\ntsc             TypeScript compiler (config: tsconfig.json)\nyarn-deduplicate Shares duplicate packages in yarn.lock\ngitlint         Checks commit messages for common formatting errors.(config: .gitlint)\nsemgrep-py      Syntactic Grep (semgrep) Code Search Tool (config: ./tools/semgrep.yml)\ncustom_py       Runs custom checks for python files (config: tools/linter_lib/custom_check.py)\ncustom_nonpy    Runs custom checks for non-python files (config: tools/linter_lib/custom_check.py)\npyflakes        Standard Python bug and code smell linter (config: tools/linter_lib/pyflakes.py)\npep8_1of2       Standard Python style linter on 50% of files (config: tools/linter_lib/pep8.py)\npep8_2of2       Standard Python style linter on other 50% of files (config: tools/linter_lib/pep8.py)\n```\n\nPlease make sure external linter (here `eslint`) is accessible via bash or in the\nvirtual env where this linter will run.\n\n## Writing custom rules\n\nYou can write your own custom rules for any language using regular expression\nin zulint. Doing it is very simple and there are tons of examples available\nin [Zulip's custom_check.py file](https://github.com/zulip/zulip/blob/master/tools/linter_lib/custom_check.py).\n\nIn the [above example](#adding-third-party-linters) you can add custom rules via `@linter_config.lint` decorator.\nFor eg:\n\n```python\n\nfrom zulint.custom_rules import RuleList\n\n@linter_config.lint\ndef check_custom_rules():\n    # type: () -\u003e int\n    \"\"\"Check trailing whitespace for specified files\"\"\"\n    trailing_whitespace_rule = RuleList(\n        langs=file_types,\n        rules=[{\n            'pattern': r'\\s+$',\n            'strip': '\\n',\n            'description': 'Fix trailing whitespace'\n        }]\n    )\n    failed = trailing_whitespace_rule.check(by_lang, verbose=args.verbose)\n    return 1 if failed else 0\n```\n\n#### RuleList\nA new custom rule is defined via the `RuleList` class. `RuleList` takes the following arguments:\n\n```python\nlangs                             # The languages this rule will run on. eg: ['py', 'bash']\nrules                             # List of custom `Rule`s to run. See definition of Rule below for more details.\nmax_length                        # Set a max length value for each line in the files. eg: 79\nlength_exclude                    # List of files to exclude from `max_length` limit. eg: [\"README\"]\nshebang_rules                     # List of shebang `Rule`s to run in `langs`. Default: []\nexclude_files_in                  # Directory to exclude from all rules. eg: 'app/' Default: None\nexclude_max_length_fns            # List of file names to exclude from max_length limit. eg: [test, example] Defautl: []\nexclude_max_length_line_patterns  # List of line patterns to exclude from max_length limit. eg: [\"`\\{\\{ api_url \\}\\}[^`]+`\"]\n```\n\n#### Rule\nA rule is a python dictionary containing regular expression,\nwhich will be run on each line in the `langs`' files specified in the `RuleList`.\nIt has a lot of additional features which you can use to run the pattern in\nspecific areas of your codebase.\n\nFind below all the keys that a `Rule` can have along with the\ntype of inputs they take.\n\n```python\nRule = TypedDict(\"Rule\", {\n    \"bad_lines\": List[str],\n    \"description\": str,\n    \"exclude\": Set[str],\n    \"exclude_line\": Set[Tuple[str, str]],\n    \"exclude_pattern\": str,\n    \"good_lines\": List[str],\n    \"include_only\": Set[str],\n    \"pattern\": str,\n    \"strip\": str,\n    \"strip_rule\": str,\n}, total=False)\n```\n\n* `pattern` is your regular expression to be run on all the eligible lines (i.e. lines which haven't been excluded by you).\n* `description` is the message that will be displayed if a pattern match is found.\n* `good_lines` are the list of sample lines which shouldn't match the pattern.\n* `bad_lines` are like `good_lines` but they match the pattern.\n\n**NOTE**: `patten` is run on `bad_lines` and `good_lines` and you can use them as an example to tell the developer\n      what is wrong with their code and how to fix it.\n\n* `exclude` List of folders to exclude.\n* `exclude_line` Tuple of filename and pattern to exclude from pattern check.\neg:\n\n```python\n('zerver/lib/actions.py', \"user_profile.save()  # Can't use update_fields because of how the foreign key works.\")`\n```\n\n* `exclude_pattern`: pattern to exclude from the matching patterns.\n* `include_only`: `pattern` is only run on these files.\n\n## Development Setup\n\nRun the following commands in a terminal to install zulint.\n```\ngit clone git@github.com:zulip/zulint.git\npython3 -m venv zulint_env\nsource zulint_env/bin/activate\npip install -e .\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzulip%2Fzulint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzulip%2Fzulint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzulip%2Fzulint/lists"}