├── apps ├── site │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ ├── favicon.ico │ │ │ └── logo.svg │ ├── blog │ │ ├── index.md │ │ ├── authors.yml │ │ └── tags.yml │ ├── src │ │ ├── components │ │ │ ├── Row.module.css │ │ │ ├── Alert.module.css │ │ │ ├── HomepageFeatures │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ │ ├── Button.module.css │ │ │ ├── Row.tsx │ │ │ ├── Badge.tsx │ │ │ ├── RuleBanner.module.css │ │ │ ├── Alert.tsx │ │ │ ├── Button.tsx │ │ │ └── RuleBanner.tsx │ │ ├── theme │ │ │ ├── types.ts │ │ │ └── MDXComponents.ts │ │ ├── utils.ts │ │ ├── pages │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── types.ts │ │ └── css │ │ │ └── custom.css │ ├── docs │ │ ├── rules │ │ │ ├── index.mdx │ │ │ ├── empty-file.mdx │ │ │ ├── no-return-try.mdx │ │ │ ├── case-convention.mdx │ │ │ ├── line-length.mdx │ │ │ ├── no-unresolved.mdx │ │ │ ├── duplicate-case.mdx │ │ │ ├── no-catch-return.mdx │ │ │ ├── RulesPage.module.css │ │ │ ├── avoid-as.mdx │ │ │ ├── returned-stack-reference.mdx │ │ │ ├── must-return-ref.mdx │ │ │ ├── homeless-try.mdx │ │ │ ├── unused-decls.mdx │ │ │ ├── useless-error-return.mdx │ │ │ ├── allocator-first-param.mdx │ │ │ ├── suppressed-errors.mdx │ │ │ └── no-print.mdx │ │ ├── getting-started.md │ │ ├── installation.md │ │ └── configuration │ │ │ ├── ignore.md │ │ │ └── index.md │ ├── tsconfig.json │ ├── .gitignore │ ├── sidebars.ts │ ├── README.md │ └── package.json └── vscode-extension │ ├── .vscodeignore │ ├── .gitignore │ ├── src │ ├── util.ts │ └── extension.ts │ ├── build.ts │ ├── tsconfig.json │ ├── LICENSE │ ├── README.md │ └── package.json ├── test ├── fixtures │ ├── simple │ │ ├── fail │ │ │ └── syntax_missing_semi.zig │ │ └── pass │ │ │ ├── fn_add.zig │ │ │ ├── container_error.zig │ │ │ ├── unresolved_import.zig │ │ │ ├── block_comptime.zig │ │ │ ├── struct_members.zig │ │ │ ├── top_level_struct.zig │ │ │ ├── fn_in_fn.zig │ │ │ ├── enum_members.zig │ │ │ ├── struct_tuple.zig │ │ │ ├── stmt_test.zig │ │ │ ├── foo.zig │ │ │ ├── block.zig │ │ │ ├── loops_for.zig │ │ │ ├── fibonacci.zig │ │ │ ├── fn_comptime.zig │ │ │ ├── cond_if.zig │ │ │ ├── writer_interface.zig │ │ │ └── loops_while.zig │ └── config │ │ └── zlint.json ├── snapshots │ ├── snapshot-coverage │ │ └── simple │ │ │ ├── fail.snap │ │ │ ├── pass.snap │ │ │ ├── fail │ │ │ └── syntax_missing_semi.zig.snap │ │ │ └── pass │ │ │ ├── unresolved_import.zig.snap │ │ │ ├── block_comptime.zig.snap │ │ │ ├── container_error.zig.snap │ │ │ ├── struct_members.zig.snap │ │ │ ├── fn_add.zig.snap │ │ │ └── top_level_struct.zig.snap │ └── semantic-coverage │ │ ├── ghostty.snap │ │ └── bun.snap ├── harness.zig ├── repos.json ├── test_e2e.zig ├── README.md └── semantic │ └── ecosystem_coverage.zig ├── tasks ├── create-ast-json.sh ├── lldb │ └── .lldbinit ├── submodules.sh ├── constants.zig ├── init.sh └── install.sh ├── zls.json ├── .prettierignore ├── docs ├── assets │ └── diagnostic-example.jpg └── rules │ ├── case-convention.md │ ├── empty-file.md │ ├── no-return-try.md │ ├── line-length.md │ ├── no-catch-return.md │ ├── no-unresolved.md │ ├── avoid-as.md │ ├── returned-stack-reference.md │ ├── must-return-ref.md │ ├── unused-decls.md │ ├── homeless-try.md │ ├── useless-error-return.md │ └── suppressed-errors.md ├── .prettierrc ├── src ├── linter │ ├── disable_directives.zig │ ├── rules │ │ └── snapshots │ │ │ ├── no-return-try.snap │ │ │ ├── empty-file.snap │ │ │ ├── no-unresolved.snap │ │ │ ├── unused-decls.snap │ │ │ ├── avoid-as.snap │ │ │ ├── must-return-ref.snap │ │ │ ├── no-catch-return.snap │ │ │ ├── no-print.snap │ │ │ ├── useless-error-return.snap │ │ │ ├── allocator-first-param.snap │ │ │ ├── homeless-try.snap │ │ │ ├── line-length.snap │ │ │ ├── suppressed-errors.snap │ │ │ ├── case-convention.snap │ │ │ └── duplicate-case.snap │ ├── rules.zig │ ├── config │ │ ├── Rules.zig │ │ ├── rules_config_rules.zig │ │ └── rules_config.zig │ ├── RuleSet.zig │ ├── disable_directives │ │ └── Comment.zig │ └── test │ │ └── lint_context_test.zig ├── reporter.zig ├── zig.zig ├── Semantic │ ├── ast.zig │ ├── test │ │ ├── modules_test.zig │ │ ├── util.zig │ │ ├── scopes_test.zig │ │ └── members_and_exports_test.zig │ ├── ModuleRecord.zig │ ├── Parse.zig │ └── builtins.zig ├── lint.zig ├── util │ ├── debug_only.zig │ ├── feature_flags.zig │ └── env.zig ├── root.zig ├── cli │ ├── test │ │ └── print_ast_test.zig │ └── print_command.zig ├── zig │ └── 0.14.1 │ │ ├── LICENSE │ │ └── primitives.zig ├── reporter │ ├── formatter.zig │ ├── StringWriter.zig │ └── formatters │ │ └── JSONFormatter.zig ├── source.zig ├── main.zig └── util.zig ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitignore ├── .vscode └── settings.json ├── .typos.toml ├── .github ├── workflows │ ├── pr.yaml │ ├── autofix.yaml │ ├── codecov.yaml │ ├── gh-pages.yaml │ ├── js.yaml │ └── release.yaml ├── labeler.yml └── ISSUE_TEMPLATE │ ├── rule_suggestion.md │ ├── bug_report.md │ └── feature_request.md ├── package.json ├── entitlements.dev.plist ├── LICENSE └── README.md /apps/site/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/simple/fail/syntax_missing_semi.zig: -------------------------------------------------------------------------------- 1 | const x = 1 2 | -------------------------------------------------------------------------------- /tasks/create-ast-json.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p tmp 3 | touch tmp/ast.json 4 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fn_add.zig: -------------------------------------------------------------------------------- 1 | fn add(x: i32, y: i32) i32 { 2 | return x + y; 3 | } 4 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/fail.snap: -------------------------------------------------------------------------------- 1 | Passed: 100% (1/1) 2 | Panics: 0% (0/1) 3 | 4 | -------------------------------------------------------------------------------- /zls.json: -------------------------------------------------------------------------------- 1 | { 2 | "enable_build_on_save": true, 3 | "build_on_save_step": "check" 4 | } 5 | -------------------------------------------------------------------------------- /apps/site/blog/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Blog 3 | --- 4 | 5 | Blog has not been implemented yet. 6 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/container_error.zig: -------------------------------------------------------------------------------- 1 | const Error = error{ 2 | Foo, 3 | Bar, 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/unresolved_import.zig: -------------------------------------------------------------------------------- 1 | const foo = @import("this/file/doesnotexist.zig"); 2 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass.snap: -------------------------------------------------------------------------------- 1 | Passed: 100% (18/18) 2 | Panics: 0% (0/18) 3 | 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | zlint.schema.json 2 | .zig-cache 3 | zig-out 4 | **/.docusaurus 5 | **/build 6 | .github 7 | -------------------------------------------------------------------------------- /apps/site/src/components/Row.module.css: -------------------------------------------------------------------------------- 1 | .flexRow { 2 | display: flex; 3 | gap: 0.5rem; 4 | } 5 | -------------------------------------------------------------------------------- /apps/vscode-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | build.ts 4 | bun.lock 5 | tsconfig.json 6 | -------------------------------------------------------------------------------- /test/fixtures/config/zlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "unsafe-undefined": "warn" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/site/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonIsaac/zlint/HEAD/apps/site/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/assets/diagnostic-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DonIsaac/zlint/HEAD/docs/assets/diagnostic-example.jpg -------------------------------------------------------------------------------- /apps/site/src/components/Alert.module.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | display: flex; 3 | align-items: center; 4 | gap: 0.25rem; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/block_comptime.zig: -------------------------------------------------------------------------------- 1 | const x = blk: { 2 | var y = 1; 3 | y += 2; 4 | break :blk y + 1; 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/struct_members.zig: -------------------------------------------------------------------------------- 1 | pub const Foo = struct{ 2 | a: u32, 3 | 4 | const B: u32 = 0; 5 | pub const C = 0; 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /apps/site/docs/rules/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rules 3 | hide_table_of_contents: true 4 | --- 5 | 6 | import RulesPage from './RulesPage' 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/site/src/theme/types.ts: -------------------------------------------------------------------------------- 1 | export type Size = 'sm' | 'md' | 'lg' 2 | export type Variant = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' 3 | -------------------------------------------------------------------------------- /src/linter/disable_directives.zig: -------------------------------------------------------------------------------- 1 | pub const Comment = @import("./disable_directives/Comment.zig"); 2 | pub const Parser = @import("./disable_directives/Parser.zig"); 3 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy 2 | 3 | USER root 4 | RUN apt-get update && \ 5 | apt-get install -y \ 6 | valgrind \ 7 | kcov 8 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/top_level_struct.zig: -------------------------------------------------------------------------------- 1 | a: u32, 2 | b: []const u8 = "", 3 | 4 | const Foo = @This(); 5 | 6 | pub fn new() Foo { 7 | return Foo{ .a = 0, .b = "hello" }; 8 | } 9 | -------------------------------------------------------------------------------- /src/reporter.zig: -------------------------------------------------------------------------------- 1 | const reporter = @import("./reporter/Reporter.zig"); 2 | pub const Reporter = reporter.Reporter; 3 | pub const Options = reporter.Options; 4 | 5 | pub const formatter = @import("./reporter/formatter.zig"); 6 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/fail/syntax_missing_semi.zig.snap: -------------------------------------------------------------------------------- 1 | 𝙭 syntax error: expected ';' after declaration 2 | ╭─[syntax_missing_semi.zig:1:11] 3 | 1 │ const x = 1 4 | · ─ 5 | ╰──── 6 | 7 | -------------------------------------------------------------------------------- /apps/site/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Getting Started 6 | 7 | ZLint works out of the box with sensible defaults. 8 | ```sh 9 | # in your repo's root directory 10 | zlint 11 | ``` 12 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fn_in_fn.zig: -------------------------------------------------------------------------------- 1 | pub fn foo(a: u32) u32 { 2 | const inner = struct { 3 | fn bar(b: u32) u32 { 4 | return b + 1; 5 | } 6 | }; 7 | 8 | return inner.bar(a); 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/enum_members.zig: -------------------------------------------------------------------------------- 1 | pub const Foo = enum { 2 | a, 3 | b, 4 | c, 5 | 6 | pub const Bar: u32 = 1; 7 | 8 | pub fn isNotA(self: Foo) bool { 9 | return self != Foo.a; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /apps/site/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/struct_tuple.zig: -------------------------------------------------------------------------------- 1 | const x = u64; 2 | const Foo = struct { u32, i32, x }; 3 | const Namespace = struct { 4 | const Member = u32; 5 | }; 6 | const Bar = struct { Namespace.Member }; 7 | 8 | const f: Foo = .{ 1, 2, 3 }; 9 | -------------------------------------------------------------------------------- /test/snapshots/semantic-coverage/ghostty.snap: -------------------------------------------------------------------------------- 1 | Passed: 99.42197% (516/519) 2 | Panics: 0% (0/519) 3 | 4 | src/cli/list_themes.zig: error.FullMismatch 5 | src/terminal/PageList.zig: error.FullMismatch 6 | src/terminal/Parser.zig: error.FullMismatch 7 | -------------------------------------------------------------------------------- /apps/site/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const withAOrAn = (str: string): string => { 2 | const a = /^[aeiou]/i.test(str) ? 'an' : 'a' 3 | return `${a} ${str}` 4 | } 5 | 6 | export const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1) 7 | -------------------------------------------------------------------------------- /apps/site/src/components/Button.module.css: -------------------------------------------------------------------------------- 1 | .buttonRow { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | gap: 0.5rem; 6 | } 7 | 8 | .flexButton { 9 | display: flex; 10 | align-items: center; 11 | gap: 0.25rem; 12 | } 13 | -------------------------------------------------------------------------------- /apps/site/src/theme/MDXComponents.ts: -------------------------------------------------------------------------------- 1 | import MDXComponents from '@theme-original/MDXComponents' 2 | import RuleBanner from '@site/src/components/RuleBanner' 3 | 4 | export default { 5 | // Re-use the default mapping 6 | ...MDXComponents, 7 | RuleBanner, 8 | } 9 | -------------------------------------------------------------------------------- /apps/site/blog/authors.yml: -------------------------------------------------------------------------------- 1 | don: 2 | name: Don Isaac 3 | title: ZLint Maintainer 4 | url: https://github.com/DonIsaac 5 | image_url: https://github.com/DonIsaac.png 6 | page: true 7 | socials: 8 | x: Don_dev_ 9 | linkedin: donisaac 10 | github: DonIsaac 11 | -------------------------------------------------------------------------------- /apps/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "strict": true, 7 | }, 8 | "exclude": [".docusaurus", "build"] 9 | } 10 | -------------------------------------------------------------------------------- /test/harness.zig: -------------------------------------------------------------------------------- 1 | pub const TestSuite = @import("harness/TestSuite.zig"); 2 | 3 | const runner = @import("harness/runner.zig"); 4 | pub const getRunner = runner.getRunner; 5 | pub const addTest = runner.addTest; 6 | pub const globalShutdown = runner.globalShutdown; 7 | pub const TestFile = runner.TestFile; 8 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/stmt_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn add(a: u32, b: u32) u32 { 4 | return a + b; 5 | } 6 | 7 | test add { 8 | try std.testing.expectEqual(2, add(1, 1)); 9 | } 10 | test "add twice" { 11 | try std.testing.expectEqual(3, add(1, add(1, 1))); 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | zig-out 3 | .zig-cache 4 | 5 | # test artifacts 6 | .coverage 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # os 12 | .DS_Store 13 | 14 | # editor 15 | *.swp 16 | *.swo 17 | .idea 18 | 19 | # temp & backup files 20 | tmp/** 21 | .tmp/ 22 | temp/ 23 | *.tmp 24 | 25 | vendor 26 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-return-try.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-return-try: This error union can be directly returned. 2 | ╭─[no-return-try.zig:2:18] 3 | 1 │ fn foo() !void { return; } 4 | 2 │ fn bar() !void { return try foo(); } 5 | · ────────── 6 | ╰──── 7 | help: Replace `return try` with `return` 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/foo.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | var bad: []const u8 = undefined; 4 | 5 | pub const good: ?[]const u8 = null; 6 | 7 | const Foo = struct { 8 | foo: u32 = undefined, 9 | const Bar: u32 = 1; 10 | fn baz(self: *Foo) void { 11 | std.debug.print("{d}\n", .{self.foo}); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "zig.zls.enableBuildOnSave": true, 3 | "zig.zls.buildOnSaveStep": "check", 4 | "zls.zigExePath": "./vendor/zig14/zig", 5 | "files.exclude": { 6 | ".coverage": true, 7 | ".zig-cache": true, 8 | "zig-out": true, 9 | }, 10 | "search.exclude": { 11 | "bun.lock": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/site/src/components/Row.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import styles from './Row.module.css' 3 | import React from 'react' 4 | 5 | type Props = React.ComponentProps<'div'> 6 | const FlexRow = React.forwardRef(function Row({ className, ...props }, ref) { 7 | return 8 | }) 9 | export default FlexRow 10 | -------------------------------------------------------------------------------- /tasks/lldb/.lldbinit: -------------------------------------------------------------------------------- 1 | 2 | command script import ./tasks/lldb/lldb_pretty_printers.py 3 | type category enable zig.lang 4 | type category enable zig.std 5 | 8 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-identifiers] 2 | # used to store multi-array slice to Node.Data. 3 | datas = "datas" 4 | 5 | [files] 6 | extend-exclude = [ 7 | "src/walk/glob_test.zig", 8 | "src/zig/0.14.1/tokenizer.zig", 9 | ] 10 | 11 | [type.zig] 12 | extend-glob = ["*.zig", "*.zog"] 13 | check-file = true 14 | 15 | [type.zig.extend-words] 16 | "hlep" = "hlep" # "--hlep" is checked for in options parsing, used as --help alias. 17 | -------------------------------------------------------------------------------- /apps/site/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # codegen'd files (that we don't want in git) 12 | # `just docs` 13 | static/lib-docs 14 | 15 | # Misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /tasks/submodules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run this with `just submodules` 3 | mkdir -p zig-out/repos 4 | repos=$(cat test/repos.json) 5 | 6 | while read repository 7 | do 8 | name=$(echo "$repository" | jq -r .name) 9 | repo_url=$(echo "$repository" | jq -r .repo_url) 10 | hash=$(echo "$repository" | jq -r .hash) 11 | 12 | just clone-submodule "zig-out/repos/$name" $repo_url $hash 13 | 14 | done < <(echo "$repos" | jq -c '.[]') 15 | -------------------------------------------------------------------------------- /apps/site/blog/tags.yml: -------------------------------------------------------------------------------- 1 | facebook: 2 | label: Facebook 3 | permalink: /facebook 4 | description: Facebook tag description 5 | 6 | hello: 7 | label: Hello 8 | permalink: /hello 9 | description: Hello tag description 10 | 11 | docusaurus: 12 | label: Docusaurus 13 | permalink: /docusaurus 14 | description: Docusaurus tag description 15 | 16 | hola: 17 | label: Hola 18 | permalink: /hola 19 | description: Hola tag description 20 | -------------------------------------------------------------------------------- /tasks/constants.zig: -------------------------------------------------------------------------------- 1 | /// Directory where generated rule documentation files are saved. 2 | pub const @"docs/rules" = "apps/site/docs/rules"; 3 | /// Directory where rule source code is located. 4 | pub const @"linter/rules" = "src/linter/rules"; 5 | /// File where generated RulesConfig.Rules struct is saved. 6 | pub const @"Rules.zig" = "src/linter/config/Rules.zig"; 7 | /// File where the JSON schema for zlint config is saved. 8 | pub const @"zlint.schema.json" = "zlint.schema.json"; 9 | -------------------------------------------------------------------------------- /apps/site/src/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { Variant } from '../theme/types' 2 | import clsx from 'clsx' 3 | 4 | interface Props extends React.ComponentProps<'span'> { 5 | /** 6 | * @default 'primary' 7 | */ 8 | variant?: Variant 9 | } 10 | export default function Badge({ variant = 'primary', children, className: classNameProp, ...props }: Props) { 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/site/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | gap: 0.5rem; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/block.zig: -------------------------------------------------------------------------------- 1 | fn empty() void { 2 | const x = {}; 3 | return x; 4 | } 5 | 6 | fn one() void { 7 | var y = 1; 8 | { 9 | y = 1; 10 | } 11 | } 12 | 13 | fn two() void { 14 | var z = 1; 15 | { 16 | const a = 1; 17 | z = a; 18 | } 19 | } 20 | 21 | fn many() void { 22 | var a = 1; 23 | { 24 | const b = 1; 25 | const c = 2; 26 | const d = 3; 27 | const e = 4; 28 | const f = 5; 29 | a += b + c + d + e + f; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/repos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "zig", 4 | "repo_url": "https://github.com/ziglang/zig.git", 5 | "hash": "e7b18a7ce69f30c85f21ec8ad6a70211abf5f24b" 6 | }, 7 | { 8 | "name": "bun", 9 | "repo_url": "https://github.com/oven-sh/bun.git", 10 | "hash": "499eac0d4929e1abea298161abf07c13d8c57237" 11 | }, 12 | { 13 | "name": "ghostty", 14 | "repo_url": "https://github.com/ghostty-org/ghostty", 15 | "hash": "1c0677faabf79b5dbaf96eef058e0a16983d0538" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Label PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | pr: 12 | if: github.repository == 'DonIsaac/zlint' 13 | name: Label PR 14 | permissions: 15 | contents: read 16 | pull-requests: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 20 | - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zlint", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Don Isaac", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/DonIsaac/zlint" 9 | }, 10 | "categories": [ 11 | "Linters" 12 | ], 13 | "packageManager": "bun@1.3.0", 14 | "workspaces": [ 15 | "apps/*" 16 | ], 17 | "scripts": { 18 | "fmt": "prettier -u --write .", 19 | "fmt:some": "prettier -u --write" 20 | }, 21 | "devDependencies": { 22 | "@types/bun": "latest", 23 | "prettier": "^3.6.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/vscode-extension/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | *.vsix 9 | 10 | # code coverage 11 | coverage 12 | *.lcov 13 | 14 | # logs 15 | logs 16 | _.log 17 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 18 | 19 | # dotenv environment variable files 20 | .env 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .env.local 25 | 26 | # caches 27 | .eslintcache 28 | .cache 29 | *.tsbuildinfo 30 | 31 | # IntelliJ based IDEs 32 | .idea 33 | 34 | # Finder (MacOS) folder config 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/util.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream' 2 | import assert from 'node:assert' 3 | 4 | export function readableStreamToString(stream: Readable) { 5 | assert(stream && stream.readable) 6 | const { promise, resolve } = Promise.withResolvers() 7 | 8 | const chunks: Buffer[] = [] 9 | const finalize = () => Buffer.concat(chunks).toString('utf8') 10 | stream 11 | .on('data', (chunk) => chunks.push(chunk)) 12 | .on('end', () => resolve(finalize())) 13 | .on('close', () => resolve(finalize())) 14 | 15 | return promise 16 | } 17 | -------------------------------------------------------------------------------- /src/zig.zig: -------------------------------------------------------------------------------- 1 | // NOTE: 2 | // This is a copy/paste of pieces from `std.zig` from v0.14.1 3 | // MIT license. See `src/zig/0.14.1/LICENSE` for details. 4 | pub const @"0.14.1" = struct { 5 | pub const Ast = @import("zig/0.14.1/Ast.zig"); 6 | pub const Parse = @import("zig/0.14.1/Parse.zig"); 7 | pub const Token = @import("zig/0.14.1/tokenizer.zig").Token; 8 | pub const Tokenizer = @import("zig/0.14.1/tokenizer.zig").Tokenizer; 9 | pub const primitives = @import("zig/0.14.1/primitives.zig"); 10 | pub const string_literal = @import("zig/0.14.1/string_literal.zig"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/empty-file.snap: -------------------------------------------------------------------------------- 1 | 𝙭 empty-file: empty-file.zig has zero bytes 2 | 3 | 4 | 𝙭 empty-file: empty-file.zig contains only whitespace 5 | 6 | 7 | 𝙭 empty-file: empty-file.zig contains only whitespace 8 | 9 | 10 | 𝙭 empty-file: empty-file.zig contains only whitespace 11 | 12 | 13 | 𝙭 empty-file: empty-file.zig only contains comments 14 | 15 | 16 | 𝙭 empty-file: empty-file.zig only contains comments 17 | 18 | 19 | 𝙭 empty-file: empty-file.zig only contains comments 20 | 21 | 22 | 𝙭 empty-file: empty-file.zig only contains comments 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-unresolved.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-unresolved: Unresolved import to 'does-not-exist.zig' 2 | ╭─[src/no-unresolved.zig:1:19] 3 | 1 │ const x = @import("does-not-exist.zig"); 4 | · ──────────┬────────── 5 | · ╰── file 'does-not-exist.zig' does not exist 6 | ╰──── 7 | 8 | 𝙭 no-unresolved: Unresolved import to directory './walk' 9 | ╭─[src/no-unresolved.zig:1:19] 10 | 1 │ const x = @import("./walk"); 11 | · ────┬──── 12 | · ╰── './walk' is a folder 13 | ╰──── 14 | 15 | -------------------------------------------------------------------------------- /apps/site/src/components/RuleBanner.module.css: -------------------------------------------------------------------------------- 1 | .ruleBanner { 2 | margin-top: calc(var(--ifm-h1-vertical-rhythm-bottom) * -0.5rem); 3 | } 4 | 5 | .severityBanner { 6 | padding: 0.5rem; 7 | margin: var(--ifm-spacing-horizontal) 0; 8 | 9 | &.warning { 10 | --ifm-alert-color-foreground: var(--ifm-color-warning); 11 | --ifm-alert-color: var(--ifm-color-warning); 12 | /* background-color: var(--ifm-color-warning); */ 13 | } 14 | &.error { 15 | background-color: var(--ifm-color-warning); 16 | } 17 | &.notice { 18 | background-color: var(--ifm-color-info); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/unused-decls.snap: -------------------------------------------------------------------------------- 1 | 𝙭 unused-decls: variable 'x' is declared but never used. 2 | ╭─[unused-decls.zig:1:7] 3 | 1 │ const x = 1; 4 | · ─ 5 | ╰──── 6 | 7 | 𝙭 unused-decls: variable 'Allocator' is declared but never used. 8 | ╭─[unused-decls.zig:1:35] 9 | 1 │ const std = @import("std"); const Allocator = std.mem.Allocator; 10 | · ───────── 11 | ╰──── 12 | 13 | 𝙭 unused-decls: variable 'x' is declared but never used. 14 | ╭─[unused-decls.zig:1:14] 15 | 1 │ extern const x: usize; 16 | · ─ 17 | ╰──── 18 | 19 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | A-cli: 2 | - changed-files: 3 | - any-glob-to-any-file: ["src/cli/**"] 4 | 5 | A-linter: 6 | - changed-files: 7 | - any-glob-to-any-file: ["src/linter/**", "src/linter.zig"] 8 | 9 | A-semantic: 10 | - changed-files: 11 | - any-glob-to-any-file: ["src/Semantic/**", "src/Semantic.zig"] 12 | 13 | A-reporter: 14 | - changed-files: 15 | - any-glob-to-any-file: ["src/reporter/**", "src/reporter.zig"] 16 | 17 | C-ci: 18 | - changed-files: 19 | - any-glob-to-any-file: [".github/workflows/**"] 20 | 21 | C-test: 22 | - changed-files: 23 | - any-glob-to-any-file: ["test/**"] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rule_suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rule Suggestion 3 | about: Suggest a new lint rule 4 | title: 'rule: `some-rule-name`' 5 | labels: A-linter, C-rule 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | Please describe what syntax or coding patterns this rule prohibits. Make sure you describe _why_ its bad. 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule 16 | ```zig 17 | // put examples of code that should produce any violations 18 | ``` 19 | 20 | Examples of **correct** code for this rule 21 | ```zig 22 | // Put examples of code that should _not_ produce any violations 23 | ``` 24 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/avoid-as.snap: -------------------------------------------------------------------------------- 1 | 𝙭 avoid-as: Prefer using type annotations over @as(). 2 | ╭─[avoid-as.zig:1:11] 3 | 1 │ const x = @as(u32, 1); 4 | · ─── 5 | ╰──── 6 | help: Use a type annotation instead. 7 | 8 | 𝙭 avoid-as: Unnecessary use of @as(). 9 | ╭─[avoid-as.zig:1:16] 10 | 1 │ const x: u32 = @as(u32, 1); 11 | · ─── 12 | ╰──── 13 | help: Remove the @as() call. 14 | 15 | 𝙭 avoid-as: Unnecessary use of @as(). 16 | ╭─[avoid-as.zig:1:31] 17 | 1 │ const Foo = struct { x: u32 = @as(u32, 1) }; 18 | · ─── 19 | ╰──── 20 | help: Remove the @as() call. 21 | 22 | -------------------------------------------------------------------------------- /test/test_e2e.zig: -------------------------------------------------------------------------------- 1 | const test_runner = @import("harness/runner.zig"); 2 | 3 | 4 | // Allows recovery from panics in test cases. Errors get saved to that suite's 5 | // snapshot file, and testing continues. 6 | pub const panic = @import("recover").panic; 7 | 8 | // test suites 9 | const semantic_coverage = @import("semantic/ecosystem_coverage.zig"); 10 | const snapshot_coverage = @import("semantic/snapshot_coverage.zig"); 11 | 12 | pub fn main() !void { 13 | const runner = test_runner.getRunner(); 14 | defer runner.deinit(); 15 | try runner 16 | .addTest(semantic_coverage.SUITE) 17 | .addTest(snapshot_coverage.SUITE) 18 | .runAll(); 19 | } 20 | -------------------------------------------------------------------------------- /docs/rules/case-convention.md: -------------------------------------------------------------------------------- 1 | # `case-convention` 2 | 3 | > Category: style 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Checks for function names that are not in camel case. Specially coming from Rust, 10 | some people may be used to use snake_case for their functions, which can lead to 11 | inconsistencies in the code 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule: 16 | 17 | ```zig 18 | fn this_one_is_in_snake_case() void {} 19 | ``` 20 | 21 | Examples of **correct** code for this rule: 22 | 23 | ```zig 24 | fn thisFunctionIsInCamelCase() void {} 25 | ``` 26 | 27 | ## Configuration 28 | 29 | This rule has no configuration. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: C-bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version** 11 | Please provide the semver version or git commit hash of the version of zlint you're using 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /apps/site/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { Variant } from '../theme/types' 3 | import styles from './Alert.module.css' 4 | 5 | type AlertProps = React.ComponentProps<'div'> & { 6 | /** 7 | * @default 'info' 8 | */ 9 | variant?: Variant 10 | my?: 'sm' | 'md' | 'lg' | null | undefined 11 | } 12 | export default function Alert({ variant = 'info', className, my = 'md', ...props }: AlertProps) { 13 | return ( 14 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: C-enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Semantic/ast.zig: -------------------------------------------------------------------------------- 1 | //! Re-exports of data structures used in Zig's AST. 2 | //! 3 | //! Also includes additional types used in other semantic components. 4 | const std = @import("std"); 5 | const NominalId = @import("util").NominalId; 6 | const zig = @import("../zig.zig").@"0.14.1"; 7 | 8 | pub const Ast = zig.Ast; 9 | pub const Node = Ast.Node; 10 | 11 | /// The struct used in AST tokens SOA is not pub so we hack it in here. 12 | pub const RawToken = struct { 13 | tag: zig.Token.Tag, 14 | start: Ast.ByteOffset, 15 | pub const Tag = zig.Token.Tag; 16 | }; 17 | 18 | pub const TokenIndex = Ast.TokenIndex; 19 | pub const NodeIndex = Node.Index; 20 | pub const MaybeTokenId = NominalId(Ast.TokenIndex).Optional; 21 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # E2E Tests 2 | 3 | Tl;Dr: 4 | 5 | ```sh 6 | just submodules 7 | just e2e 8 | ``` 9 | 10 | This folder contains E2E & integration tests. They mostly focus on semantic 11 | analysis. They can be run with `just e2e`. 12 | 13 | ## Structure 14 | 15 | These tests are actually compiled as a binary, not as a test suite (e.g. `zig 16 | build test`). `zlint` is linked as a module to this binary. Anything that is 17 | marked `pub` in `src/root.ts` is importiable via `@import("zlint")` within a 18 | test file. 19 | 20 | Several test suites run checks on a set of popular zig codebases. These repos 21 | are configured in `repos.json`. You must run `just submodules` to clone them 22 | before running e2e tests. 23 | 24 | -------------------------------------------------------------------------------- /src/lint.zig: -------------------------------------------------------------------------------- 1 | pub const Linter = @import("linter/linter.zig").Linter; 2 | pub const LintService = @import("linter/LintService.zig"); 3 | pub const Config = @import("linter/Config.zig"); 4 | pub const Rule = @import("linter/rule.zig").Rule; 5 | pub const rules = @import("linter/rules.zig"); 6 | pub const Fix = @import("linter/fix.zig").Fix; 7 | 8 | test { 9 | const std = @import("std"); 10 | 11 | // Ensure intellisense. Especially important when authoring a new rule. 12 | std.testing.refAllDecls(@This()); 13 | std.testing.refAllDeclsRecursive(@import("linter/rules.zig")); 14 | 15 | // Test suites 16 | _ = @import("linter/test/disabling_rules_test.zig"); 17 | _ = @import("linter/test/fix_test.zig"); 18 | } 19 | -------------------------------------------------------------------------------- /apps/vscode-extension/build.ts: -------------------------------------------------------------------------------- 1 | var isProd = process.env.NODE_ENV === "production"; 2 | for (const arg of process.argv) { 3 | switch (arg) { 4 | case "--production": 5 | case "-p": 6 | isProd = true; 7 | break; 8 | } 9 | } 10 | 11 | const res = await Bun.build({ 12 | entrypoints: ["src/extension.ts"], 13 | outdir: "dist", 14 | external: ["vscode"], 15 | target: "node", 16 | format: "cjs", 17 | sourcemap: "linked", 18 | minify: isProd && { 19 | whitespace: false, 20 | syntax: true, 21 | identifiers: true 22 | } 23 | }); 24 | for (const log of res.logs) { 25 | console.log(`[${log.level}] ${log.message}`); 26 | } 27 | if (!res.success) process.exit(1); 28 | -------------------------------------------------------------------------------- /apps/vscode-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /entitlements.dev.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | com.apple.security.get-task-allow 15 | 16 | com.apple.security.cs.debugger 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yaml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["main"] 7 | 8 | permissions: 9 | contents: read 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | 14 | env: 15 | ZIG_VERSION: 0.14.1 16 | 17 | jobs: 18 | autofix: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: mlugg/setup-zig@v2 23 | with: 24 | version: ${{ env.ZIG_VERSION }} 25 | 26 | - name: Fix typos 27 | uses: crate-ci/typos@v1.26.8 28 | with: 29 | write_changes: true 30 | - run: zig fmt src test/harness tasks build.zig build.zig.zon 31 | 32 | - run: rm -f ./typos 33 | 34 | - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a 35 | -------------------------------------------------------------------------------- /apps/site/src/types.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'arktype' 2 | 3 | export namespace Rule { 4 | export const Severity = type("'off' | 'warning' | 'err' | 'notice'") 5 | export type Severity = typeof Severity.infer 6 | 7 | /** `FixMeta` in `src/linter/fix.zig` */ 8 | export const FixMeta = type({ 9 | kind: '"fix" | "suggestion" | "none"', 10 | dangerous: 'boolean', 11 | }) 12 | export type FixMeta = typeof FixMeta.infer 13 | 14 | export const Category = type( 15 | "'compiler' | 'correctness' | 'suspicious' | 'restriction' | 'pedantic' | 'style' | 'nursery'" 16 | ) 17 | export type Category = typeof Category.infer 18 | 19 | export const Meta = type({ 20 | name: 'string', 21 | category: Category, 22 | fix: FixMeta, 23 | default: Severity, 24 | }) 25 | export type Meta = typeof Meta.infer; 26 | } 27 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/must-return-ref.snap: -------------------------------------------------------------------------------- 1 | 𝙭 must-return-ref: Members of type `ArenaAllocator` must be passed by reference 2 | ╭─[must-return-ref.zig:7:12] 3 | 6 │ pub fn getArena(self: *Foo) ArenaAllocator { 4 | 7 │ return self.arena; 5 | · ─────┬───── 6 | · ╰── This is a copy, not a move. 7 | 8 │ } 8 | ╰──── 9 | help: This type records its allocation size, so mutating a copy can result in a memory leak. 10 | 11 | 𝙭 must-return-ref: Members of type `ArrayList` must be passed by reference 12 | ╭─[must-return-ref.zig:5:12] 13 | 4 │ } else { 14 | 5 │ return self.list; 15 | · ────┬──── 16 | · ╰── This is a copy, not a move. 17 | 6 │ } 18 | ╰──── 19 | help: This type records its allocation size, so mutating a copy can result in a memory leak. 20 | 21 | -------------------------------------------------------------------------------- /src/util/debug_only.zig: -------------------------------------------------------------------------------- 1 | const IS_DEBUG = @import("../util.zig").IS_DEBUG; 2 | 3 | /// Wraps a type so that it is eliminated in release builds. Useful for 4 | /// eliminating container members that are used only for debug checks. 5 | /// 6 | /// ## Example 7 | /// ```zig 8 | /// const Foo = struct { 9 | /// debug_check: DebugOnly(u32) = debugOnly(u32, 42), 10 | /// }; 11 | /// ``` 12 | pub fn DebugOnly(comptime T: type) type { 13 | return comptime if (IS_DEBUG) T else void; 14 | } 15 | 16 | /// Eliminates a value in release builds. In debug builds, this is an identity 17 | /// function. 18 | /// 19 | /// ## Example 20 | /// ```zig 21 | /// const Foo = struct { 22 | /// debug_check: DebugOnly(u32) = debugOnly(u32, 42), 23 | /// }; 24 | /// ``` 25 | pub inline fn debugOnly(T: type, value: T) DebugOnly(T) { 26 | return if (IS_DEBUG) value; 27 | } 28 | -------------------------------------------------------------------------------- /docs/rules/empty-file.md: -------------------------------------------------------------------------------- 1 | # `empty-file` 2 | 3 | > Category: style 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | This rule checks for empty .zig files in the project. 10 | A file should be deemed empty if it has no content (zero bytes) or only comments and whitespace characters, 11 | as defined by the standard library in [`std.ascii.whitespace`](https://ziglang.org/documentation/master/std/#std.ascii.whitespace). 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule: 16 | 17 | ```zig 18 | // an "empty" file is actually a file without meaningful code: just comments (doc or normal) or whitespace 19 | ``` 20 | 21 | Examples of **correct** code for this rule: 22 | 23 | ```zig 24 | fn exampleFunction() void { 25 | } 26 | ``` 27 | 28 | ## Configuration 29 | 30 | This rule has no configuration. 31 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const zig = @import("zig.zig").@"0.14.1"; 4 | pub const Semantic = @import("Semantic.zig"); 5 | pub const Source = @import("source.zig").Source; 6 | 7 | pub const report = @import("reporter.zig"); 8 | 9 | pub const lint = @import("lint.zig"); 10 | 11 | /// Internal. Exported for codegen. 12 | pub const json = @import("json.zig"); 13 | 14 | pub const printer = struct { 15 | pub const Printer = @import("printer/Printer.zig"); 16 | pub const SemanticPrinter = @import("printer/SemanticPrinter.zig"); 17 | pub const AstPrinter = @import("printer/AstPrinter.zig"); 18 | }; 19 | 20 | pub const walk = @import("visit/walk.zig"); 21 | 22 | test { 23 | std.testing.refAllDecls(@import("util")); 24 | std.testing.refAllDeclsRecursive(printer); 25 | std.testing.refAllDeclsRecursive(zig); 26 | } 27 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-catch-return.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-catch-return: Caught error is immediately returned 2 | ╭─[no-catch-return.zig:3:19] 3 | 2 │ fn foo() !void { 4 | 3 │ bar() catch |e| return e; 5 | · ──────── 6 | 4 │ } 7 | ╰──── 8 | help: Use a `try` statement to return unhandled errors. 9 | 10 | 𝙭 no-catch-return: Caught error is immediately returned 11 | ╭─[no-catch-return.zig:4:5] 12 | 3 │ bar() catch |e| { 13 | 4 │ return e; 14 | · ──────── 15 | 5 │ }; 16 | ╰──── 17 | help: Use a `try` statement to return unhandled errors. 18 | 19 | 𝙭 no-catch-return: Caught error is immediately returned 20 | ╭─[no-catch-return.zig:5:5] 21 | 4 │ // comments won't save you 22 | 5 │ return e; 23 | · ──────── 24 | 6 │ }; 25 | ╰──── 26 | help: Use a `try` statement to return unhandled errors. 27 | 28 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/loops_for.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn forOverArr() void { 4 | const arr = [_]u32{ 1, 2, 3, 4, 5 }; 5 | 6 | for (arr) |i| { 7 | const power_of_two = 1 << i; 8 | std.debug.print("2^{} = {}\n", .{ i, power_of_two }); 9 | } 10 | } 11 | 12 | // Copied from https://ziglang.org/documentation/master/#toc-Multidimensional-Arrays 13 | fn forWithMultiBindingClosure() void { 14 | const mat4x4 = [4][4]f32{ 15 | [_]f32{ 1.0, 0.0, 0.0, 0.0 }, 16 | [_]f32{ 0.0, 1.0, 0.0, 1.0 }, 17 | [_]f32{ 0.0, 0.0, 1.0, 0.0 }, 18 | [_]f32{ 0.0, 0.0, 0.0, 1.0 }, 19 | }; 20 | for (mat4x4, 0..) |row, row_index| { 21 | for (row, 0..) |cell, column_index| { 22 | if (row_index == column_index) { 23 | try std.testing.expect(cell == 1.0); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fibonacci.zig: -------------------------------------------------------------------------------- 1 | // source: https://stackoverflow.com/q/70761612 2 | const std = @import("std"); 3 | const Managed = std.math.big.int.Managed; 4 | 5 | pub fn main() anyerror!void { 6 | const allocator = std.heap.c_allocator; 7 | 8 | var a = try Managed.initSet(allocator, 0); 9 | defer a.deinit(); 10 | var b = try Managed.initSet(allocator, 1); 11 | defer b.deinit(); 12 | var i: u128 = 0; 13 | 14 | var c = try Managed.init(allocator); 15 | defer c.deinit(); 16 | 17 | while (i < 1000000) : (i += 1) { 18 | try c.add(a.toConst(), b.toConst()); 19 | 20 | a.swap(&b); // This is more efficient than using Clone! 21 | b.swap(&c); // This reduced memory leak. 22 | } 23 | 24 | const as = try a.toString(allocator, 10, std.fmt.Case.lower); 25 | defer allocator.free(as); 26 | std.log.info("Fib: {s}", .{as}); 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/fn_comptime.zig: -------------------------------------------------------------------------------- 1 | fn Box(T: type) type { 2 | return struct { 3 | inner: *T, 4 | const Self = @This(); 5 | 6 | pub fn deref(self: *Self) *T { 7 | return self.inner; 8 | } 9 | }; 10 | } 11 | 12 | fn FixedIntArray(comptime size: usize) type { 13 | // since this function has comptime arguments, this scope is comptime, so 14 | // variables can be typed to comptime_int 15 | var l = 0; 16 | l += 1; 17 | if (l > 5) { 18 | @compileError("bad l"); 19 | } 20 | return struct { 21 | inner: [size]u32, 22 | }; 23 | } 24 | 25 | fn add(comptime a: isize, b: usize) usize { 26 | comptime { 27 | const c = 3; 28 | if (a < c) { 29 | @compileError("a cannot be less than 3"); 30 | } 31 | } 32 | const a2: usize = @intCast(a); 33 | return a2 + b; 34 | } 35 | -------------------------------------------------------------------------------- /src/util/feature_flags.zig: -------------------------------------------------------------------------------- 1 | //! Feature flags guard work-in-progress features until they are ready to be 2 | //! shipped. 3 | //! 4 | //! All flags are comptime-known. This may change in the future. 5 | 6 | /// Enable language server features. 7 | /// 8 | /// Not yet implemented. 9 | pub const lsp: bool = false; 10 | 11 | /// Wraps `T` so that it becomes `void` if `feature_flag` is not enabled. 12 | pub fn IfEnabled(feature_flag: anytype, T: type) type { 13 | const flag: bool = @field(@This(), @tagName(feature_flag)); 14 | return if (flag) T else void; 15 | } 16 | 17 | /// Returns `value` if `feature_flag` is enabled, void otherwise. 18 | pub fn ifEnabled(feature_flag: anytype, T: type, value: T) IfEnabled(feature_flag, T) { 19 | const flag: bool = @field(@This(), @tagName(feature_flag)); 20 | if (flag) { 21 | return value; 22 | } else { 23 | return {}; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/site/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Z 5 | 6 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/no-print.snap: -------------------------------------------------------------------------------- 1 | 𝙭 no-print: Using `std.debug.print` is not allowed. 2 | ╭─[no-print.zig:3:13] 3 | 2 │ fn foo() void { 4 | 3 │ std.debug.print("This should not be here: {d}\n", .{42}); 5 | · ───── 6 | 4 │ } 7 | ╰──── 8 | help: End-users don't want to see debug logs. Use `std.log` instead. 9 | 10 | 𝙭 no-print: Using `std.debug.print` is not allowed. 11 | ╭─[no-print.zig:4:9] 12 | 3 │ fn foo() void { 13 | 4 │ debug.print("This should not be here: {d}\n", .{42}); 14 | · ───── 15 | 5 │ } 16 | ╰──── 17 | help: End-users don't want to see debug logs. Use `std.log` instead. 18 | 19 | 𝙭 no-print: Using `std.debug.print` is not allowed. 20 | ╭─[no-print.zig:4:3] 21 | 3 │ fn foo() void { 22 | 4 │ print("This should not be here: {d}\n", .{42}); 23 | · ───── 24 | 5 │ } 25 | ╰──── 26 | help: End-users don't want to see debug logs. Use `std.log` instead. 27 | 28 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/cond_if.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | // comptime if expression 5 | const msg = if (builtin.mode == .Debug) 6 | "Debug mode" 7 | else 8 | "Release mode"; 9 | 10 | fn simpleIf() void { 11 | var i: u32 = 1; 12 | 13 | // lonely if 14 | if (i % 2 == 0) { 15 | std.debug.print("Even\n", .{}); 16 | } 17 | 18 | if (i > 5) { 19 | const pow = i * i; // should be in new scope 20 | i = pow; 21 | } else { 22 | i = 0; 23 | } 24 | } 25 | 26 | fn comptimeIf() void { 27 | comptime if (builtin.os.tag == .windows) { 28 | @compileError("Windows is not supported"); 29 | }; 30 | } 31 | 32 | fn ifWithPayload() void { 33 | const res: anyerror!u32 = 1; 34 | if (res) |x| { 35 | std.debug.print("value: {d}\n", .{x}); 36 | } else |err| { 37 | std.debug.print("error: {s}\n", .{err}); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/rules/no-return-try.md: -------------------------------------------------------------------------------- 1 | # `no-return-try` 2 | 3 | > Category: pedantic 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Disallows `return`ing a `try` expression. 10 | 11 | Returning an error union directly has the same exact semantics as `try`ing 12 | it and then returning the result. 13 | 14 | ## Examples 15 | 16 | Examples of **incorrect** code for this rule: 17 | 18 | ```zig 19 | const std = @import("std"); 20 | 21 | fn foo() !void { 22 | return error.OutOfMemory; 23 | } 24 | 25 | fn bar() !void { 26 | return try foo(); 27 | } 28 | ``` 29 | 30 | Examples of **correct** code for this rule: 31 | 32 | ```zig 33 | const std = @import("std"); 34 | 35 | fn foo() !void { 36 | return error.OutOfMemory; 37 | } 38 | 39 | fn bar() !void { 40 | errdefer { 41 | std.debug.print("this still gets printed.\n", .{}); 42 | } 43 | 44 | return foo(); 45 | } 46 | ``` 47 | 48 | ## Configuration 49 | 50 | This rule has no configuration. 51 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yaml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | env: 11 | ZIG_VERSION: 0.14.1 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | codecov: 19 | name: Collect and upload test coverage 20 | runs-on: "ubuntu-22.04" 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install kcov 24 | run: | 25 | sudo apt-get update 26 | sudo apt-get install -y kcov 27 | - uses: extractions/setup-just@v2 28 | - uses: mlugg/setup-zig@v2 29 | with: 30 | version: ${{ env.ZIG_VERSION }} 31 | 32 | - run: just submodules 33 | - run: zig build 34 | - run: just coverage 35 | - uses: codecov/codecov-action@v4 36 | with: 37 | verbose: true 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /apps/site/docs/rules/empty-file.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"empty-file","category":"style","default":"warning","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `empty-file` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | This rule checks for empty .zig files in the project. 12 | A file should be deemed empty if it has no content (zero bytes) or only comments and whitespace characters, 13 | as defined by the standard library in [`std.ascii.whitespace`](https://ziglang.org/documentation/master/std/#std.ascii.whitespace). 14 | 15 | ## Examples 16 | 17 | Examples of **incorrect** code for this rule: 18 | 19 | ```zig 20 | // an "empty" file is actually a file without meaningful code: just comments (doc or normal) or whitespace 21 | ``` 22 | 23 | Examples of **correct** code for this rule: 24 | 25 | ```zig 26 | fn exampleFunction() void { 27 | } 28 | ``` 29 | 30 | ## Configuration 31 | 32 | This rule has no configuration. 33 | -------------------------------------------------------------------------------- /apps/site/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs' 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | docs: [{ type: 'autogenerated', dirName: '.' }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | } 32 | 33 | export default sidebars 34 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/writer_interface.zig: -------------------------------------------------------------------------------- 1 | // source: https://www.openmymind.net/Zig-Interfaces/ 2 | const Writer = struct { 3 | // These two fields are the same as before 4 | ptr: *anyopaque, 5 | writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void, 6 | 7 | // This is new 8 | fn init(ptr: anytype) Writer { 9 | const T = @TypeOf(ptr); 10 | const ptr_info = @typeInfo(T); 11 | 12 | const gen = struct { 13 | pub fn writeAll(pointer: *anyopaque, data: []const u8) anyerror!void { 14 | const self: T = @ptrCast(@alignCast(pointer)); 15 | return ptr_info.Pointer.child.writeAll(self, data); 16 | } 17 | }; 18 | 19 | return .{ 20 | .ptr = ptr, 21 | .writeAllFn = gen.writeAll, 22 | }; 23 | } 24 | 25 | // This is the same as before 26 | pub fn writeAll(self: Writer, data: []const u8) !void { 27 | return self.writeAllFn(self.ptr, data); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tasks/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # Exit on error 4 | 5 | function try_install() { 6 | package_manager=$1 7 | package_name=$2 8 | binary_name=${3:-$2} 9 | 10 | if ! which $binary_name > /dev/null; then 11 | echo "Installing $package_name" 12 | brew install $package_name 13 | else 14 | echo "$package_name is already installed, skipping" 15 | fi 16 | } 17 | 18 | 19 | if which brew > /dev/null; then 20 | try_install brew entr 21 | try_install brew typos-cli typos 22 | try_install brew kcov 23 | try_install brew bun 24 | elif which apt-get > /dev/null; then 25 | try_install apt-get entr 26 | try_install apt-get typos-cli typos 27 | try_install apt-get kcov 28 | if ! which bun > /dev/null; then 29 | echo "Bun is not installed. Please follow steps on https://bun.sh" 30 | fi 31 | else 32 | echo "No supported package manager found. Please install dependencies manually and/or update this script to support your package manager." 33 | exit 1 34 | fi 35 | -------------------------------------------------------------------------------- /apps/site/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | > NOTE: Docusarus uses [infirma](https://infima.dev/docs/getting-started/introduction) css for styling. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | yarn 11 | ``` 12 | 13 | ## Local Development 14 | 15 | ```bash 16 | yarn start 17 | ``` 18 | 19 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 20 | 21 | ## Build 22 | 23 | ```bash 24 | yarn build 25 | ``` 26 | 27 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 28 | 29 | ## Deployment 30 | 31 | Using SSH: 32 | 33 | ```bash 34 | USE_SSH=true yarn deploy 35 | ``` 36 | 37 | Not using SSH: 38 | 39 | ```bash 40 | GIT_USER= yarn deploy 41 | ``` 42 | 43 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 44 | -------------------------------------------------------------------------------- /apps/site/docs/rules/no-return-try.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"no-return-try","category":"pedantic","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `no-return-try` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows `return`ing a `try` expression. 12 | 13 | Returning an error union directly has the same exact semantics as `try`ing 14 | it and then returning the result. 15 | 16 | ## Examples 17 | 18 | Examples of **incorrect** code for this rule: 19 | 20 | ```zig 21 | const std = @import("std"); 22 | 23 | fn foo() !void { 24 | return error.OutOfMemory; 25 | } 26 | 27 | fn bar() !void { 28 | return try foo(); 29 | } 30 | ``` 31 | 32 | Examples of **correct** code for this rule: 33 | 34 | ```zig 35 | const std = @import("std"); 36 | 37 | fn foo() !void { 38 | return error.OutOfMemory; 39 | } 40 | 41 | fn bar() !void { 42 | errdefer { 43 | std.debug.print("this still gets printed.\n", .{}); 44 | } 45 | 46 | return foo(); 47 | } 48 | ``` 49 | 50 | ## Configuration 51 | 52 | This rule has no configuration. 53 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs Site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | BUN_VERSION: latest 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pages: write # deploy to gh pages 16 | id-token: write # to verify the deployment originates from an appropriate source 17 | contents: read # to read the contents of the repo 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: oven-sh/setup-bun@v2 21 | with: 22 | bun-version: ${{ env.BUN_VERSION }} 23 | 24 | - name: bun install 25 | run: bun install --frozen-lockfile 26 | 27 | - uses: actions/cache@v4 28 | with: 29 | path: apps/site/build 30 | key: docs 31 | 32 | - name: build 33 | run: bun run build 34 | working-directory: apps/site 35 | 36 | # TODO: add permissions and uncomment this 37 | # - name: deploy 38 | # run: bun run deploy 39 | # working-directory: apps/site 40 | # env: 41 | # USE_SSH: true 42 | -------------------------------------------------------------------------------- /src/Semantic/test/modules_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const test_util = @import("util.zig"); 3 | 4 | const Semantic = @import("../../Semantic.zig"); 5 | 6 | const t = std.testing; 7 | 8 | test "@import(\"std\")" { 9 | const src = 10 | \\const std = @import("std"); 11 | ; 12 | var sema = try test_util.build(src); 13 | defer sema.deinit(); 14 | try t.expectEqual(1, sema.modules.imports.items.len); 15 | const import = sema.modules.imports.items[0]; 16 | try t.expectEqualStrings("std", import.specifier); 17 | try t.expectEqual(.module, import.kind); 18 | try t.expect(import.node != Semantic.NULL_NODE); 19 | } 20 | 21 | test "@import(\"foo.zig\")" { 22 | const src = 23 | \\const std = @import("foo.zig"); 24 | ; 25 | var sema = try test_util.build(src); 26 | defer sema.deinit(); 27 | try t.expectEqual(1, sema.modules.imports.items.len); 28 | const import = sema.modules.imports.items[0]; 29 | try t.expectEqualStrings("foo.zig", import.specifier); 30 | try t.expectEqual(.file, import.kind); 31 | try t.expect(import.node != Semantic.NULL_NODE); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Don Isaac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cli/test/print_ast_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Source = @import("../../source.zig").Source; 3 | 4 | // const allocator = std.testing.allocator; 5 | const print_command = @import("../print_command.zig"); 6 | 7 | const source_code: [:0]const u8 = 8 | \\pub const Foo = struct { 9 | \\ a: u32, 10 | \\ b: bool, 11 | \\ c: ?[]const u8, 12 | \\}; 13 | \\const x: Foo = Foo{ .a = 1, .b = true, .c = "hello" }; 14 | ; 15 | 16 | test print_command { 17 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 18 | defer arena.deinit(); 19 | const allocator = arena.allocator(); 20 | var source = try Source.fromString(allocator, @constCast(source_code), "foo.zig"); 21 | var buf = try std.ArrayList(u8).initCapacity(allocator, source.text().len); 22 | const writer = buf.writer(); 23 | 24 | try print_command.parseAndPrint(allocator, .{ .verbose = true }, source, writer.any()); 25 | // fixme: lots of trailing commas 26 | // const parsed = try std.json.parseFromSliceLeaky(json.Value, allocator, buf.items, .{}); 27 | // try std.testing.expect(parsed == .object); 28 | } 29 | -------------------------------------------------------------------------------- /docs/rules/line-length.md: -------------------------------------------------------------------------------- 1 | # `line-length` 2 | 3 | > Category: style 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Checks if any line goes beyond a given number of columns. 10 | 11 | ## Examples 12 | 13 | Examples of **incorrect** code for this rule (with a threshold of 120 columns): 14 | 15 | ```zig 16 | const std = @import("std"); 17 | const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 18 | fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 19 | return 123; 20 | } 21 | ``` 22 | 23 | Examples of **correct** code for this rule (with a threshold of 120 columns): 24 | 25 | ```zig 26 | const std = @import("std"); 27 | const longStructInMultipleLines = struct { 28 | max_length: u32 = 120, 29 | a: usize = 123, 30 | b: usize = 12354, 31 | c: usize = 1234352, 32 | }; 33 | fn Get123Constant() u32 { 34 | return 123; 35 | } 36 | ``` 37 | 38 | ## Configuration 39 | 40 | This rule accepts the following options: 41 | 42 | - max_length: int 43 | -------------------------------------------------------------------------------- /apps/vscode-extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Don Isaac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/zig/0.14.1/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) Zig contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/linter/rules.zig: -------------------------------------------------------------------------------- 1 | pub const AllocatorFirstParam = @import("./rules/allocator_first_param.zig"); 2 | pub const AvoidAs = @import("./rules/avoid_as.zig"); 3 | pub const CaseConvention = @import("./rules/case_convention.zig"); 4 | pub const EmptyFile = @import("./rules/empty_file.zig"); 5 | pub const HomelessTry = @import("./rules/homeless_try.zig"); 6 | pub const LineLength = @import("./rules/line_length.zig"); 7 | pub const MustReturnRef = @import("./rules/must_return_ref.zig"); 8 | pub const NoCatchReturn = @import("./rules/no_catch_return.zig"); 9 | pub const NoPrint = @import("./rules/no_print.zig"); 10 | pub const NoReturnTry = @import("./rules/no_return_try.zig"); 11 | pub const NoUnresolved = @import("./rules/no_unresolved.zig"); 12 | pub const ReturnedStackReference = @import("./rules/returned_stack_reference.zig"); 13 | pub const SuppressedErrors = @import("./rules/suppressed_errors.zig"); 14 | pub const UnsafeUndefined = @import("./rules/unsafe_undefined.zig"); 15 | pub const UnusedDecls = @import("./rules/unused_decls.zig"); 16 | pub const UselessErrorReturn = @import("./rules/useless_error_return.zig"); 17 | pub const DuplicateCase = @import("./rules/duplicate_case.zig"); 18 | -------------------------------------------------------------------------------- /src/Semantic/ModuleRecord.zig: -------------------------------------------------------------------------------- 1 | const ModuleRecord = @This(); 2 | 3 | imports: std.ArrayListUnmanaged(ImportEntry) = .{}, 4 | 5 | pub fn deinit(self: *ModuleRecord, allocator: Allocator) void { 6 | self.imports.deinit(allocator); 7 | self.* = undefined; 8 | } 9 | 10 | /// An import to a module or file. 11 | /// 12 | /// Does not include `@cImport`s (for now, this may change). 13 | /// 14 | /// ### Example 15 | ///```zig 16 | /// @import("some_module") // kind: module 17 | /// // ^^^^^^^^^^^ specifier 18 | /// ``` 19 | pub const ImportEntry = struct { 20 | specifier: []const u8, 21 | /// The `@import` node 22 | node: NodeIndex, 23 | kind: Kind, 24 | 25 | pub const Kind = enum { 26 | /// An import to a named module, such as `std`, `builtin`, or some dependency. 27 | /// 28 | /// Non-intrinsic modules are set in `build.zid`. 29 | module, 30 | /// An import to a `.zig` or `.zon` file. 31 | /// 32 | /// Specifier is a relative path to the file. 33 | file, 34 | }; 35 | }; 36 | 37 | const std = @import("std"); 38 | const Allocator = std.mem.Allocator; 39 | const NodeIndex = @import("ast.zig").NodeIndex; 40 | -------------------------------------------------------------------------------- /src/linter/config/Rules.zig: -------------------------------------------------------------------------------- 1 | //! Auto-generated by `tasks/confgen.zig`. Do not edit manually. 2 | const RuleConfig = @import("rule_config.zig").RuleConfig; 3 | const rules = @import("../rules.zig"); 4 | allocator_first_param: RuleConfig(rules.AllocatorFirstParam) = .{}, 5 | avoid_as: RuleConfig(rules.AvoidAs) = .{}, 6 | case_convention: RuleConfig(rules.CaseConvention) = .{}, 7 | empty_file: RuleConfig(rules.EmptyFile) = .{}, 8 | homeless_try: RuleConfig(rules.HomelessTry) = .{}, 9 | line_length: RuleConfig(rules.LineLength) = .{}, 10 | must_return_ref: RuleConfig(rules.MustReturnRef) = .{}, 11 | no_catch_return: RuleConfig(rules.NoCatchReturn) = .{}, 12 | no_print: RuleConfig(rules.NoPrint) = .{}, 13 | no_return_try: RuleConfig(rules.NoReturnTry) = .{}, 14 | no_unresolved: RuleConfig(rules.NoUnresolved) = .{}, 15 | returned_stack_reference: RuleConfig(rules.ReturnedStackReference) = .{}, 16 | suppressed_errors: RuleConfig(rules.SuppressedErrors) = .{}, 17 | unsafe_undefined: RuleConfig(rules.UnsafeUndefined) = .{}, 18 | unused_decls: RuleConfig(rules.UnusedDecls) = .{}, 19 | useless_error_return: RuleConfig(rules.UselessErrorReturn) = .{}, 20 | duplicate_case: RuleConfig(rules.DuplicateCase) = .{}, 21 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/unresolved_import.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "foo", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [], 29 | 30 | }, 31 | ], 32 | "unresolvedReferences": [], 33 | "modules": { 34 | "imports": [ 35 | { 36 | "specifier": "this/file/doesnotexist.zig", 37 | "node": 3, 38 | "kind": "file", 39 | 40 | }, 41 | ], 42 | }, 43 | "scopes": { 44 | "id": 0, 45 | "flags": ["top"], 46 | "bindings": { 47 | "@This()": 0, 48 | "foo": 1, 49 | 50 | }, 51 | "children": [], 52 | 53 | }, 54 | } -------------------------------------------------------------------------------- /src/linter/config/rules_config_rules.zig: -------------------------------------------------------------------------------- 1 | // Auto-generated by `tasks/confgen.zig`. Do not edit manually. 2 | const RuleConfig = @import("rule_config.zig").RuleConfig; 3 | const rules = @import("../rules.zig"); 4 | allocator_first_param: RuleConfig(rules.AllocatorFirstParam) = .{}, 5 | avoid_as: RuleConfig(rules.AvoidAs) = .{}, 6 | case_convention: RuleConfig(rules.CaseConvention) = .{}, 7 | empty_file: RuleConfig(rules.EmptyFile) = .{}, 8 | homeless_try: RuleConfig(rules.HomelessTry) = .{}, 9 | line_length: RuleConfig(rules.LineLength) = .{}, 10 | must_return_ref: RuleConfig(rules.MustReturnRef) = .{}, 11 | no_catch_return: RuleConfig(rules.NoCatchReturn) = .{}, 12 | no_print: RuleConfig(rules.NoPrint) = .{}, 13 | no_return_try: RuleConfig(rules.NoReturnTry) = .{}, 14 | no_unresolved: RuleConfig(rules.NoUnresolved) = .{}, 15 | returned_stack_reference: RuleConfig(rules.ReturnedStackReference) = .{}, 16 | suppressed_errors: RuleConfig(rules.SuppressedErrors) = .{}, 17 | unsafe_undefined: RuleConfig(rules.UnsafeUndefined) = .{}, 18 | unused_decls: RuleConfig(rules.UnusedDecls) = .{}, 19 | useless_error_return: RuleConfig(rules.UselessErrorReturn) = .{}, 20 | duplicate_case: RuleConfig(rules.DuplicateCase) = .{}, 21 | -------------------------------------------------------------------------------- /docs/rules/no-catch-return.md: -------------------------------------------------------------------------------- 1 | # `no-catch-return` 2 | 3 | > Category: pedantic 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows `catch` blocks that immediately return the caught error. 10 | 11 | Catch blocks that do nothing but return their error can and should be 12 | replaced with a `try` statement. This rule allows for `catch`es that 13 | have side effects such as printing the error or switching over it. 14 | 15 | ## Examples 16 | 17 | Examples of **incorrect** code for this rule: 18 | 19 | ```zig 20 | fn foo() !void { 21 | riskyOp() catch |e| return e; 22 | riskyOp() catch |e| { return e; }; 23 | } 24 | ``` 25 | 26 | Examples of **correct** code for this rule: 27 | 28 | ```zig 29 | const std = @import("std"); 30 | 31 | fn foo() !void{ 32 | try riskyOp(); 33 | } 34 | 35 | // re-throwing with side effects is fine 36 | fn bar() !void { 37 | riskyOp() catch |e| { 38 | std.debug.print("Error: {any}\n", .{e}); 39 | return e; 40 | }; 41 | } 42 | 43 | // throwing a new error is fine 44 | fn baz() !void { 45 | riskyOp() catch |e| return error.OutOfMemory; 46 | } 47 | ``` 48 | 49 | ## Configuration 50 | 51 | This rule has no configuration. 52 | -------------------------------------------------------------------------------- /docs/rules/no-unresolved.md: -------------------------------------------------------------------------------- 1 | # `no-unresolved` 2 | 3 | > Category: correctness 4 | > 5 | > Enabled by default?: Yes (error) 6 | 7 | ## What This Rule Does 8 | 9 | Checks for imports to files that do not exist. 10 | 11 | This rule only checks for file-based imports. Modules added by `build.zig` 12 | are not checked. More precisely, imports to paths ending in `.zig` will be 13 | resolved. This rule checks that a file exists at the imported path and is 14 | not a directory. Symlinks are allowed but are not followed. 15 | 16 | ## Examples 17 | 18 | Assume the following directory structure: 19 | 20 | ```plaintext 21 | . 22 | ├── foo.zig 23 | ├── mod 24 | │ └── bar.zig 25 | ├── not_a_file.zig 26 | │ └── baz.zig 27 | └── root.zig 28 | ``` 29 | 30 | Examples of **incorrect** code for this rule: 31 | 32 | ```zig 33 | // root.zig 34 | const x = @import("mod/foo.zig"); // foo.zig is in the root directory. 35 | const y = @import("not_a_file.zig"); // directory, not a file 36 | ``` 37 | 38 | Examples of **correct** code for this rule: 39 | 40 | ```zig 41 | // root.zig 42 | const x = @import("foo.zig"); 43 | const y = @import("mod/bar.zig"); 44 | ``` 45 | 46 | ## Configuration 47 | 48 | This rule has no configuration. 49 | -------------------------------------------------------------------------------- /src/reporter/formatter.zig: -------------------------------------------------------------------------------- 1 | //! Formatters process diagnostics for a `Reporter`. 2 | 3 | pub const Github = @import("formatters/GithubFormatter.zig"); 4 | pub const Graphical = @import("formatters/GraphicalFormatter.zig"); 5 | pub const JSON = @import("formatters/JSONFormatter.zig"); 6 | 7 | pub const Meta = struct { 8 | report_statistics: bool, 9 | }; 10 | 11 | pub const Kind = enum { 12 | graphical, 13 | github, 14 | json, 15 | 16 | const FormatMap = std.StaticStringMapWithEql( 17 | Kind, 18 | std.static_string_map.eqlAsciiIgnoreCase, 19 | ); 20 | const formats = FormatMap.initComptime(&[_]struct { []const u8, Kind }{ 21 | .{ "github", .github }, 22 | .{ "gh", .github }, 23 | .{ "json", .json }, 24 | .{ "graphical", .graphical }, 25 | .{ "default", .graphical }, 26 | }); 27 | 28 | /// Get a formatter kind by name. Names are case-insensitive. 29 | pub fn fromString(str: []const u8) ?Kind { 30 | return formats.get(str); 31 | } 32 | }; 33 | 34 | pub const FormatError = Writer.Error || Allocator.Error; 35 | 36 | const std = @import("std"); 37 | const Writer = std.io.AnyWriter; 38 | const Allocator = std.mem.Allocator; 39 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu on M1 Mac", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "build": { 7 | "dockerfile": "Dockerfile", 8 | "context": "." 9 | }, 10 | // Features to add to the dev container. More info: https://containers.dev/features. 11 | "features": { 12 | // homebrew intentionally excluded. Not available on aarch64. 13 | "ghcr.io/devcontainers-extra/features/curl-apt-get:1": {}, 14 | "ghcr.io/devcontainers-extra/features/zig:1": {}, 15 | "ghcr.io/guiyomh/features/just:0": {} 16 | }, 17 | 18 | // Use 'postCreateCommand' to run commands after the container is created. 19 | // "postCreateCommand": "uname -a", 20 | 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | "vscode": { 24 | "extensions": [ 25 | "ziglang.vscode-zig", 26 | "vadimcn.vscode-lldb" 27 | ] 28 | } 29 | } 30 | 31 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 32 | // "remoteUser": "root" 33 | } 34 | -------------------------------------------------------------------------------- /docs/rules/avoid-as.md: -------------------------------------------------------------------------------- 1 | # `avoid-as` 2 | 3 | > Category: pedantic 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows using `@as()` when types can be otherwise inferred. 10 | 11 | Zig has powerful [Result Location Semantics](https://ziglang.org/documentation/master/#Result-Location-Semantics) for inferring what type 12 | something should be. This happens in function parameters, return types, 13 | and type annotations. `@as()` is a last resort when no other contextual 14 | information is available. In any other case, other type inference mechanisms 15 | should be used. 16 | 17 | > [!NOTE] 18 | > Checks for function parameters and return types are not yet implemented. 19 | 20 | ## Examples 21 | 22 | Examples of **incorrect** code for this rule: 23 | 24 | ```zig 25 | const x = @as(u32, 1); 26 | 27 | fn foo(x: u32) u64 { 28 | return @as(u64, x); // type is inferred from return type 29 | } 30 | foo(@as(u32, 1)); // type is inferred from function signature 31 | ``` 32 | 33 | Examples of **correct** code for this rule: 34 | 35 | ```zig 36 | const x: u32 = 1; 37 | 38 | fn foo(x: u32) void { 39 | // ... 40 | } 41 | foo(1); 42 | ``` 43 | 44 | ## Configuration 45 | 46 | This rule has no configuration. 47 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/useless-error-return.snap: -------------------------------------------------------------------------------- 1 | 𝙭 useless-error-return: Function 'foo' has an error union return type but never returns an error. 2 | ╭─[useless-error-return.zig:1:4] 3 | 1 │ fn foo() !void { return; } 4 | · ─┬─ 5 | · ╰── 'foo' is declared here 6 | ╰──── 7 | help: Remove the error union return type. 8 | 9 | 𝙭 useless-error-return: Function 'init' has an error union return type but suppresses all its errors. 10 | ╭─[useless-error-return.zig:3:10] 11 | 2 │ pub const Foo = struct { 12 | 3 │ pub fn init(allocator: std.mem.Allocator) !Foo { 13 | · ──┬── 14 | · ╰── 'init' is declared here 15 | 4 │ const new = allocator.create(Foo) catch @panic("OOM"); 16 | · ──┬── 17 | · ╰── It catches errors here 18 | ╰──── 19 | help: Use `try` to propagate errors to the caller. 20 | 21 | 𝙭 useless-error-return: Function 'foo' has an error union return type but never returns an error. 22 | ╭─[useless-error-return.zig:1:4] 23 | 1 │ fn foo() !void { 24 | · ─┬─ 25 | · ╰── 'foo' is declared here 26 | 2 │ const e = bar(); 27 | ╰──── 28 | help: Remove the error union return type. 29 | 30 | -------------------------------------------------------------------------------- /apps/site/docs/rules/case-convention.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"case-convention","category":"style","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `case-convention` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Enforces Zig's naming convention. 12 | 13 | :::warning 14 | Only functions are checked at this time. 15 | ::: 16 | 17 | ## Functions 18 | 19 | In general, functions that return values should use camelCase names while 20 | those that return types should use PascalCase. Specially coming from Rust, 21 | some people may be used to use snake_case for their functions, which can 22 | lead to inconsistencies in the code. 23 | 24 | Note that `extern` functions are not checked since you cannot change 25 | their names. 26 | 27 | ## Examples 28 | 29 | Examples of **incorrect** code for this rule: 30 | 31 | ```zig 32 | fn this_one_is_in_snake_case() void {} 33 | fn generic(T: type) T { return T{}; } 34 | ``` 35 | 36 | Examples of **correct** code for this rule: 37 | 38 | ```zig 39 | fn thisFunctionIsInCamelCase() void {} 40 | fn Generic(T: type) T { return T{}; } 41 | extern fn this_is_declared_in_c() void; 42 | ``` 43 | 44 | ## Configuration 45 | 46 | This rule has no configuration. 47 | -------------------------------------------------------------------------------- /apps/site/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Installation 6 | 7 | The fastest way to install ZLint is with our [installation script](https://github.com/donisaac/zlint/blob/main/tasks/install.sh). 8 | 9 | ```sh 10 | curl -fsSL https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/tasks/install.sh | bash 11 | ``` 12 | 13 | :::warning 14 | This installation script does not work on Windows. Please download Windows 15 | binaries directly from the 16 | [releases page](https://github.com/DonIsaac/zlint/releases/latest). 17 | ::: 18 | 19 | :::info[Want to contribute?] 20 | If you're good at PowerShell and want to contribute, there is an open issue 21 | for [adding a PowerShell installation script](https://github.com/DonIsaac/zlint/issues/221) 22 | ::: 23 | 24 | ## Manual Installation 25 | 26 | Each release is available on the [releases page](https://github.com/DonIsaac/zlint/releases/latest). 27 | Click on the correct binary for your platform to download it. 28 | 29 | ## Building from Source 30 | Clone this repo and compile the project with [Zig](https://ziglang.org/)'s build 31 | system. 32 | 33 | ```zig 34 | zig build --release=safe 35 | ``` 36 | 37 | :::tip 38 | Full setup instructions are available [here](./contributing/index.mdx). 39 | ::: 40 | -------------------------------------------------------------------------------- /apps/site/docs/rules/line-length.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"line-length","category":"style","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `line-length` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Checks if any line goes beyond a given number of columns. 12 | 13 | ## Examples 14 | 15 | Examples of **incorrect** code for this rule (with a threshold of 120 columns): 16 | 17 | ```zig 18 | const std = @import("std"); 19 | const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 20 | fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 21 | return 123; 22 | } 23 | ``` 24 | 25 | Examples of **correct** code for this rule (with a threshold of 120 columns): 26 | 27 | ```zig 28 | const std = @import("std"); 29 | const longStructInMultipleLines = struct { 30 | max_length: u32 = 120, 31 | a: usize = 123, 32 | b: usize = 12354, 33 | c: usize = 1234352, 34 | }; 35 | fn Get123Constant() u32 { 36 | return 123; 37 | } 38 | ``` 39 | 40 | ## Configuration 41 | 42 | This rule accepts the following options: 43 | 44 | - max_length: int 45 | -------------------------------------------------------------------------------- /test/snapshots/semantic-coverage/bun.snap: -------------------------------------------------------------------------------- 1 | Passed: 97.37443% (853/876) 2 | Panics: 0% (0/876) 3 | 4 | src/bun.js/api/BunObject.zig: error.FullMismatch 5 | src/bun.js/node/util/parse_args.zig: error.FullMismatch 6 | src/bun.js/test/expect.zig: error.FullMismatch 7 | src/bun.js/webcore/ByteStream.zig: error.FullMismatch 8 | src/bun.js/webcore/Request.zig: error.FullMismatch 9 | src/cli.zig: error.FullMismatch 10 | src/cli/audit_command.zig: error.FullMismatch 11 | src/cli/pack_command.zig: error.FullMismatch 12 | src/cli/pm_view_command.zig: error.FullMismatch 13 | src/css/css_parser.zig: error.FullMismatch 14 | src/css/small_list.zig: error.FullMismatch 15 | src/css/values/number.zig: error.FullMismatch 16 | src/install/PackageInstaller.zig: error.FullMismatch 17 | src/install/PackageManager/PackageManagerEnqueue.zig: error.FullMismatch 18 | src/install/PackageManager/install_with_manager.zig: error.FullMismatch 19 | src/install/PackageManager/patchPackage.zig: error.FullMismatch 20 | src/install/extract_tarball.zig: error.FullMismatch 21 | src/install/lockfile.zig: error.FullMismatch 22 | src/install/lockfile/bun.lock.zig: error.FullMismatch 23 | src/install/npm.zig: error.FullMismatch 24 | src/shell/Builtin.zig: error.FullMismatch 25 | src/string.zig: error.FullMismatch 26 | src/sys.zig: error.FullMismatch 27 | -------------------------------------------------------------------------------- /test/fixtures/simple/pass/loops_while.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn simpleWhile() void { 4 | var i: u32 = 0; 5 | 6 | while (i < 5) { 7 | const power_of_two = 1 << i; 8 | std.debug.print("2^{} = {}\n", .{ i, power_of_two }); 9 | i += 1; 10 | } 11 | } 12 | 13 | fn whileWithClosure() void { 14 | var map = std.AutoHashMap(u32, u32){}; 15 | map.put(1, 1); 16 | map.put(2, 2); 17 | const iter = map.iterator(); 18 | while (iter.next()) |ent| { 19 | const k = ent.key_ptr.*; 20 | const v = ent.value_ptr.*; 21 | std.debug.print("{d}: {d}\n", .{ k, v }); 22 | } 23 | } 24 | 25 | // copied from https://ziglang.org/documentation/master/#while 26 | fn whileWithExpr() !void { 27 | var x: usize = 1; 28 | var y: usize = 1; 29 | while (x * y < 2000) : ({ 30 | x *= 2; 31 | y *= 3; 32 | }) { 33 | const my_xy = x * y; 34 | try std.testing.expect(my_xy < 2000); 35 | } 36 | } 37 | 38 | // copied from https://ziglang.org/documentation/master/#while 39 | fn rangeHasNumber(begin: usize, end: usize, number: usize) bool { 40 | var i = begin; 41 | return while (i < end) : (i += 1) { 42 | if (i == number) { 43 | break true; 44 | } 45 | } else false; 46 | } 47 | -------------------------------------------------------------------------------- /apps/site/docs/rules/no-unresolved.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"no-unresolved","category":"correctness","default":"err","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `no-unresolved` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Checks for imports to files that do not exist. 12 | 13 | This rule only checks for file-based imports. Modules added by `build.zig` 14 | are not checked. More precisely, imports to paths ending in `.zig` will be 15 | resolved. This rule checks that a file exists at the imported path and is 16 | not a directory. Symlinks are allowed but are not followed. 17 | 18 | ## Examples 19 | 20 | Assume the following directory structure: 21 | 22 | ```plaintext 23 | . 24 | ├── foo.zig 25 | ├── mod 26 | │ └── bar.zig 27 | ├── not_a_file.zig 28 | │ └── baz.zig 29 | └── root.zig 30 | ``` 31 | 32 | Examples of **incorrect** code for this rule: 33 | 34 | ```zig 35 | // root.zig 36 | const x = @import("mod/foo.zig"); // foo.zig is in the root directory. 37 | const y = @import("not_a_file.zig"); // directory, not a file 38 | ``` 39 | 40 | Examples of **correct** code for this rule: 41 | 42 | ```zig 43 | // root.zig 44 | const x = @import("foo.zig"); 45 | const y = @import("mod/bar.zig"); 46 | ``` 47 | 48 | ## Configuration 49 | 50 | This rule has no configuration. 51 | -------------------------------------------------------------------------------- /docs/rules/returned-stack-reference.md: -------------------------------------------------------------------------------- 1 | # `returned-stack-reference` 2 | 3 | > Category: nursery 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Checks for functions that return references to stack-allocated memory. 10 | 11 | > [!NOTE] 12 | > This rule is still in early development. PRs to improve it are welcome. 13 | 14 | It is illegal to use stack-allocated memory outside of the function that 15 | allocated it. Once that function returns and the stack is popped, the memory 16 | is no longer valid and may cause segfaults or undefined behavior. 17 | 18 | ```zig 19 | const std = @import("std"); 20 | fn foo() *u32 { 21 | var x: u32 = 1; // x is on the stack 22 | return &x; 23 | } 24 | fn bar() void { 25 | const x = foo(); 26 | std.debug.print("{d}\n", .{x}); // crashes 27 | } 28 | ``` 29 | 30 | ## Examples 31 | 32 | Examples of **incorrect** code for this rule: 33 | 34 | ```zig 35 | const std = @import("std"); 36 | fn foo() *u32 { 37 | var x: u32 = 1; 38 | return &x; 39 | } 40 | fn bar() []u32 { 41 | var x: [1]u32 = .{1}; 42 | return x[0..]; 43 | } 44 | ``` 45 | 46 | Examples of **correct** code for this rule: 47 | 48 | ```zig 49 | fn foo() *u32 { 50 | var x = std.heap.page_allocator.create(u32); 51 | x.* = 1; 52 | return x; 53 | } 54 | ``` 55 | 56 | ## Configuration 57 | 58 | This rule has no configuration. 59 | -------------------------------------------------------------------------------- /apps/site/docs/rules/duplicate-case.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"duplicate-case","category":"suspicious","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `duplicate-case` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Checks for duplicate cases in switch statements. 12 | 13 | This rule identifies when switch statements have case branches that could 14 | be merged together without affecting program behavior. It does _not_ check 15 | that the value being switched over is the same; rather it checks whether 16 | the target expressions are duplicates. 17 | 18 | ## Examples 19 | 20 | Examples of **incorrect** code for this rule: 21 | 22 | ```zig 23 | fn foo() void { 24 | const x = switch (1) { 25 | 1 => 1, 26 | else => 1, 27 | }; 28 | } 29 | 30 | fn bar(y: u32) void { 31 | const x = switch (y) { 32 | 1 => y + 1, 33 | 2 => 1 + y, 34 | else => y * 2, 35 | }; 36 | } 37 | ``` 38 | 39 | Examples of **correct** code for this rule: 40 | 41 | ```zig 42 | fn foo() void { 43 | const x = switch (1) { 44 | 1 => 1, 45 | 2 => 2, 46 | }; 47 | } 48 | 49 | fn bar(y: u32) void { 50 | const x = switch (y) { 51 | 1 => y + 1, 52 | 2 => y * 2, 53 | 3 => y - 1, 54 | }; 55 | } 56 | ``` 57 | 58 | ## Configuration 59 | 60 | This rule has no configuration. 61 | -------------------------------------------------------------------------------- /apps/site/docs/rules/no-catch-return.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"no-catch-return","category":"pedantic","default":"warning","fix":{"kind":"fix","dangerous":false}}' 3 | --- 4 | 5 | # `no-catch-return` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows `catch` blocks that immediately return the caught error. 12 | 13 | Catch blocks that do nothing but return their error can and should be 14 | replaced with a `try` statement. This rule allows for `catch`es that 15 | have side effects such as printing the error or switching over it. 16 | 17 | ## Examples 18 | 19 | Examples of **incorrect** code for this rule: 20 | 21 | ```zig 22 | fn foo() !void { 23 | riskyOp() catch |e| return e; 24 | riskyOp() catch |e| { return e; }; 25 | } 26 | ``` 27 | 28 | Examples of **correct** code for this rule: 29 | 30 | ```zig 31 | const std = @import("std"); 32 | 33 | fn foo() !void{ 34 | try riskyOp(); 35 | } 36 | 37 | // re-throwing with side effects is fine 38 | fn bar() !void { 39 | riskyOp() catch |e| { 40 | std.debug.print("Error: {any}\n", .{e}); 41 | return e; 42 | }; 43 | } 44 | 45 | // throwing a new error is fine 46 | fn baz() !void { 47 | riskyOp() catch |e| return error.OutOfMemory; 48 | } 49 | ``` 50 | 51 | ## Configuration 52 | 53 | This rule has no configuration. 54 | -------------------------------------------------------------------------------- /apps/site/docs/rules/RulesPage.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | padding: 2rem 0; 3 | } 4 | 5 | @media (min-width: 997px) { 6 | .page { 7 | max-width: 800px; 8 | margin: auto; 9 | } 10 | } 11 | 12 | .searchBar { 13 | width: 100%; 14 | padding: 0.75rem 1rem; 15 | font-size: 1rem; 16 | border: 1px solid var(--ifm-color-emphasis-300); 17 | border-radius: 0.5rem; 18 | background-color: var(--ifm-background-color); 19 | color: var(--ifm-color-content); 20 | } 21 | 22 | .rulesGrid { 23 | display: grid; 24 | grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); 25 | gap: 1.5rem; 26 | align-items: start; 27 | } 28 | 29 | .ruleCard { 30 | height: 100%; 31 | display: flex; 32 | flex-direction: column; 33 | color: var(--ifm-heading-color); 34 | transition: all 0.2s ease-in-out; 35 | transform: translateY(0); 36 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 37 | 38 | &:hover { 39 | text-decoration: none; 40 | transform: translateY(-4px); 41 | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); 42 | } 43 | } 44 | 45 | .noSearchResults { 46 | text-align: center; 47 | padding: 3rem; 48 | color: var(--ifm-color-emphasis-600); 49 | min-height: 30vh; 50 | } 51 | 52 | .cardHeader { 53 | h2, 54 | h3, 55 | h4, 56 | h5, 57 | h6 { 58 | margin: 0 0 0.5rem 0; 59 | } 60 | } 61 | 62 | .cardBody { 63 | flex: 1; 64 | p { 65 | margin: 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apps/site/docs/rules/avoid-as.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"avoid-as","category":"pedantic","default":"warning","fix":{"kind":"fix","dangerous":false}}' 3 | --- 4 | 5 | # `avoid-as` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows using `@as()` when types can be otherwise inferred. 12 | 13 | Zig has powerful [Result Location Semantics](https://ziglang.org/documentation/master/#Result-Location-Semantics) for inferring what type 14 | something should be. This happens in function parameters, return types, 15 | and type annotations. `@as()` is a last resort when no other contextual 16 | information is available. In any other case, other type inference mechanisms 17 | should be used. 18 | 19 | :::warning 20 | 21 | Checks for function parameters and return types are not yet implemented. 22 | 23 | ::: 24 | 25 | ## Examples 26 | 27 | Examples of **incorrect** code for this rule: 28 | 29 | ```zig 30 | const x = @as(u32, 1); 31 | 32 | fn foo(x: u32) u64 { 33 | return @as(u64, x); // type is inferred from return type 34 | } 35 | foo(@as(u32, 1)); // type is inferred from function signature 36 | ``` 37 | 38 | Examples of **correct** code for this rule: 39 | 40 | ```zig 41 | const x: u32 = 1; 42 | 43 | fn foo(x: u32) void { 44 | // ... 45 | } 46 | foo(1); 47 | ``` 48 | 49 | ## Configuration 50 | 51 | This rule has no configuration. 52 | -------------------------------------------------------------------------------- /docs/rules/must-return-ref.md: -------------------------------------------------------------------------------- 1 | # `must-return-ref` 2 | 3 | > Category: suspicious 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows returning copies of types that store a `capacity`. 10 | 11 | Zig does not have move semantics. Returning a value by value copies it. 12 | Returning a copy of a struct's field that records how much memory it has 13 | allocated can easily lead to memory leaks. 14 | 15 | ```zig 16 | const std = @import("std"); 17 | pub const Foo = struct { 18 | list: std.ArrayList(u32), 19 | pub fn getList(self: *Foo) std.ArrayList(u32) { 20 | return self.list; 21 | } 22 | }; 23 | 24 | pub fn main() !void { 25 | var foo: Foo = .{ 26 | .list = try std.ArrayList(u32).init(std.heap.page_allocator) 27 | }; 28 | defer foo.list.deinit(); 29 | var list = foo.getList(); 30 | try list.append(1); // leaked! 31 | } 32 | ``` 33 | 34 | ## Examples 35 | 36 | Examples of **incorrect** code for this rule: 37 | 38 | ```zig 39 | fn foo(self: *Foo) std.ArrayList(u32) { 40 | return self.list; 41 | } 42 | ``` 43 | 44 | Examples of **correct** code for this rule: 45 | 46 | ```zig 47 | // pass by reference 48 | fn foo(self: *Foo) *std.ArrayList(u32) { 49 | return &self.list; 50 | } 51 | 52 | // new instances are fine 53 | fn foo() ArenaAllocator { 54 | return std.mem.ArenaAllocator.init(std.heap.page_allocator); 55 | } 56 | ``` 57 | 58 | ## Configuration 59 | 60 | This rule has no configuration. 61 | -------------------------------------------------------------------------------- /apps/site/docs/rules/returned-stack-reference.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"returned-stack-reference","category":"nursery","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `returned-stack-reference` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Checks for functions that return references to stack-allocated memory. 12 | 13 | :::warning 14 | 15 | This rule is still in early development. PRs to improve it are welcome. 16 | 17 | ::: 18 | 19 | It is illegal to use stack-allocated memory outside of the function that 20 | allocated it. Once that function returns and the stack is popped, the memory 21 | is no longer valid and may cause segfaults or undefined behavior. 22 | 23 | ```zig 24 | const std = @import("std"); 25 | fn foo() *u32 { 26 | var x: u32 = 1; // x is on the stack 27 | return &x; 28 | } 29 | fn bar() void { 30 | const x = foo(); 31 | std.debug.print("{d}\n", .{x}); // crashes 32 | } 33 | ``` 34 | 35 | ## Examples 36 | 37 | Examples of **incorrect** code for this rule: 38 | 39 | ```zig 40 | const std = @import("std"); 41 | fn foo() *u32 { 42 | var x: u32 = 1; 43 | return &x; 44 | } 45 | fn bar() []u32 { 46 | var x: [1]u32 = .{1}; 47 | return x[0..]; 48 | } 49 | ``` 50 | 51 | Examples of **correct** code for this rule: 52 | 53 | ```zig 54 | fn foo() *u32 { 55 | var x = std.heap.page_allocator.create(u32); 56 | x.* = 1; 57 | return x; 58 | } 59 | ``` 60 | 61 | ## Configuration 62 | 63 | This rule has no configuration. 64 | -------------------------------------------------------------------------------- /apps/site/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { JSX, PropsWithChildren } from 'react' 2 | import Link from '@docusaurus/Link' 3 | import clsx from 'clsx' 4 | import styles from './Button.module.css' 5 | import { Size, Variant } from '../theme/types' 6 | 7 | interface BaseButtonProps { 8 | size?: Size 9 | variant?: Variant 10 | outline?: boolean 11 | } 12 | interface LinkButtonProps extends BaseButtonProps, React.ComponentProps { 13 | href: string 14 | as?: never 15 | } 16 | interface ButtonButtonProps extends BaseButtonProps, React.HTMLAttributes { 17 | href?: never 18 | as?: React.ComponentType 19 | } 20 | 21 | export type ButtonProps = LinkButtonProps | ButtonButtonProps 22 | 23 | export default function Button({ 24 | children, 25 | as, 26 | href, 27 | size = 'lg', 28 | variant = 'primary', 29 | outline, 30 | className: classNameProp, 31 | ...props 32 | }: ButtonProps): JSX.Element { 33 | const Component = as ?? href ? Link : 'button' 34 | const className = clsx( 35 | 'button', 36 | size && `button--${size}`, 37 | variant && `button--${variant}`, 38 | { 39 | 'button--outline': outline, 40 | }, 41 | styles.flexButton, 42 | classNameProp 43 | ) 44 | return ( 45 | 46 | {children} 47 | 48 | ) 49 | } 50 | 51 | Button.Row = function ButtonRow({ children }: PropsWithChildren) { 52 | return {children} 53 | } 54 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/allocator-first-param.snap: -------------------------------------------------------------------------------- 1 | 𝙭 allocator-first-param: Allocators should be the first parameter of a function 2 | ╭─[allocator-first-param.zig:1:16] 3 | 1 │ fn foo(x: u32, allocator: Allocator) void { } 4 | · ───────── 5 | ╰──── 6 | 7 | 𝙭 allocator-first-param: Allocators should be the first parameter of a function 8 | ╭─[allocator-first-param.zig:1:16] 9 | 1 │ fn foo(x: u32, allocator: SomeExoticAllocatorThatIsWeird) void { } 10 | · ───────── 11 | ╰──── 12 | 13 | 𝙭 allocator-first-param: Allocators should be the first parameter of a function 14 | ╭─[allocator-first-param.zig:1:16] 15 | 1 │ fn foo(x: u32, thingy: Allocator) void { } 16 | · ────── 17 | ╰──── 18 | 19 | 𝙭 allocator-first-param: Allocators should be the first parameter of a function 20 | ╭─[allocator-first-param.zig:1:16] 21 | 1 │ fn foo(x: u32, thingy: std.mem.Allocator) void { } 22 | · ────── 23 | ╰──── 24 | 25 | 𝙭 allocator-first-param: Allocators should be the first parameter of a function 26 | ╭─[allocator-first-param.zig:1:16] 27 | 1 │ fn foo(x: u32, y: std.heap.ArenaAllocator) void { } 28 | · ─ 29 | ╰──── 30 | 31 | 𝙭 allocator-first-param: Allocators should be the first parameter of a function 32 | ╭─[allocator-first-param.zig:1:53] 33 | 1 │ const Foo = struct { pub fn bar(self: *Foo, x: u32, allocator: Allocator) void {} }; 34 | · ───────── 35 | ╰──── 36 | 37 | -------------------------------------------------------------------------------- /apps/site/src/css/custom.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css2?family=Abyssinica+SIL%3Aital%2Cwght%400%2C400&display=swap); 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #f7a41d; 11 | --ifm-color-primary-dark: #e6991c; 12 | --ifm-color-primary-darker: #d08b1b; 13 | --ifm-color-primary-darkest: #b47818; 14 | --ifm-color-primary-light: #fbb037; 15 | --ifm-color-primary-lighter: #febc51; 16 | --ifm-color-primary-lightest: #fbbb54; 17 | --ifm-code-font-size: 95%; 18 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 19 | } 20 | 21 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 22 | [data-theme='dark'] { 23 | --ifm-color-primary: #f7a41d; 24 | --ifm-color-primary-dark: #e6991c; 25 | --ifm-color-primary-darker: #e6991c; 26 | --ifm-color-primary-darkest: #e6991c; 27 | --ifm-color-primary-light: #fbb037; 28 | --ifm-color-primary-lighter: #febc51; 29 | --ifm-color-primary-lightest: #fbbb54; 30 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 31 | --ifm-color-info: #1c61ec; 32 | --ifm-color-warning: #ce9908; 33 | } 34 | 35 | /* .alert { 36 | display: flex; 37 | align-items: center; 38 | gap: 0.25rem; 39 | } */ 40 | 41 | .font-abyssinica { 42 | font-family: 'Abyssinica SIL', serif; 43 | } 44 | .text--black { 45 | color: #000000; 46 | } 47 | -------------------------------------------------------------------------------- /apps/site/docs/rules/must-return-ref.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"must-return-ref","category":"suspicious","default":"warning","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `must-return-ref` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows returning copies of types that store a `capacity`. 12 | 13 | Zig does not have move semantics. Returning a value by value copies it. 14 | Returning a copy of a struct's field that records how much memory it has 15 | allocated can easily lead to memory leaks. 16 | 17 | ```zig 18 | const std = @import("std"); 19 | pub const Foo = struct { 20 | list: std.ArrayList(u32), 21 | pub fn getList(self: *Foo) std.ArrayList(u32) { 22 | return self.list; 23 | } 24 | }; 25 | 26 | pub fn main() !void { 27 | var foo: Foo = .{ 28 | .list = try std.ArrayList(u32).init(std.heap.page_allocator) 29 | }; 30 | defer foo.list.deinit(); 31 | var list = foo.getList(); 32 | try list.append(1); // leaked! 33 | } 34 | ``` 35 | 36 | ## Examples 37 | 38 | Examples of **incorrect** code for this rule: 39 | 40 | ```zig 41 | fn foo(self: *Foo) std.ArrayList(u32) { 42 | return self.list; 43 | } 44 | ``` 45 | 46 | Examples of **correct** code for this rule: 47 | 48 | ```zig 49 | // pass by reference 50 | fn foo(self: *Foo) *std.ArrayList(u32) { 51 | return &self.list; 52 | } 53 | 54 | // new instances are fine 55 | fn foo() ArenaAllocator { 56 | return std.mem.ArenaAllocator.init(std.heap.page_allocator); 57 | } 58 | ``` 59 | 60 | ## Configuration 61 | 62 | This rule has no configuration. 63 | -------------------------------------------------------------------------------- /apps/vscode-extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { ConfigService } from './ConfigService' 3 | import { BinaryService } from './BinaryService' 4 | import { DiagnosticsService } from './DiagnosticsService' 5 | 6 | export function activate(context: vscode.ExtensionContext) { 7 | const logs = vscode.window.createOutputChannel('zlint') 8 | logs.clear() 9 | logs.appendLine('zlint extension activated') 10 | const config = new ConfigService(logs) 11 | logs.appendLine('Found config: ' + JSON.stringify(config.config)) 12 | const bin = new BinaryService(config, logs) 13 | const diagnostics = new DiagnosticsService(config, bin, logs) 14 | 15 | bin 16 | .findZLintBinary() 17 | .catch((e) => logs.appendLine('error finding zlint binary: ' + e)) 18 | 19 | const lintCmd = vscode.commands.registerCommand('zig.zlint.lint', () => { 20 | if (!bin.ready) { 21 | logs.appendLine('zlint binary not ready, not running zlint.lint') 22 | return 23 | } 24 | logs.appendLine('collecting diagnostics') 25 | diagnostics 26 | .collectDiagnostics() 27 | .then(() => logs.appendLine('done')) 28 | .catch((e) => logs.appendLine('error collecting diagnostics: ' + e)) 29 | }) 30 | 31 | // FIXME: this command is not working 32 | // const toggleCommand = vscode.commands.registerCommand( 33 | // 'zig.zlint.toggle', 34 | // () => config.set( 35 | // 'enabled', 36 | // !config.config.enabled, 37 | // vscode.ConfigurationTarget.WorkspaceFolder 38 | // ), 39 | // ) 40 | 41 | 42 | context.subscriptions.push(config, bin, lintCmd) 43 | } 44 | -------------------------------------------------------------------------------- /apps/vscode-extension/README.md: -------------------------------------------------------------------------------- 1 | # ZLint VSCode Extension 2 | 3 | A Visual Studio Code extension that integrates the [ZLint](https://github.com/DonIsaac/zlint) linter for Zig code. 4 | 5 | ## ✨ Features 6 | 7 | - 🔍 **Real-time Linting**: Automatically detects and highlights issues in your Zig code 8 | - ⚡️ **Fast Performance**: Quick analysis without slowing down your editor 9 | - 💡 **Detailed Diagnostics**: Clear error messages with explanations and suggestions 10 | - 🛠️ **Configurable**: Customize linting behavior through VSCode settings 11 | 12 | ## 📦 Installation 13 | 14 | 1. Install the ZLint binary: 15 | ```sh 16 | curl -fsSL https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/tasks/install.sh | bash 17 | ``` 18 | 19 | 2. Install the extension from the VSCode marketplace or build it locally: 20 | ```sh 21 | bun install 22 | ``` 23 | 24 | ## ⚙️ Configuration 25 | 26 | The extension can be configured through VSCode settings: 27 | 28 | ```json 29 | { 30 | "zig.zlint": { 31 | "enabled": true, 32 | "path": "/path/to/zlint" // Optional: specify custom path to zlint binary 33 | } 34 | } 35 | ``` 36 | 37 | ### Settings 38 | 39 | - `zig.zlint.enabled`: Enable/disable the extension (default: `true`) 40 | - `zig.zlint.path`: Custom path to the zlint binary (optional) 41 | 42 | ## 🔨 Building from Source 43 | 44 | 1. Clone the repository 45 | 2. Install dependencies: 46 | ```sh 47 | bun install 48 | ``` 49 | 3. Build the extension: 50 | ```sh 51 | bun run index.ts 52 | ``` 53 | 54 | ## 📝 License 55 | 56 | This extension is licensed under the same terms as the ZLint project. 57 | -------------------------------------------------------------------------------- /src/Semantic/Parse.zig: -------------------------------------------------------------------------------- 1 | const Parse = @This(); 2 | 3 | ast: Ast, 4 | // NOTE: We re-tokenize and store our own tokens b/c AST throws away the end 5 | // position of each token. B/c of this, `ast.stokenSlice` re-tokenizes each 6 | // time. So we do it once, eat the memory overhead, and help the linter avoid 7 | // constant re-tokenization. 8 | // NOTE: allocated in _arena 9 | tokens: TokenList.Slice, 10 | comments: CommentList.Slice, 11 | 12 | pub fn build( 13 | allocator: Allocator, 14 | source: [:0]const u8, 15 | ) Allocator.Error!struct { Parse, tokenizer.TokenBundle.Stats } { 16 | var token_bundle = try tokenizer.tokenize( 17 | allocator, 18 | source, 19 | ); 20 | errdefer { 21 | // NOTE: free'd in reverse order they're allocated 22 | token_bundle.comments.deinit(allocator); 23 | token_bundle.tokens.deinit(allocator); 24 | } 25 | 26 | const ast = try Ast.parse(allocator, source, .zig); 27 | return .{ 28 | Parse{ 29 | .ast = ast, 30 | .tokens = token_bundle.tokens, 31 | .comments = token_bundle.comments, 32 | }, 33 | token_bundle.stats, 34 | }; 35 | } 36 | 37 | pub fn deinit(self: *Parse, allocator: Allocator) void { 38 | self.ast.deinit(allocator); 39 | self.tokens.deinit(allocator); 40 | self.comments.deinit(allocator); 41 | } 42 | 43 | const std = @import("std"); 44 | const tokenizer = @import("tokenizer.zig"); 45 | const zig = @import("../zig.zig").@"0.14.1"; 46 | const Allocator = std.mem.Allocator; 47 | const Ast = zig.Ast; 48 | const TokenList = tokenizer.TokenList; 49 | const CommentList = tokenizer.CommentList; 50 | -------------------------------------------------------------------------------- /src/linter/RuleSet.zig: -------------------------------------------------------------------------------- 1 | rules: std.ArrayListUnmanaged(Rule.WithSeverity) = .{}, 2 | 3 | const RuleSet = @This(); 4 | 5 | /// Total number of all lint rules. 6 | pub const RULES_COUNT: usize = @typeInfo(all_rules).@"struct".decls.len; 7 | const ALL_RULE_IMPLS_SIZE: usize = Rule.MAX_SIZE * @typeInfo(all_rules).@"struct".decls.len; 8 | const ALL_RULES_SIZE: usize = @sizeOf(Rule.WithSeverity) * @typeInfo(all_rules).@"struct".decls.len; 9 | 10 | pub fn ensureTotalCapacityForAllRules(self: *RuleSet, arena: Allocator) Allocator.Error!void { 11 | try self.rules.ensureTotalCapacityPrecise(arena.allocator(), ALL_RULE_IMPLS_SIZE); 12 | } 13 | 14 | pub fn loadRulesFromConfig(self: *RuleSet, arena: Allocator, config: *const RulesConfig) !void { 15 | try self.rules.ensureUnusedCapacity(arena, ALL_RULES_SIZE); 16 | const info = @typeInfo(RulesConfig.Rules); 17 | inline for (info.@"struct".fields) |field| { 18 | const rule = @field(config.rules, field.name); 19 | if (rule.severity != Severity.off) { 20 | self.rules.appendAssumeCapacity(.{ 21 | .severity = rule.severity, 22 | // FIXME: unsafe const cast 23 | .rule = @constCast(&rule).rule(), 24 | }); 25 | } 26 | } 27 | } 28 | 29 | pub fn deinit(self: *RuleSet, arena: Allocator) void { 30 | self.rules.deinit(arena); 31 | } 32 | 33 | const std = @import("std"); 34 | const Allocator = std.mem.Allocator; 35 | const Rule = @import("rule.zig").Rule; 36 | const RulesConfig = @import("config/rules_config.zig").RulesConfig; 37 | const all_rules = @import("rules.zig"); 38 | const Severity = @import("../Error.zig").Severity; 39 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/block_comptime.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "x", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [2], 29 | 30 | }, 31 | { 32 | "name": "y", 33 | "debugName": "", 34 | "token": 7, 35 | "declNode": "simple_var_decl", 36 | "scope": 1, 37 | "flags": ["variable"], 38 | "references": [ 39 | {"symbol":2,"scope":1,"node":"identifier","identifier":"y","flags":["read","write"]}, 40 | {"symbol":2,"scope":1,"node":"identifier","identifier":"y","flags":["read"]}, 41 | 42 | ], 43 | "members": [], 44 | "exports": [], 45 | 46 | }, 47 | ], 48 | "unresolvedReferences": [], 49 | "modules": { 50 | "imports": [ 51 | ], 52 | }, 53 | "scopes": { 54 | "id": 0, 55 | "flags": ["top"], 56 | "bindings": { 57 | "@This()": 0, 58 | "x": 1, 59 | 60 | }, 61 | "children": [ 62 | { 63 | "id": 1, 64 | "flags": ["block","comptime"], 65 | "bindings": { 66 | "y": 2, 67 | 68 | }, 69 | "children": [], 70 | 71 | }, 72 | ], 73 | 74 | }, 75 | } -------------------------------------------------------------------------------- /apps/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zlint", 3 | "version": "0.0.0", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/DonIsaac/zlint", 8 | "directory": "apps/site" 9 | }, 10 | "scripts": { 11 | "docusaurus": "docusaurus", 12 | "start": "docusaurus start", 13 | "build": "docusaurus build", 14 | "swizzle": "docusaurus swizzle", 15 | "deploy": "docusaurus deploy", 16 | "clear": "docusaurus clear", 17 | "serve": "docusaurus serve", 18 | "write-translations": "docusaurus write-translations", 19 | "write-heading-ids": "docusaurus write-heading-ids", 20 | "typecheck": "tsc", 21 | "build:favicon": "pushd static/img && magick -background transparent -define 'icon:auto-resize=16,24,32,64' logo.svg favicon.ico" 22 | }, 23 | "dependencies": { 24 | "@docusaurus/core": "3.8.1", 25 | "@docusaurus/preset-classic": "3.8.1", 26 | "@mdx-js/react": "^3.0.0", 27 | "arktype": "^2.1.20", 28 | "clsx": "^2.0.0", 29 | "lucide-react": "^0.525.0", 30 | "prism-react-renderer": "^2.3.0", 31 | "react": "^19.0.0", 32 | "react-dom": "^19.0.0", 33 | "react-markdown": "^10.1.0" 34 | }, 35 | "devDependencies": { 36 | "@docusaurus/module-type-aliases": "3.8.1", 37 | "@docusaurus/tsconfig": "3.8.1", 38 | "@docusaurus/types": "3.8.1", 39 | "@types/react": "^19.2.2", 40 | "typescript": "~5.6.2" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.5%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 3 chrome version", 50 | "last 3 firefox version", 51 | "last 5 safari version" 52 | ] 53 | }, 54 | "engines": { 55 | "node": ">=18.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/homeless-try.snap: -------------------------------------------------------------------------------- 1 | 𝙭 homeless-try: `try` cannot be used in functions that do not return errors. 2 | ╭─[homeless-try.zig:2:4] 3 | 1 │ const std = @import("std"); 4 | 2 │ fn foo() void { 5 | · ─┬─ 6 | · ╰── function `foo` is declared here. 7 | 3 │ const x = try std.heap.page_allocator.alloc(u8, 8); 8 | · ─┬─ 9 | · ╰── it cannot propagate error unions. 10 | ╰──── 11 | help: Change the return type to `!void`. 12 | 13 | 𝙭 homeless-try: `try` cannot be used outside of a function or test block. 14 | ╭─[homeless-try.zig:2:11] 15 | 1 │ const std = @import("std"); 16 | 2 │ const x = try std.heap.page_allocator.alloc(u8, 8); 17 | · ─┬─ 18 | · ╰── there is nowhere to propagate errors to. 19 | ╰──── 20 | 21 | 𝙭 homeless-try: `try` cannot be used outside of a function or test block. 22 | ╭─[homeless-try.zig:4:17] 23 | 3 │ const Bar = struct { 24 | 4 │ baz: []u8 = try std.heap.page_allocator.alloc(u8, 8), 25 | · ─┬─ 26 | · ╰── there is nowhere to propagate errors to. 27 | 5 │ }; 28 | ╰──── 29 | 30 | 𝙭 homeless-try: `try` cannot be used in functions that do not return errors. 31 | ╭─[homeless-try.zig:2:8] 32 | 1 │ const std = @import("std"); 33 | 2 │ pub fn push(list: std.ArrayList(u32), x: u32, comptime assume_capacity: bool) if(assume_capacity) void else void { 34 | · ──┬── 35 | · ╰── function `push` is declared here. 36 | 3 │ if (comptime assume_capacity) { 37 | 5 │ } else { 38 | 6 │ try list.append(x); 39 | · ─┬─ 40 | · ╰── it cannot propagate error unions. 41 | 7 │ } 42 | ╰──── 43 | help: Change the return type to `!if(assume_capacity) void else void`. 44 | 45 | -------------------------------------------------------------------------------- /docs/rules/unused-decls.md: -------------------------------------------------------------------------------- 1 | # `unused-decls` 2 | 3 | > Category: correctness 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows container-scoped variables that are declared but never used. Note 10 | that top-level declarations are included. 11 | 12 | The Zig compiler checks for unused parameters, payloads bound by `if`, 13 | `catch`, etc, and `const`/`var` declaration within functions. However, 14 | variables and functions declared in container scopes are not given the same 15 | treatment. This rule handles those cases. 16 | 17 | > [!WARNING] 18 | > ZLint's semantic analyzer does not yet record references to variables on 19 | > member access expressions (e.g. `bar` on `foo.bar`). It also does not 20 | > handle method calls correctly. Until these features are added, only 21 | > top-level `const` variable declarations are checked. 22 | 23 | ## Examples 24 | 25 | Examples of **incorrect** code for this rule: 26 | 27 | ```zig 28 | // `std` used, but `Allocator` is not. 29 | const std = @import("std"); 30 | const Allocator = std.mem.Allocator; 31 | 32 | // Variables available to other code, either via `export` or `pub`, are not 33 | // reported. 34 | pub const x = 1; 35 | export fn foo(x: u32) void {} 36 | 37 | // `extern` functions are not reported 38 | extern fn bar(a: i32) void; 39 | ``` 40 | 41 | Examples of **correct** code for this rule: 42 | 43 | ```zig 44 | // Discarded variables are considered "used". 45 | const x = 1; 46 | _ = x; 47 | 48 | // non-container scoped variables are allowed by this rule but banned by the 49 | // compiler. `x`, `y`, and `z` are ignored by this rule. 50 | pub fn foo(x: u32) void { 51 | const y = true; 52 | var z: u32 = 1; 53 | } 54 | ``` 55 | 56 | ## Configuration 57 | 58 | This rule has no configuration. 59 | -------------------------------------------------------------------------------- /docs/rules/homeless-try.md: -------------------------------------------------------------------------------- 1 | # `homeless-try` 2 | 3 | > Category: compiler 4 | > 5 | > Enabled by default?: Yes (error) 6 | 7 | ## What This Rule Does 8 | 9 | Checks for `try` statements used outside of error-returning functions. 10 | 11 | As a `compiler`-level lint, this rule checks for errors also caught by the 12 | Zig compiler. 13 | 14 | ## Examples 15 | 16 | Examples of **incorrect** code for this rule: 17 | 18 | ```zig 19 | const std = @import("std"); 20 | 21 | var not_in_a_function = try std.heap.page_allocator.alloc(u8, 8); 22 | 23 | fn foo() void { 24 | var my_str = try std.heap.page_allocator.alloc(u8, 8); 25 | } 26 | 27 | fn bar() !void { 28 | const Baz = struct { 29 | property: u32 = try std.heap.page_allocator.alloc(u8, 8), 30 | }; 31 | } 32 | ``` 33 | 34 | Examples of **correct** code for this rule: 35 | 36 | ```zig 37 | fn foo() !void { 38 | var my_str = try std.heap.page_allocator.alloc(u8, 8); 39 | } 40 | ``` 41 | 42 | Zig allows `try` in comptime scopes in or nested within functions. This rule 43 | does not flag these cases. 44 | 45 | ```zig 46 | const std = @import("std"); 47 | fn foo(x: u32) void { 48 | comptime { 49 | // valid 50 | try bar(x); 51 | } 52 | } 53 | fn bar(x: u32) !void { 54 | return if (x == 0) error.Unreachable else void; 55 | } 56 | ``` 57 | 58 | Zig also allows `try` on functions whose error union sets are empty. ZLint 59 | does _not_ respect this case. Please refactor such functions to not return 60 | an error union. 61 | 62 | ```zig 63 | const std = @import("std"); 64 | fn foo() !u32 { 65 | // compiles, but treated as a violation. `bar` should return `u32`. 66 | const x = try bar(); 67 | return x + 1; 68 | } 69 | fn bar() u32 { 70 | return 1; 71 | } 72 | ``` 73 | 74 | ## Configuration 75 | 76 | This rule has no configuration. 77 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/line-length.snap: -------------------------------------------------------------------------------- 1 | 𝙭 line-length: line length of 196 characters is too big. 2 | ╭─[line-length.zig:3:1] 3 | 2 │ fn foo() std.mem.Allocator.Error!void { 4 | 3 │ // ok so this is a super unnecessary line that is artificially being made long through this self-referential comment thats keeps on going until hitting a number of columns that violates the rule 5 | · ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 6 | 4 │ _ = try std.heap.page_allocator.alloc(u8, 8); 7 | ╰──── 8 | 9 | 𝙭 line-length: line length of 126 characters is too big. 10 | ╭─[line-length.zig:2:1] 11 | 1 │ const std = @import("std"); 12 | 2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 13 | · ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 14 | 3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 15 | ╰──── 16 | 17 | 𝙭 line-length: line length of 151 characters is too big. 18 | ╭─[line-length.zig:3:1] 19 | 2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 }; 20 | 3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 { 21 | · ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 22 | 4 │ return 123; 23 | ╰──── 24 | 25 | -------------------------------------------------------------------------------- /docs/rules/useless-error-return.md: -------------------------------------------------------------------------------- 1 | # `useless-error-return` 2 | 3 | > Category: suspicious 4 | > 5 | > Enabled by default?: No 6 | 7 | ## What This Rule Does 8 | 9 | Detects functions that have an error union return type but never actually return an error. 10 | This can happen in two ways: 11 | 12 | 1. The function never returns an error value 13 | 2. The function catches all errors internally and never propagates them to the caller 14 | 15 | Having an error union return type when errors are never returned makes the code less clear 16 | and forces callers to handle errors that will never occur. 17 | 18 | ## Examples 19 | 20 | Examples of **incorrect** code for this rule: 21 | 22 | ```zig 23 | // Function declares error return but only returns void 24 | fn foo() !void { 25 | return; 26 | } 27 | 28 | // Function catches all errors internally 29 | pub fn init(allocator: std.mem.Allocator) !Foo { 30 | const new = allocator.create(Foo) catch @panic("OOM"); 31 | new.* = .{}; 32 | return new; 33 | } 34 | 35 | // Function only returns success value 36 | fn bar() !void { 37 | const e = baz(); 38 | return e; 39 | } 40 | ``` 41 | 42 | Examples of **correct** code for this rule: 43 | 44 | ```zig 45 | // Function properly propagates errors 46 | fn foo() !void { 47 | return error.Oops; 48 | } 49 | 50 | // Function returns result of fallible operation 51 | fn bar() !void { 52 | return baz(); 53 | } 54 | 55 | // Function propagates caught errors 56 | fn qux() !void { 57 | bar() catch |e| return e; 58 | } 59 | 60 | // Function with conditional error return 61 | fn check(x: bool) !void { 62 | return if (x) error.Invalid else {}; 63 | } 64 | 65 | // Empty error set is explicitly allowed 66 | fn noErrors() error{}!void {} 67 | ``` 68 | 69 | ## Configuration 70 | 71 | This rule has no configuration. 72 | -------------------------------------------------------------------------------- /apps/site/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import clsx from 'clsx' 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 4 | import Layout from '@theme/Layout' 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures' 6 | import Heading from '@theme/Heading' 7 | 8 | import styles from './index.module.css' 9 | import { GithubIcon, BookOpenIcon } from 'lucide-react' 10 | import Button from '../components/Button' 11 | 12 | function HomepageHeader() { 13 | const { siteConfig } = useDocusaurusContext() 14 | return ( 15 | 16 | 17 | 18 | Z 19 | Lint 20 | 21 | {siteConfig.tagline} 22 | 23 | 24 | Get Started 25 | 26 | 34 | GitHub 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | export default function Home(): ReactNode { 43 | const { siteConfig } = useDocusaurusContext() 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /apps/site/docs/configuration/ignore.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ignore 3 | title: Ignoring Files and Rules 4 | --- 5 | 6 | ZLint provides several ways to ignore files and specific rules, each with varying 7 | degrees of granularity. 8 | 9 | ## Ignoring Whole Files 10 | ZLint respects `.gitignore` files by default; no files ignored by git will be 11 | linted. 12 | 13 | To ignore additional files, provide a list of glob patterns in the `ignore` 14 | field of your `zlint.json` file. 15 | 16 | ```json title="zlint.json" 17 | { 18 | "ignore": ["src/test/**"], 19 | "rules": { /* ... */ } 20 | } 21 | ``` 22 | 23 | ## Disabling Rules 24 | You can globally disable rules by setting their level to `off` in your 25 | configuration file. 26 | 27 | ```json title="zlint.json" 28 | { 29 | "rules": { 30 | "unused-decls": "off" 31 | } 32 | } 33 | ``` 34 | 35 | ## Disabling Rules for a Single File 36 | 37 | You can use [ESLint-style disable 38 | directives](https://eslint.org/docs/latest/user-guide/configuring/ignoring-code) 39 | to disable one or more rules for a single file. Put a `// zlint-disable` comment 40 | at the top of your file to disable all rules, or add a list of rules to disable. 41 | You may put arbitrary text after `--` to explain why you're disabling the rules 42 | if you want. 43 | 44 | ```zig title="src/bad.zig" 45 | // zlint-disable unused-decls, unsafe-undefined -- this is an optional explanation 46 | 47 | const std = @import("std"); 48 | // highlight-next-line 49 | const unused = @import("./foo.zig"); // would normally be reported by `unused-decls` 50 | 51 | fn foo() u32 { 52 | // highlight-next-line 53 | var x: u32 = undefined; // would normally be reported by `unsafe-undefined` 54 | return x; 55 | } 56 | ``` 57 | 58 | :::warning 59 | Next-line disable directives are not yet supported. Track issue 60 | [#184](https://github.com/DonIsaac/zlint/issues/184) for updates. 61 | ::: 62 | -------------------------------------------------------------------------------- /apps/site/docs/rules/homeless-try.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"homeless-try","category":"compiler","default":"err","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `homeless-try` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Checks for `try` statements used outside of error-returning functions. 12 | 13 | As a `compiler`-level lint, this rule checks for errors also caught by the 14 | Zig compiler. 15 | 16 | ## Examples 17 | 18 | Examples of **incorrect** code for this rule: 19 | 20 | ```zig 21 | const std = @import("std"); 22 | 23 | var not_in_a_function = try std.heap.page_allocator.alloc(u8, 8); 24 | 25 | fn foo() void { 26 | var my_str = try std.heap.page_allocator.alloc(u8, 8); 27 | } 28 | 29 | fn bar() !void { 30 | const Baz = struct { 31 | property: u32 = try std.heap.page_allocator.alloc(u8, 8), 32 | }; 33 | } 34 | ``` 35 | 36 | Examples of **correct** code for this rule: 37 | 38 | ```zig 39 | fn foo() !void { 40 | var my_str = try std.heap.page_allocator.alloc(u8, 8); 41 | } 42 | ``` 43 | 44 | Zig allows `try` in comptime scopes in or nested within functions. This rule 45 | does not flag these cases. 46 | 47 | ```zig 48 | const std = @import("std"); 49 | fn foo(x: u32) void { 50 | comptime { 51 | // valid 52 | try bar(x); 53 | } 54 | } 55 | fn bar(x: u32) !void { 56 | return if (x == 0) error.Unreachable else void; 57 | } 58 | ``` 59 | 60 | Zig also allows `try` on functions whose error union sets are empty. ZLint 61 | does _not_ respect this case. Please refactor such functions to not return 62 | an error union. 63 | 64 | ```zig 65 | const std = @import("std"); 66 | fn foo() !u32 { 67 | // compiles, but treated as a violation. `bar` should return `u32`. 68 | const x = try bar(); 69 | return x + 1; 70 | } 71 | fn bar() u32 { 72 | return 1; 73 | } 74 | ``` 75 | 76 | ## Configuration 77 | 78 | This rule has no configuration. 79 | -------------------------------------------------------------------------------- /src/util/env.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const unicode = std.unicode; 4 | const posix = std.posix; 5 | 6 | const native_os = builtin.os.tag; 7 | 8 | /// Categories for environment values. Used to check flags, not when you actually 9 | /// want the value itself 10 | pub const ValueKind = enum { 11 | /// Environment variable is present 12 | defined, 13 | /// Environment variable has a "truthy" value (`1`, `on`, whatever) 14 | enabled, 15 | }; 16 | 17 | /// Check a flag-like environment variable. Whether the flag is "on" depends on 18 | /// `kind`: 19 | /// - `.defined`: `true` if the env var is present at all 20 | /// - `.enabled`: `true` if it has an affirmative value (`1` or `on`). 21 | /// Case-insensitive. 22 | pub fn checkEnvFlag(comptime key: []const u8, comptime kind: ValueKind) bool { 23 | if (kind == .defined) return std.process.hasEnvVarConstant(key); 24 | if (native_os == .windows) { 25 | const key_w = unicode.utf8ToUtf16LeStringLiteral(key); 26 | const value = std.process.getenvW(key_w) orelse return false; 27 | // true for 1, on 28 | // NOTE: yes? 29 | return switch (value.len) { 30 | 0 => false, 31 | 1 => value[0] == '1', 32 | 2 => (value[0] == 'o' or value[0] == 'O') and (value[1] == 'n' or value[1] == 'N'), 33 | else => false, 34 | }; 35 | } else if (native_os == .wasi and !builtin.link_libc) { 36 | @compileError("ahg we need to support WASI?"); 37 | } else { 38 | const value = posix.getenv(key) orelse return false; 39 | // true for 1, on 40 | // NOTE: yes? 41 | return switch (value.len) { 42 | 0 => false, 43 | 1 => value[0] == '1', 44 | 2 => (value[0] == 'o' or value[0] == 'O') and (value[1] == 'n' or value[1] == 'N'), 45 | else => false, 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/container_error.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "Error", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const","error"], 24 | "references": [ 25 | 26 | ], 27 | "members": [2,3], 28 | "exports": [], 29 | 30 | }, 31 | { 32 | "name": "Bar", 33 | "debugName": "", 34 | "token": 7, 35 | "declNode": "error_set_decl", 36 | "scope": 1, 37 | "flags": ["member","error"], 38 | "references": [ 39 | 40 | ], 41 | "members": [], 42 | "exports": [], 43 | 44 | }, 45 | { 46 | "name": "Foo", 47 | "debugName": "", 48 | "token": 5, 49 | "declNode": "error_set_decl", 50 | "scope": 1, 51 | "flags": ["member","error"], 52 | "references": [ 53 | 54 | ], 55 | "members": [], 56 | "exports": [], 57 | 58 | }, 59 | ], 60 | "unresolvedReferences": [], 61 | "modules": { 62 | "imports": [ 63 | ], 64 | }, 65 | "scopes": { 66 | "id": 0, 67 | "flags": ["top"], 68 | "bindings": { 69 | "@This()": 0, 70 | "Error": 1, 71 | 72 | }, 73 | "children": [ 74 | { 75 | "id": 1, 76 | "flags": ["error"], 77 | "bindings": { 78 | "Bar": 2, 79 | "Foo": 3, 80 | 81 | }, 82 | "children": [], 83 | 84 | }, 85 | ], 86 | 87 | }, 88 | } -------------------------------------------------------------------------------- /src/zig/0.14.1/primitives.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Set of primitive type and value names. 4 | /// Does not include `_` or integer type names. 5 | pub const names = std.StaticStringMap(void).initComptime(.{ 6 | .{"anyerror"}, 7 | .{"anyframe"}, 8 | .{"anyopaque"}, 9 | .{"bool"}, 10 | .{"c_int"}, 11 | .{"c_long"}, 12 | .{"c_longdouble"}, 13 | .{"c_longlong"}, 14 | .{"c_char"}, 15 | .{"c_short"}, 16 | .{"c_uint"}, 17 | .{"c_ulong"}, 18 | .{"c_ulonglong"}, 19 | .{"c_ushort"}, 20 | .{"comptime_float"}, 21 | .{"comptime_int"}, 22 | .{"f128"}, 23 | .{"f16"}, 24 | .{"f32"}, 25 | .{"f64"}, 26 | .{"f80"}, 27 | .{"false"}, 28 | .{"isize"}, 29 | .{"noreturn"}, 30 | .{"null"}, 31 | .{"true"}, 32 | .{"type"}, 33 | .{"undefined"}, 34 | .{"usize"}, 35 | .{"void"}, 36 | }); 37 | 38 | /// Returns true if a name matches a primitive type or value, excluding `_`. 39 | /// Integer type names like `u8` or `i32` are only matched for syntax, 40 | /// so this will still return true when they have an oversized bit count 41 | /// or leading zeroes. 42 | pub fn isPrimitive(name: []const u8) bool { 43 | if (names.get(name) != null) return true; 44 | if (name.len < 2) return false; 45 | const first_c = name[0]; 46 | if (first_c != 'i' and first_c != 'u') return false; 47 | for (name[1..]) |c| switch (c) { 48 | '0'...'9' => {}, 49 | else => return false, 50 | }; 51 | return true; 52 | } 53 | 54 | test isPrimitive { 55 | const expect = std.testing.expect; 56 | try expect(!isPrimitive("")); 57 | try expect(!isPrimitive("_")); 58 | try expect(!isPrimitive("haberdasher")); 59 | try expect(isPrimitive("bool")); 60 | try expect(isPrimitive("false")); 61 | try expect(isPrimitive("comptime_float")); 62 | try expect(isPrimitive("u1")); 63 | try expect(isPrimitive("i99999999999999")); 64 | } 65 | -------------------------------------------------------------------------------- /apps/site/docs/rules/unused-decls.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"unused-decls","category":"correctness","default":"warning","fix":{"kind":"fix","dangerous":true}}' 3 | --- 4 | 5 | # `unused-decls` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows container-scoped variables that are declared but never used. Note 12 | that top-level declarations are included. 13 | 14 | The Zig compiler checks for unused parameters, payloads bound by `if`, 15 | `catch`, etc, and `const`/`var` declaration within functions. However, 16 | variables and functions declared in container scopes are not given the same 17 | treatment. This rule handles those cases. 18 | 19 | :::warning 20 | 21 | ZLint's semantic analyzer does not yet record references to variables on 22 | member access expressions (e.g. `bar` on `foo.bar`). It also does not 23 | handle method calls correctly. Until these features are added, only 24 | top-level `const` variable declarations are checked. 25 | 26 | ::: 27 | 28 | ## Examples 29 | 30 | Examples of **incorrect** code for this rule: 31 | 32 | ```zig 33 | // `std` used, but `Allocator` is not. 34 | const std = @import("std"); 35 | const Allocator = std.mem.Allocator; 36 | 37 | // Variables available to other code, either via `export` or `pub`, are not 38 | // reported. 39 | pub const x = 1; 40 | export fn foo(x: u32) void {} 41 | 42 | // `extern` functions are not reported 43 | extern fn bar(a: i32) void; 44 | ``` 45 | 46 | Examples of **correct** code for this rule: 47 | 48 | ```zig 49 | // Discarded variables are considered "used". 50 | const x = 1; 51 | _ = x; 52 | 53 | // non-container scoped variables are allowed by this rule but banned by the 54 | // compiler. `x`, `y`, and `z` are ignored by this rule. 55 | pub fn foo(x: u32) void { 56 | const y = true; 57 | var z: u32 = 1; 58 | } 59 | ``` 60 | 61 | ## Configuration 62 | 63 | This rule has no configuration. 64 | -------------------------------------------------------------------------------- /apps/site/docs/rules/useless-error-return.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"useless-error-return","category":"suspicious","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `useless-error-return` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Detects functions that have an error union return type but never actually return an error. 12 | This can happen in two ways: 13 | 14 | 1. The function never returns an error value 15 | 2. The function catches all errors internally and never propagates them to the caller 16 | 17 | Having an error union return type when errors are never returned makes the code less clear 18 | and forces callers to handle errors that will never occur. 19 | 20 | ## Examples 21 | 22 | Examples of **incorrect** code for this rule: 23 | 24 | ```zig 25 | // Function declares error return but only returns void 26 | fn foo() !void { 27 | return; 28 | } 29 | 30 | // Function catches all errors internally 31 | pub fn init(allocator: std.mem.Allocator) !Foo { 32 | const new = allocator.create(Foo) catch @panic("OOM"); 33 | new.* = .{}; 34 | return new; 35 | } 36 | 37 | // Function only returns success value 38 | fn bar() !void { 39 | const e = baz(); 40 | return e; 41 | } 42 | ``` 43 | 44 | Examples of **correct** code for this rule: 45 | 46 | ```zig 47 | // Function properly propagates errors 48 | fn foo() !void { 49 | return error.Oops; 50 | } 51 | 52 | // Function returns result of fallible operation 53 | fn bar() !void { 54 | return baz(); 55 | } 56 | 57 | // Function propagates caught errors 58 | fn qux() !void { 59 | bar() catch |e| return e; 60 | } 61 | 62 | // Function with conditional error return 63 | fn check(x: bool) !void { 64 | return if (x) error.Invalid else {}; 65 | } 66 | 67 | // Empty error set is explicitly allowed 68 | fn noErrors() error{}!void {} 69 | ``` 70 | 71 | ## Configuration 72 | 73 | This rule has no configuration. 74 | -------------------------------------------------------------------------------- /src/source.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ptrs = @import("smart-pointers"); 3 | const fs = std.fs; 4 | 5 | const Allocator = std.mem.Allocator; 6 | const Arc = ptrs.Arc; 7 | const assert = std.debug.assert; 8 | 9 | pub const ArcStr = Arc([:0]u8); 10 | 11 | pub const Source = struct { 12 | // contents: Arc([]const u8), 13 | contents: ArcStr, 14 | pathname: ?[]const u8 = null, 15 | gpa: Allocator, 16 | 17 | /// Create a source from an opened file. This file must be opened with at least read permissions. 18 | /// 19 | /// Both `file` and `pathname` are moved into the source. 20 | pub fn init(gpa: Allocator, file: fs.File, pathname: ?[]const u8) !Source { 21 | defer file.close(); 22 | const meta = try file.metadata(); 23 | const contents = try gpa.allocSentinel(u8, meta.size(), 0); 24 | errdefer gpa.free(contents); 25 | const bytes_read = try file.readAll(contents); 26 | assert(bytes_read == meta.size()); 27 | return Source{ 28 | .contents = try ArcStr.init(gpa, contents), 29 | .pathname = pathname, 30 | .gpa = gpa, 31 | }; 32 | } 33 | /// Create a source file directly from a string. Takes ownership of both 34 | /// `contents` and `pathname`. 35 | /// 36 | /// Primarily used for testing. 37 | pub fn fromString(gpa: Allocator, contents: [:0]u8, pathname: ?[]const u8) Allocator.Error!Source { 38 | const contents_arc = try ArcStr.init(gpa, contents); 39 | return Source{ 40 | .contents = contents_arc, 41 | .pathname = pathname, 42 | .gpa = gpa, 43 | }; 44 | } 45 | 46 | pub inline fn text(self: *const Source) [:0]const u8 { 47 | return self.contents.deref().*; 48 | } 49 | 50 | pub fn deinit(self: *Source) void { 51 | self.contents.deinit(); 52 | if (self.pathname) |p| self.gpa.free(p); 53 | self.* = undefined; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/suppressed-errors.snap: -------------------------------------------------------------------------------- 1 | 𝙭 suppressed-errors: `catch` statement suppresses errors 2 | ╭─[suppressed-errors.zig:2:9] 3 | 1 │ fn foo() void { 4 | 2 │ bar() catch {}; 5 | · ──────── 6 | 3 │ } 7 | ╰──── 8 | help: Handle this error or propagate it to the caller with `try`. 9 | 10 | 𝙭 suppressed-errors: `catch` statement suppresses errors 11 | ╭─[suppressed-errors.zig:2:9] 12 | 1 │ fn foo() void { 13 | 2 │ bar() catch |_| {}; 14 | · ──────────── 15 | 3 │ } 16 | ╰──── 17 | help: Handle this error or propagate it to the caller with `try`. 18 | 19 | 𝙭 suppressed-errors: `catch` statement suppresses errors 20 | ╭─[suppressed-errors.zig:2:9] 21 | 1 │ fn foo() void { 22 | 2 │ bar() catch { 23 | · ─────── 24 | 3 │ // ignore 25 | ╰──── 26 | help: Handle this error or propagate it to the caller with `try`. 27 | 28 | 𝙭 suppressed-errors: Caught error is mishandled with `unreachable` 29 | ╭─[suppressed-errors.zig:2:15] 30 | 1 │ fn foo() void { 31 | 2 │ bar() catch unreachable; 32 | · ─────────── 33 | 3 │ } 34 | ╰──── 35 | help: Use `try` to propagate this error. If this branch shouldn't happen, use `@panic` or `std.debug.panic` instead. 36 | 37 | 𝙭 suppressed-errors: Caught error is mishandled with `unreachable` 38 | ╭─[suppressed-errors.zig:2:17] 39 | 1 │ fn foo() void { 40 | 2 │ bar() catch { unreachable; }; 41 | · ─────────── 42 | 3 │ } 43 | ╰──── 44 | help: Use `try` to propagate this error. If this branch shouldn't happen, use `@panic` or `std.debug.panic` instead. 45 | 46 | 𝙭 suppressed-errors: Caught error is mishandled with `unreachable` 47 | ╭─[suppressed-errors.zig:4:11] 48 | 3 │ break :blk w.print("{}", .{5}); 49 | 4 │ } catch unreachable; 50 | · ─────────── 51 | 5 │ } 52 | ╰──── 53 | help: Use `try` to propagate this error. If this branch shouldn't happen, use `@panic` or `std.debug.panic` instead. 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/js.yaml: -------------------------------------------------------------------------------- 1 | name: JavaScript 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: [opened, synchronize] 8 | paths-ignore: 9 | - "*.zig" 10 | - "*.zon" 11 | - "*.snap" 12 | - "*.md" 13 | - "*.sh" 14 | - ".vscode/*" 15 | - ".github/*" 16 | - "!.github/workflows/js.yaml" 17 | push: 18 | branches: 19 | - main 20 | paths-ignore: 21 | - "*.zig" 22 | - "*.zon" 23 | - "*.snap" 24 | - "*.md" 25 | - "*.sh" 26 | - ".vscode/*" 27 | - ".github/*" 28 | - "!.github/workflows/js.yaml" 29 | 30 | concurrency: 31 | group: ${{ github.workflow }}-${{ github.ref }} 32 | 33 | env: 34 | BUN_VERSION: latest 35 | 36 | jobs: 37 | lint: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: oven-sh/setup-bun@v2 42 | with: 43 | bun-version: ${{ env.BUN_VERSION }} 44 | - name: Oxlint 45 | run: bunx oxlint@latest --format github -D correctness -D suspicious -D perf 46 | 47 | check-vscode: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: oven-sh/setup-bun@v2 52 | with: 53 | bun-version: ${{ env.BUN_VERSION }} 54 | - name: Install Dependencies 55 | run: bun install --frozen-lockfile 56 | - name: Typecheck 57 | working-directory: apps/vscode-extension 58 | run: bun tsc --noEmit 59 | 60 | check-site: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v4 64 | - uses: oven-sh/setup-bun@v2 65 | with: 66 | bun-version: ${{ env.BUN_VERSION }} 67 | - name: Install Dependencies 68 | run: bun install --frozen-lockfile 69 | - name: Typecheck 70 | working-directory: apps/site 71 | run: bun typecheck 72 | - name: Build 73 | working-directory: apps/site 74 | run: bun run build 75 | 76 | -------------------------------------------------------------------------------- /docs/rules/suppressed-errors.md: -------------------------------------------------------------------------------- 1 | # `suppressed-errors` 2 | 3 | > Category: suspicious 4 | > 5 | > Enabled by default?: Yes (warning) 6 | 7 | ## What This Rule Does 8 | 9 | Disallows suppressing or otherwise mishandling caught errors. 10 | 11 | Functions that return error unions could error during "normal" execution. 12 | If they didn't, they would not return an error or would panic instead. 13 | 14 | This rule enforces that errors are 15 | 16 | 1. Propagated up to callers either implicitly or by returning a new error, 17 | ```zig 18 | const a = try foo(); 19 | const b = foo catch |e| { 20 | switch (e) { 21 | FooError.OutOfMemory => error.OutOfMemory, 22 | // ... 23 | } 24 | } 25 | ``` 26 | 2. Are inspected and handled to continue normal execution 27 | ```zig 28 | /// It's fine if users are missing a config file, and open() + err 29 | // handling is faster than stat() then open() 30 | var config?: Config = openConfig() catch null; 31 | ``` 32 | 3. Caught and `panic`ed on to provide better crash diagnostics 33 | ```zig 34 | const str = try allocator.alloc(u8, size) catch @panic("Out of memory"); 35 | ``` 36 | 37 | ## Examples 38 | 39 | Examples of **incorrect** code for this rule: 40 | 41 | ```zig 42 | const x = foo() catch {}; 43 | const y = foo() catch { 44 | // comments within empty catch blocks are still considered violations. 45 | }; 46 | // `unreachable` is for code that will never be reached due to invariants. 47 | const y = foo() catch unreachable 48 | ``` 49 | 50 | Examples of **correct** code for this rule: 51 | 52 | ```zig 53 | const x = foo() catch @panic("foo failed."); 54 | const y = foo() catch { 55 | std.debug.print("Foo failed.\n", .{}); 56 | }; 57 | const z = foo() catch null; 58 | // Writer errors may be safely ignored 59 | writer.print("{}", .{5}) catch {}; 60 | 61 | // suppression is allowed in tests 62 | test foo { 63 | foo() catch {}; 64 | } 65 | ``` 66 | 67 | ## Configuration 68 | 69 | This rule has no configuration. 70 | -------------------------------------------------------------------------------- /apps/vscode-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zlint-vscode", 3 | "displayName": "ZLint", 4 | "description": "A linter for the Zig programming language", 5 | "version": "0.0.0", 6 | "type": "commonjs", 7 | "main": "dist/extension.js", 8 | "private": true, 9 | "license": "MIT", 10 | "categories": [ 11 | "Linters" 12 | ], 13 | "publisher": "disaac", 14 | "author": { 15 | "name": "Don Isaac", 16 | "url": "https://github.com/DonIsaac" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/DonIsaac/zlint", 21 | "directory": "apps/vscode-extension" 22 | }, 23 | "engines": { 24 | "vscode": "^1.78.0" 25 | }, 26 | "scripts": { 27 | "package": "vsce package --no-dependencies -o zlint.vsix", 28 | "build:prod": "bun build.ts --production", 29 | "install-extension": "code --install-extension zlint.vsix --force", 30 | "all": "bun run build && bun package && bun install-extension" 31 | }, 32 | "devDependencies": { 33 | "@types/bun": "latest", 34 | "@types/vscode": "^1.78.0" 35 | }, 36 | "peerDependencies": { 37 | "typescript": "^5" 38 | }, 39 | "activationEvents": [ 40 | "onLanguage:zig" 41 | ], 42 | "contributes": { 43 | "commands": [ 44 | { 45 | "command": "zig.zlint.lint", 46 | "title": "Run ZLint", 47 | "category": "ZLint" 48 | } 49 | ], 50 | "configuration": { 51 | "title": "ZLint", 52 | "properties": { 53 | "zig.zlint.enabled": { 54 | "type": "boolean", 55 | "default": true, 56 | "description": "Enable ZLint" 57 | }, 58 | "zig.zlint.path": { 59 | "type": "string", 60 | "description": "Path to `zlint` binary. By default, it will use the one in your PATH." 61 | } 62 | } 63 | }, 64 | "jsonValidation": [ 65 | { 66 | "fileMatch": "zlint.json", 67 | "url": "https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/zlint.schema.json" 68 | } 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Semantic/test/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const _source = @import("../../source.zig"); 4 | const Semantic = @import("../../Semantic.zig"); 5 | const report = @import("../../reporter.zig"); 6 | 7 | const printer = @import("../../root.zig").printer; 8 | 9 | const t = std.testing; 10 | const print = std.debug.print; 11 | 12 | pub fn build(src: [:0]const u8) !Semantic { 13 | var r = try report.Reporter.graphical( 14 | std.io.getStdErr().writer().any(), 15 | t.allocator, 16 | report.formatter.Graphical.Theme.unicodeNoColor(), 17 | ); 18 | defer r.deinit(); 19 | var builder = Semantic.Builder.init(t.allocator); 20 | var source = try _source.Source.fromString( 21 | t.allocator, 22 | try t.allocator.dupeZ(u8, src), 23 | try t.allocator.dupe(u8, "test.zig"), 24 | ); 25 | defer source.deinit(); 26 | builder.withSource(&source); 27 | defer builder.deinit(); 28 | 29 | var result = builder.build(src) catch |e| { 30 | print("Analysis failed on source:\n\n{s}\n\n", .{src}); 31 | return e; 32 | }; 33 | errdefer result.value.deinit(); 34 | if (result.hasErrors()) { 35 | print("Analysis failed.\n", .{}); 36 | r.reportErrors(result.errors.toManaged(t.allocator)); 37 | print("\nSource:\n\n{s}\n\n", .{src}); 38 | return error.AnalysisFailed; 39 | } 40 | 41 | return result.value; 42 | } 43 | 44 | pub fn debugSemantic(semantic: *const Semantic) !void { 45 | var p = printer.Printer.init(t.allocator, std.io.getStdErr().writer()); 46 | defer p.deinit(); 47 | var sp = printer.SemanticPrinter.new(&p, semantic); 48 | 49 | print("Symbol table:\n\n", .{}); 50 | try sp.printSymbolTable(); 51 | 52 | print("\n\nUnresolved references:\n\n", .{}); 53 | try sp.printUnresolvedReferences(); 54 | 55 | print("\n\nScopes:\n\n", .{}); 56 | try sp.printScopeTree(); 57 | print("\n\n", .{}); 58 | 59 | print("\n\nModules:\n\n", .{}); 60 | try sp.printModuleRecord(); 61 | print("\n\n", .{}); 62 | } 63 | -------------------------------------------------------------------------------- /apps/site/docs/rules/allocator-first-param.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"allocator-first-param","category":"style","default":"off","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `allocator-first-param` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Checks that functions taking allocators as parameters have the allocator as 12 | the first parameter. This conforms to common Zig conventions. 13 | 14 | ## Rule Details 15 | 16 | This rule looks for functions take an `Allocator` parameter and reports a 17 | violation if 18 | 19 | - it is not the first parameter, or 20 | - there is a `self` parameter, and the allocator does not immediately follow it. 21 | 22 | Parameters are considered to be an allocator if 23 | 24 | - the are named `allocator`, `alloc`, `gpa`, or `arena`, or one of those 25 | with leading/trailing underscores, 26 | - their type ends with `Allocator` 27 | 28 | Parameters are considered to be a `self` parameter if 29 | 30 | - they are named `self`, `this`, or one of those with leading/trailing underscores. 31 | - their type is `@This()`, `*@This()`, etc. 32 | - their type is a Capitalized and the function is within the definition of a 33 | similarly named container (e.g. a struct). 34 | 35 | ## Examples 36 | 37 | Examples of **incorrect** code for this rule: 38 | 39 | ```zig 40 | fn foo(x: u32, allocator: Allocator) !*u32 { 41 | const heap_x = try allocator.create(u32); 42 | heap_x.* = x; 43 | return heap_x; 44 | } 45 | ``` 46 | 47 | Examples of **correct** code for this rule: 48 | 49 | ```zig 50 | fn foo(allocator: Allocator, x: u32) !*u32 { 51 | const heap_x = try allocator.create(u32); 52 | heap_x.* = x; 53 | return heap_x; 54 | } 55 | const Foo = struct { 56 | list: std.ArrayListUnmanaged(u32) = .{}, 57 | // when writing methods, `self` must be the first parameter 58 | pub fn expandCapacity(self: *Foo, allocator: Allocator, new_len: usize) !void { 59 | try self.list.ensureTotalCapacity(allocator, new_len); 60 | } 61 | }; 62 | ``` 63 | 64 | ## Configuration 65 | 66 | This rule accepts the following options: 67 | 68 | - ignore: array 69 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/case-convention.snap: -------------------------------------------------------------------------------- 1 | 𝙭 case-convention: Function ThisFunctionIsInPascalCase name is in PascalCase. It should be camelCase 2 | ╭─[case-convention.zig:1:4] 3 | 1 │ fn ThisFunctionIsInPascalCase() void {} 4 | · ────────────────────────── 5 | ╰──── 6 | 7 | 𝙭 case-convention: Function @"this-one-is-in-kebab-case" name is in kebab-case. It should be camelCase 8 | ╭─[case-convention.zig:1:4] 9 | 1 │ fn @"this-one-is-in-kebab-case"() void {} 10 | · ──────────────────────────── 11 | ╰──── 12 | 13 | 𝙭 case-convention: Function this_one_is_in_snake_case name is in snake_case. It should be camelCase 14 | ╭─[case-convention.zig:1:4] 15 | 1 │ fn this_one_is_in_snake_case() void {} 16 | · ───────────────────────── 17 | ╰──── 18 | 19 | 𝙭 case-convention: Function @"This-is-both-Pascal-and-Kebab-kinda" name is not in camelCase 20 | ╭─[case-convention.zig:1:4] 21 | 1 │ fn @"This-is-both-Pascal-and-Kebab-kinda"() void {} 22 | · ────────────────────────────────────── 23 | ╰──── 24 | 25 | 𝙭 case-convention: Function This_is_both_snake_case_and_pascal_kinda name is not in camelCase 26 | ╭─[case-convention.zig:1:4] 27 | 1 │ fn This_is_both_snake_case_and_pascal_kinda() void {} 28 | · ──────────────────────────────────────── 29 | ╰──── 30 | 31 | 𝙭 case-convention: Function This_is_both_snake_case_and_pascal_kinda name is not in camelCase 32 | ╭─[case-convention.zig:1:4] 33 | 1 │ fn This_is_both_snake_case_and_pascal_kinda(a: u32, b: u32, c: u32, d: u32) void {} 34 | · ──────────────────────────────────────── 35 | ╰──── 36 | 37 | 𝙭 case-convention: Function 'fooBar' returns a type, but does not use PascalCase 38 | ╭─[case-convention.zig:1:4] 39 | 1 │ fn fooBar() type { return u32; } 40 | · ────── 41 | ╰──── 42 | help: By convention, Zig uses PascalCase for structs, generics, and all other type variables. 43 | 44 | 𝙭 case-convention: Function NotGeneric name is in PascalCase. It should be camelCase 45 | ╭─[case-convention.zig:1:4] 46 | 1 │ fn NotGeneric(T: type) T { return T{}; } 47 | · ────────── 48 | ╰──── 49 | 50 | -------------------------------------------------------------------------------- /apps/site/docs/rules/suppressed-errors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"suppressed-errors","category":"suspicious","default":"warning","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `suppressed-errors` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows suppressing or otherwise mishandling caught errors. 12 | 13 | Functions that return error unions could error during "normal" execution. 14 | If they didn't, they would not return an error or would panic instead. 15 | 16 | This rule enforces that errors are 17 | 18 | 1. Propagated up to callers either implicitly or by returning a new error, 19 | ```zig 20 | const a = try foo(); 21 | const b = foo catch |e| { 22 | switch (e) { 23 | FooError.OutOfMemory => error.OutOfMemory, 24 | // ... 25 | } 26 | } 27 | ``` 28 | 2. Are inspected and handled to continue normal execution 29 | ```zig 30 | /// It's fine if users are missing a config file, and open() + err 31 | // handling is faster than stat() then open() 32 | var config?: Config = openConfig() catch null; 33 | ``` 34 | 3. Caught and `panic`ed on to provide better crash diagnostics 35 | ```zig 36 | const str = try allocator.alloc(u8, size) catch @panic("Out of memory"); 37 | ``` 38 | 39 | ## Examples 40 | 41 | Examples of **incorrect** code for this rule: 42 | 43 | ```zig 44 | const x = foo() catch {}; 45 | const y = foo() catch { 46 | // comments within empty catch blocks are still considered violations. 47 | }; 48 | // `unreachable` is for code that will never be reached due to invariants. 49 | const y = foo() catch unreachable 50 | ``` 51 | 52 | Examples of **correct** code for this rule: 53 | 54 | ```zig 55 | const x = foo() catch @panic("foo failed."); 56 | const y = foo() catch { 57 | std.debug.print("Foo failed.\n", .{}); 58 | }; 59 | const z = foo() catch null; 60 | // Writer errors may be safely ignored 61 | writer.print("{}", .{5}) catch {}; 62 | 63 | // suppression is allowed in tests 64 | test foo { 65 | foo() catch {}; 66 | } 67 | ``` 68 | 69 | ## Configuration 70 | 71 | This rule has no configuration. 72 | -------------------------------------------------------------------------------- /src/linter/disable_directives/Comment.zig: -------------------------------------------------------------------------------- 1 | //! A disable directive parsed from a comment. 2 | //! 3 | //! ## Examples 4 | //! ```zig 5 | //! // zlint-disable 6 | //! // zlint-disable no undefined 7 | //! // zlint-disable-next-line 8 | //! // zlint-disable-next-line foo bar baz 9 | //! ``` 10 | 11 | /// An empty set means all rules are disabled. 12 | disabled_rules: []Span = ALL_RULES_DISABLED, 13 | /// I'm not really sure what this should be the span _of_. The entire comment? 14 | /// Just the directive? Which is more useful? 15 | span: Span, 16 | kind: Kind, 17 | 18 | pub const DisableDirectiveComment = @This(); 19 | const ALL_RULES_DISABLED = &[_]Span{}; 20 | 21 | pub const Kind = enum { 22 | /// Disables a set of rules for an entire file. 23 | /// 24 | /// `zlint-disable` 25 | global, 26 | /// Just disable the next line. 27 | /// 28 | /// `zlint-disable-next-line` 29 | line, 30 | }; 31 | 32 | /// Returns `true` if this disable directive applies to an entire file. 33 | pub inline fn isGlobal(self: *const DisableDirectiveComment) bool { 34 | return self.kind == .global; 35 | } 36 | 37 | /// Does this directive disable diagnostics for all rules? 38 | /// 39 | /// _(say that 3 times fast lol)_ 40 | pub inline fn disablesAllRules(self: *const DisableDirectiveComment) bool { 41 | return self.disabled_rules.len == 0; 42 | } 43 | 44 | pub fn eql(self: DisableDirectiveComment, other: DisableDirectiveComment) bool { 45 | if (self.kind != other.kind or !self.span.eql(other.span)) return false; 46 | if (self.disabled_rules == other.disabled_rules) return true; 47 | if (self.disabled_rules.len != other.disabled_rules.len) return false; 48 | 49 | for (self.disabled_rules, 0..) |rule, i| { 50 | if (!rule.eql(other.disabled_rules[i])) return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | pub fn deinit(self: *DisableDirectiveComment, allocator: Allocator) void { 57 | if (self.disabled_rules.len > 0) allocator.free(self.disabled_rules); 58 | self.disabled_rules.ptr = undefined; 59 | self.* = undefined; 60 | } 61 | 62 | const std = @import("std"); 63 | 64 | const Allocator = std.mem.Allocator; 65 | const Span = @import("../../span.zig").Span; 66 | -------------------------------------------------------------------------------- /apps/site/docs/rules/no-print.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | rule: '{"name":"no-print","category":"restriction","default":"warning","fix":{"kind":"none","dangerous":false}}' 3 | --- 4 | 5 | # `no-print` 6 | 7 | 8 | 9 | ## What This Rule Does 10 | 11 | Disallows the use of `std.debug.print`. 12 | 13 | `print` statements are great for debugging, but they should be removed 14 | before code gets merged. When you need debug logs in production, use 15 | `std.log` instead. 16 | 17 | This rule makes a best-effort attempt to ensure `print` calls are actually 18 | from `std.debug.print`. It will not report calls to custom print functions 19 | if they are defined within the same file. If you are getting false positives 20 | because you import a custom print function, consider disabling this rule on 21 | a file-by-file basis instead of turning it off globally. 22 | 23 | ### Tests 24 | 25 | By default, this rule ignores `print`s in test blocks and files. Files are 26 | considered to be a test file if they end with `test.zig`. You may disable 27 | this by setting `allow_tests` to `false` in the rule's metadata. 28 | 29 | ```json 30 | { 31 | "rules": { 32 | "no-print": ["warn", { "allow_tests": false }] 33 | } 34 | } 35 | ``` 36 | 37 | ## Examples 38 | 39 | Examples of **incorrect** code for this rule: 40 | 41 | ```zig 42 | const std = @import("std"); 43 | const debug = std.debug; 44 | const print = std.debug.print; 45 | fn main() void { 46 | std.debug.print("This should not be here: {d}\n", .{42}); 47 | debug.print("This should not be here: {d}\n", .{42}); 48 | print("This should not be here: {d}\n", .{42}); 49 | } 50 | ``` 51 | 52 | Examples of **correct** code for this rule: 53 | 54 | ```zig 55 | const std = @import("std"); 56 | fn foo() u32 { 57 | std.log.debug("running foo", .{}); 58 | return 1; 59 | } 60 | 61 | test foo { 62 | std.debug.print("testing foo\n", .{}); 63 | try std.testing.expectEqual(1, foo()); 64 | } 65 | ``` 66 | 67 | ```zig 68 | fn print(comptime msg: []const u8, args: anytype) void { 69 | // ... 70 | } 71 | fn main() void { 72 | print("Staring program", .{}); 73 | } 74 | ``` 75 | 76 | ## Configuration 77 | 78 | This rule accepts the following options: 79 | 80 | - allow_tests: boolean 81 | -------------------------------------------------------------------------------- /src/cli/print_command.zig: -------------------------------------------------------------------------------- 1 | //! Hacky AST printer for debugging purposes. 2 | //! 3 | //! Resolves AST nodes and prints them as JSON. This can be safely piped into a file, since `std.debug.print` writes to stderr. 4 | //! 5 | //! ## Usage 6 | //! ```sh 7 | //! # note: right now, no target file can be specified. Run 8 | //! zig build run -- --print-ast | prettier --stdin-filepath foo.ast.json > tmp/foo.ast.json 9 | //! ``` 10 | const std = @import("std"); 11 | const Allocator = std.mem.Allocator; 12 | 13 | const Options = @import("../cli/Options.zig"); 14 | const Source = @import("../source.zig").Source; 15 | const Semantic = @import("../Semantic.zig"); 16 | 17 | const Printer = @import("../printer/Printer.zig"); 18 | const AstPrinter = @import("../printer/AstPrinter.zig"); 19 | const SemanticPrinter = @import("../printer/SemanticPrinter.zig"); 20 | 21 | /// Borrows source. 22 | pub fn parseAndPrint(alloc: Allocator, opts: Options, source: Source, writer_: ?std.io.AnyWriter) !void { 23 | var builder = Semantic.Builder.init(alloc); 24 | defer builder.deinit(); 25 | var sema_result = try builder.build(source.text()); 26 | defer sema_result.deinit(); 27 | if (sema_result.hasErrors()) { 28 | for (sema_result.errors.items) |err| { 29 | std.debug.print("{s}\n", .{err.message.str}); 30 | } 31 | return; 32 | } 33 | const sema = &sema_result.value; 34 | const writer = writer_ orelse std.io.getStdOut().writer().any(); 35 | var printer = Printer.init(alloc, writer); 36 | defer printer.deinit(); 37 | var ast_printer = AstPrinter.new(&printer, .{ .verbose = opts.verbose }, source, &sema.parse.ast); 38 | ast_printer.setNodeLinks(&sema.node_links); 39 | var semantic_printer = SemanticPrinter.new(&printer, &sema_result.value); 40 | 41 | try printer.pushObject(); 42 | defer printer.pop(); 43 | try printer.pPropName("ast"); 44 | try ast_printer.printAst(); 45 | try printer.pPropName("symbols"); 46 | try semantic_printer.printSymbolTable(); 47 | try printer.pPropName("scopes"); 48 | try semantic_printer.printScopeTree(); 49 | try printer.pPropName("modules"); 50 | try semantic_printer.printModuleRecord(); 51 | } 52 | 53 | test { 54 | _ = @import("test/print_ast_test.zig"); 55 | } 56 | -------------------------------------------------------------------------------- /apps/site/docs/configuration/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Configuration 6 | 7 | Create a `zlint.json` file in the same directory as `build.zig`. This disables 8 | all default rules, only enabling the ones you choose. 9 | 10 | :::tip 11 | `zlint.json` does not yet support comments or trailing commas yet. 12 | ::: 13 | 14 | ```json title="zlint.json" 15 | { 16 | "rules": { 17 | "unsafe-undefined": "error", 18 | "homeless-try": "warn", 19 | "unused-decls": "off" 20 | } 21 | } 22 | ``` 23 | 24 | ### Configuring Rules 25 | 26 | Some rules accept configuration options. To pass them, provide an `[level, config]` tuple. 27 | 28 | ```json title="zlint.json" 29 | { 30 | "rules": { 31 | "unsafe-undefined": ["error", { "allowed_types": ["PathBuf"] }] 32 | } 33 | } 34 | ``` 35 | 36 | ### Skipping Files 37 | 38 | To skip linting specific files or groups of files, use the `ignore` field. 39 | 40 | ```json title="zlint.json" 41 | { 42 | "ignore": ["src/bad.zig", "src/subfolder/**"], 43 | "rules": { 44 | "unsafe-undefined": "error" 45 | } 46 | } 47 | ``` 48 | 49 | :::tip 50 | See [Ignoring Files](./ignore.md) for more details, including on how to 51 | disable specific rules for a single file. 52 | ::: 53 | 54 | ## Intellisense 55 | 56 | If you don't use ZLint's [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=disaac.zlint-vscode), 57 | you can get intellisense by linking directly to our [JSON Schema](https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/zlint.schema.json). 58 | 59 | ```json title="zlint.json" 60 | { 61 | // highlight-next-line 62 | "$schema": "https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/zlint.schema.json", 63 | "rules": { 64 | "unsafe-undefined": "error", 65 | "homeless-try": "warn" 66 | } 67 | } 68 | ``` 69 | 70 | Instead of using the schema directly from main, you can lock it to the specific version you use. 71 | 72 | :::warning 73 | When copying the below snippet, make sure you swap in the version of ZLint you are using! 74 | ::: 75 | 76 | ```json title="zlint.json" 77 | { 78 | // highlight-next-line 79 | "$schema": "https://raw.githubusercontent.com/DonIsaac/zlint/refs/tags/v0.7.7/zlint.schema.json", 80 | "rules": { 81 | "unsafe-undefined": "error", 82 | "homeless-try": "warn" 83 | } 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /src/reporter/StringWriter.zig: -------------------------------------------------------------------------------- 1 | /// A `std.io.Writer` that writes data to an internally managed, allocated 2 | /// buffer. 3 | const StringWriter = @This(); 4 | const Writer = io.GenericWriter(*StringWriter, Allocator.Error, write); 5 | 6 | buf: std.ArrayList(u8), 7 | 8 | /// Create a new, empty `StringWriter`. Does not allocate memory. 9 | pub fn init(allocator: Allocator) StringWriter { 10 | return StringWriter{ .buf = .init(allocator) }; 11 | } 12 | /// Free this `StringWriter`'s internal buffer. 13 | pub fn deinit(self: *StringWriter) void { 14 | self.buf.deinit(); 15 | } 16 | 17 | /// Create a new `StringWriter` that pre-allocates enough memory for at least 18 | /// `capacity` bytes. 19 | pub fn initCapacity(capacity: usize, allocator: Allocator) Allocator.Error!StringWriter { 20 | const buf = try std.ArrayList(u8).initCapacity(allocator, capacity); 21 | return StringWriter{ .buf = buf }; 22 | } 23 | 24 | /// Get the bytes written to this `StringWriter`. 25 | pub inline fn slice(self: *const StringWriter) []const u8 { 26 | return self.buf.items; 27 | } 28 | 29 | pub fn writer(self: *StringWriter) Writer { 30 | return Writer{ .context = self }; 31 | } 32 | 33 | /// Write `bytes` to this writer. Returns the number of bytes written. 34 | pub fn write(self: *StringWriter, bytes: []const u8) Allocator.Error!usize { 35 | try self.buf.appendSlice(bytes); 36 | return bytes.len; 37 | } 38 | 39 | /// Write a formatted string to this `StringWriter`. 40 | pub fn print(self: *StringWriter, comptime format: []const u8, args: anytype) Allocator.Error!void { 41 | const size = math.cast(usize, fmt.count(format, args)) orelse return error.OutOfMemory; 42 | try self.buf.ensureUnusedCapacity(size); 43 | const len = self.buf.items.len; 44 | self.buf.items.len += size; 45 | _ = fmt.bufPrint(self.buf.items[len..], format, args) catch |err| switch (err) { 46 | error.NoSpaceLeft => unreachable, // we just counted the size above 47 | }; 48 | } 49 | 50 | const std = @import("std"); 51 | const io = std.io; 52 | const fmt = std.fmt; 53 | const math = std.math; 54 | const Allocator = std.mem.Allocator; 55 | 56 | test print { 57 | const t = std.testing; 58 | var w = StringWriter.init(t.allocator); 59 | defer w.deinit(); 60 | 61 | try w.print("Hello, {s}", .{"world"}); 62 | try t.expectEqualStrings("Hello, world", w.slice()); 63 | } 64 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/struct_members.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "Foo", 19 | "debugName": "", 20 | "token": 2, 21 | "declNode": "simple_var_decl", 22 | "scope": 0, 23 | "flags": ["variable","const","struct"], 24 | "references": [ 25 | 26 | ], 27 | "members": [2], 28 | "exports": [3,4], 29 | 30 | }, 31 | { 32 | "name": "a", 33 | "debugName": "", 34 | "token": 6, 35 | "declNode": "container_field_init", 36 | "scope": 1, 37 | "flags": ["member","struct"], 38 | "references": [ 39 | 40 | ], 41 | "members": [], 42 | "exports": [], 43 | 44 | }, 45 | { 46 | "name": "B", 47 | "debugName": "", 48 | "token": 11, 49 | "declNode": "simple_var_decl", 50 | "scope": 1, 51 | "flags": ["variable","const"], 52 | "references": [ 53 | 54 | ], 55 | "members": [], 56 | "exports": [], 57 | 58 | }, 59 | { 60 | "name": "C", 61 | "debugName": "", 62 | "token": 19, 63 | "declNode": "simple_var_decl", 64 | "scope": 1, 65 | "flags": ["variable","const"], 66 | "references": [ 67 | 68 | ], 69 | "members": [], 70 | "exports": [], 71 | 72 | }, 73 | ], 74 | "unresolvedReferences": [], 75 | "modules": { 76 | "imports": [ 77 | ], 78 | }, 79 | "scopes": { 80 | "id": 0, 81 | "flags": ["top"], 82 | "bindings": { 83 | "@This()": 0, 84 | "Foo": 1, 85 | 86 | }, 87 | "children": [ 88 | { 89 | "id": 1, 90 | "flags": ["struct","block"], 91 | "bindings": { 92 | "a": 2, 93 | "B": 3, 94 | "C": 4, 95 | 96 | }, 97 | "children": [], 98 | 99 | }, 100 | ], 101 | 102 | }, 103 | } -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/fn_add.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [], 14 | "exports": [1], 15 | 16 | }, 17 | { 18 | "name": "add", 19 | "debugName": "", 20 | "token": 1, 21 | "declNode": "fn_decl", 22 | "scope": 0, 23 | "flags": ["fn"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [], 29 | 30 | }, 31 | { 32 | "name": "x", 33 | "debugName": "", 34 | "token": 3, 35 | "declNode": "identifier", 36 | "scope": 1, 37 | "flags": ["const","fn_param"], 38 | "references": [ 39 | {"symbol":2,"scope":2,"node":"identifier","identifier":"x","flags":["read"]}, 40 | 41 | ], 42 | "members": [], 43 | "exports": [], 44 | 45 | }, 46 | { 47 | "name": "y", 48 | "debugName": "", 49 | "token": 7, 50 | "declNode": "identifier", 51 | "scope": 1, 52 | "flags": ["const","fn_param"], 53 | "references": [ 54 | {"symbol":3,"scope":2,"node":"identifier","identifier":"y","flags":["read"]}, 55 | 56 | ], 57 | "members": [], 58 | "exports": [], 59 | 60 | }, 61 | ], 62 | "unresolvedReferences": [], 63 | "modules": { 64 | "imports": [ 65 | ], 66 | }, 67 | "scopes": { 68 | "id": 0, 69 | "flags": ["top"], 70 | "bindings": { 71 | "@This()": 0, 72 | "add": 1, 73 | 74 | }, 75 | "children": [ 76 | { 77 | "id": 1, 78 | "flags": ["function"], 79 | "bindings": { 80 | "x": 2, 81 | "y": 3, 82 | 83 | }, 84 | "children": [ 85 | { 86 | "id": 2, 87 | "flags": ["function","block"], 88 | "bindings": { 89 | 90 | }, 91 | "children": [], 92 | 93 | }, 94 | ], 95 | 96 | }, 97 | ], 98 | 99 | }, 100 | } -------------------------------------------------------------------------------- /src/Semantic/builtins.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const StaticStringSet = std.StaticStringMap(void); 3 | 4 | /// ## References 5 | /// - [Zig Docs - Primitive Types](https://ziglang.org/documentation/0.13.0/#Primitive-Types) 6 | const PRIMITIVE_TYPES = StaticStringSet.initComptime([_]struct { []const u8 }{ 7 | // integers 8 | .{"i8"}, 9 | .{"i16"}, 10 | .{"i32"}, 11 | .{"i64"}, 12 | .{"i128"}, 13 | .{"isize"}, 14 | .{"u8"}, // unsigned 15 | .{"u16"}, 16 | .{"u32"}, 17 | .{"u64"}, 18 | .{"u128"}, 19 | .{"usize"}, 20 | 21 | // floats 22 | .{"f16"}, 23 | .{"f32"}, 24 | .{"f64"}, 25 | .{"f80"}, 26 | .{"f128"}, 27 | 28 | // c types 29 | .{"c_char"}, 30 | .{"c_short"}, 31 | .{"c_int"}, 32 | .{"c_long"}, 33 | .{"c_longlong"}, 34 | .{"c_longdouble"}, 35 | .{"c_ushort"}, // unsigned 36 | .{"c_uint"}, 37 | .{"c_ulong"}, 38 | .{"c_ulonglong"}, 39 | 40 | // etc 41 | .{"bool"}, 42 | .{"void"}, 43 | .{"anyopaque"}, 44 | .{"noreturn"}, 45 | .{"type"}, 46 | .{"anyerror"}, 47 | .{"comptime_int"}, 48 | .{"comptime_float"}, 49 | }); 50 | 51 | /// ## References 52 | /// - [Zig Docs - Primitive Values](https://ziglang.org/documentation/0.13.0/#Primitive-Values) 53 | const PRIMITIVE_VALUES = StaticStringSet.initComptime([_]struct { []const u8 }{ 54 | .{"null"}, 55 | .{"undefined"}, 56 | .{"true"}, 57 | .{"false"}, 58 | }); 59 | 60 | /// Check if a type is built in to Zig itself. 61 | /// 62 | /// ## References 63 | /// - [Zig Docs - Primitive Types](https://ziglang.org/documentation/0.13.0/#Primitive-Types) 64 | pub fn isPrimitiveType(typename: []const u8) bool { 65 | if (PRIMITIVE_TYPES.has(typename)) return true; 66 | 67 | // Zig allows arbitrary-sized integers. 68 | if (typename.len > 2 and (typename[0] == 'u' or typename[0] == 'i')) { 69 | for (1..typename.len) |i| switch (typename[i]) { 70 | '0'...'9' => {}, 71 | else => return false, 72 | }; 73 | 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /// Check if an identifier refers to a primitive value. 81 | /// 82 | /// ## References 83 | /// - [Zig Docs - Primitive Values](https://ziglang.org/documentation/0.13.0/#Primitive-Values) 84 | pub fn isPrimitiveValue(value: []const u8) bool { 85 | return PRIMITIVE_VALUES.has(value); 86 | } 87 | -------------------------------------------------------------------------------- /apps/site/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import clsx from 'clsx' 3 | import Heading from '@theme/Heading' 4 | import styles from './styles.module.css' 5 | import Link from '@docusaurus/Link' 6 | 7 | type FeatureItem = { 8 | title: string 9 | Svg?: React.ComponentType> 10 | description: ReactNode 11 | } 12 | 13 | const FeatureList: FeatureItem[] = [ 14 | { 15 | title: '🔍 Custom Analysis', 16 | // Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 17 | description: ( 18 | <> 19 | ZLint's custom semantic analyzer, inspired by Oxc, is 20 | completely separate from the Zig compiler. This means that 21 | ZLint still checks and understands code that may otherwise be ignored by Zig due to dead code elimination 22 | > 23 | ), 24 | }, 25 | { 26 | title: '⚡️ Fast', 27 | // Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 28 | description: ( 29 | <> 30 | Designed from the ground-up to be highly performant, ZLint runs in just a few hundred milliseconds. 31 | > 32 | ), 33 | }, 34 | { 35 | title: '💡 Understandable', 36 | // Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 37 | description: ( 38 | <> 39 | Error messages are pretty, detailed, and easy to understand. Most rules come with explanations on how to fix 40 | them and what exactly is wrong. 41 | > 42 | ), 43 | }, 44 | ] 45 | 46 | function Feature({ title, Svg, description }: FeatureItem) { 47 | return ( 48 | 49 | {Svg && } 50 | 51 | {title} 52 | {description} 53 | 54 | 55 | ) 56 | } 57 | 58 | export default function HomepageFeatures(): ReactNode { 59 | return ( 60 | 61 | 62 | 63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 | 67 | 68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/Semantic/test/scopes_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Semantic = @import("../../Semantic.zig"); 3 | 4 | const t = std.testing; 5 | const build = @import("./util.zig").build; 6 | 7 | const Scope = Semantic.Scope; 8 | 9 | test "functions create a parameter list + function body scope" { 10 | const source = 11 | \\fn add(x: i32, y: i32) i32 { 12 | \\ return x + y; 13 | \\} 14 | ; 15 | var sem = try build(source); 16 | defer sem.deinit(); 17 | const scopes = sem.scopes.scopes; 18 | 19 | const root: Scope = scopes.get(0); 20 | try t.expectEqual(Semantic.ROOT_NODE_ID, root.node); 21 | try t.expectEqual(Semantic.ROOT_SCOPE_ID, root.id); 22 | try t.expectEqual(2, sem.scopes.getBindings(root.id).len); // `add` + implicit file-level container 23 | 24 | // root scope only has one child: the function declaration 25 | var children = &sem.scopes.children.items[0]; 26 | try t.expectEqual(1, children.items.len); 27 | 28 | // check the function declaration. This scope contains the parameter list 29 | const fn_decl: Scope = scopes.get(children.items[0].int()); 30 | try t.expectEqual(root.id.int() + 1, fn_decl.id.int()); 31 | try t.expectEqual(fn_decl.flags, Scope.Flags{ .s_function = true }); 32 | try t.expectEqual(root.id, fn_decl.parent.unwrap()); 33 | 34 | // it has two parameters: x and y 35 | { 36 | const bindings = sem.scopes.getBindings(fn_decl.id); 37 | try t.expectEqual(2, bindings.len); 38 | 39 | const x = sem.symbols.get(bindings[0]); 40 | try t.expectEqualStrings("x", x.name); 41 | try t.expectEqual(fn_decl.id, x.scope); 42 | 43 | const y = sem.symbols.get(bindings[1]); 44 | try t.expectEqualStrings("y", y.name); 45 | try t.expectEqual(fn_decl.id, y.scope); 46 | } 47 | 48 | // next child is the function body 49 | children = &sem.scopes.children.items[fn_decl.id.int()]; 50 | try t.expectEqual(1, children.items.len); 51 | const fn_body: Scope = scopes.get(children.items[0].int()); 52 | try t.expectEqual(fn_decl.id.int() + 1, fn_body.id.int()); 53 | try t.expectEqual(fn_decl.id.int(), fn_body.parent.unwrap().?.int()); 54 | try t.expectEqual(fn_body.flags, Scope.Flags{ .s_function = true, .s_block = true }); 55 | 56 | // this is the last one 57 | children = &sem.scopes.children.items[fn_body.id.int()]; 58 | try t.expectEqual(0, children.items.len); 59 | try t.expectEqual(3, scopes.len); // root + decl + body 60 | } 61 | -------------------------------------------------------------------------------- /src/linter/test/lint_context_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const LinterContext = @import("../lint_context.zig"); 3 | const Fix = @import("../fix.zig").Fix; 4 | const Semantic = @import("../../Semantic.zig"); 5 | const _span = @import("../../span.zig"); 6 | const Source = @import("../../source.zig").Source; 7 | 8 | const t = std.testing; 9 | const print = std.debug.print; 10 | 11 | fn createCtx(src: [:0]const u8, sema_out: *Semantic, source_out: *Source) !LinterContext { 12 | const src2 = try t.allocator.dupeZ(u8, src); 13 | var source = try Source.fromString(t.allocator, src2, null); 14 | errdefer source.deinit(); 15 | source_out.* = source; 16 | 17 | var builder = Semantic.Builder.init(t.allocator); 18 | builder.withSource(&source); 19 | defer builder.deinit(); 20 | 21 | var res = try builder.build(src); 22 | defer res.deinitErrors(); 23 | if (res.hasErrors()) { 24 | defer res.value.deinit(); 25 | print("Semantic analysis failed with {d} errors. Source:\n\n{s}\n", .{ res.errors.items.len, src }); 26 | return error.TestFailed; 27 | } 28 | sema_out.* = res.value; 29 | return LinterContext.init(t.allocator, sema_out, source_out); 30 | } 31 | 32 | test "Dangerous fixes do not get saved when only safe fixes are allowed" { 33 | var sema: Semantic = undefined; 34 | var source: Source = undefined; 35 | var ctx = try createCtx("fn foo() void { return 1; }", &sema, &source); 36 | defer { 37 | var diagnostics = ctx.takeDiagnostics(); 38 | for (diagnostics.items) |*diagnostic| { 39 | diagnostic.deinit(t.allocator); 40 | } 41 | diagnostics.deinit(); 42 | ctx.deinit(); 43 | } 44 | defer sema.deinit(); 45 | defer source.deinit(); 46 | 47 | const DangerousFixer = struct { 48 | span: _span.Span, 49 | 50 | fn remove(self: @This(), b: Fix.Builder) anyerror!Fix { 51 | var fix = b.delete(self.span); 52 | fix.meta.dangerous = true; 53 | return fix; 54 | } 55 | }; 56 | 57 | const fix_ctx = DangerousFixer{ .span = _span.Span.EMPTY }; 58 | ctx.reportWithFix( 59 | fix_ctx, 60 | ctx.diagnostic("ahhh", .{_span.LabeledSpan.from(fix_ctx.span)}), 61 | &DangerousFixer.remove, 62 | ); 63 | 64 | try t.expectEqual(1, ctx.diagnostics.items.len); 65 | try t.expectEqual(null, ctx.diagnostics.items[0].fix); 66 | try t.expectEqualStrings("ahhh", ctx.diagnostics.items[0].err.message.borrow()); 67 | } 68 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("util"); 3 | const Source = @import("source.zig").Source; 4 | const config = @import("config"); 5 | const Error = @import("./Error.zig"); 6 | 7 | const fs = std.fs; 8 | const print = std.debug.print; 9 | 10 | const Options = @import("./cli/Options.zig"); 11 | const print_cmd = @import("cli/print_command.zig"); 12 | const lint_cmd = @import("cli/lint_command.zig"); 13 | 14 | // in debug builds, include more information for debugging memory leaks, 15 | // double-frees, etc. 16 | const DebugAllocator = std.heap.GeneralPurposeAllocator(.{ 17 | .never_unmap = util.IS_DEBUG, 18 | .retain_metadata = util.IS_DEBUG, 19 | }); 20 | var debug_allocator = DebugAllocator.init; 21 | pub fn main() !u8 { 22 | const alloc = if (comptime util.IS_DEBUG) 23 | debug_allocator.allocator() 24 | else 25 | std.heap.smp_allocator; 26 | 27 | defer if (comptime util.IS_DEBUG) { 28 | _ = debug_allocator.deinit(); 29 | }; 30 | var stack = std.heap.stackFallback(16, alloc); 31 | const stack_alloc = stack.get(); 32 | 33 | var err: Error = undefined; 34 | var opts = Options.parseArgv(stack_alloc, &err) catch { 35 | std.debug.print("{s}\n{s}\n", .{ err.message, Options.usage }); 36 | err.deinit(stack_alloc); 37 | return 1; 38 | }; 39 | defer opts.deinit(stack_alloc); 40 | 41 | if (opts.version) { 42 | const stdout = std.io.getStdOut().writer(); 43 | stdout.print("{s}\n", .{config.version}) catch |e| { 44 | std.debug.panic("Failed to write version: {s}\n", .{@errorName(e)}); 45 | }; 46 | return 0; 47 | } else if (opts.print_ast) { 48 | if (opts.args.items.len == 0) { 49 | print("No files to print\nUsage: zlint --print-ast [filename]", .{}); 50 | std.process.exit(1); 51 | } 52 | 53 | const relative_path = opts.args.items[0]; 54 | print("Printing AST for {s}\n", .{relative_path}); 55 | const file = try fs.cwd().openFile(relative_path, .{}); 56 | errdefer file.close(); 57 | var source = try Source.init(alloc, file, null); 58 | defer source.deinit(); 59 | try print_cmd.parseAndPrint(alloc, opts, source, null); 60 | return 0; 61 | } 62 | 63 | return lint_cmd.lint(alloc, opts); 64 | } 65 | 66 | test { 67 | std.testing.refAllDecls(@This()); 68 | std.testing.refAllDecls(@import("visit/walk.zig")); 69 | std.testing.refAllDecls(@import("json.zig")); 70 | } 71 | -------------------------------------------------------------------------------- /src/linter/config/rules_config.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const json = std.json; 3 | const meta = std.meta; 4 | const mem = std.mem; 5 | const Schema = @import("../../json.zig").Schema; 6 | 7 | const Allocator = std.mem.Allocator; 8 | 9 | const assert = std.debug.assert; 10 | 11 | const ParseError = json.ParseError(json.Scanner); 12 | 13 | /// Configuration for all rules. 14 | /// 15 | /// The bulk of this strict is auto-generated from all registered rules via 16 | /// `tasks/confgen.zig`. 17 | pub const RulesConfig = struct { 18 | pub const Rules = @import("rules_config_rules.zig"); 19 | 20 | rules: Rules = .{}, 21 | /// See: `std.json.parseFromTokenSource()` 22 | pub fn jsonParse( 23 | allocator: Allocator, 24 | source: *json.Scanner, 25 | options: json.ParseOptions, 26 | ) !RulesConfig { 27 | var rules = Rules{}; 28 | 29 | // eat '{' 30 | if (try source.next() != .object_begin) return ParseError.UnexpectedToken; 31 | 32 | while (try source.peekNextTokenType() != .object_end) { 33 | const key_tok = try source.next(); 34 | const key = switch (key_tok) { 35 | .string => key_tok.string, 36 | else => return ParseError.UnexpectedToken, 37 | }; 38 | 39 | var found = false; 40 | inline for (meta.fields(Rules)) |field| { 41 | const RuleConfigImpl = @TypeOf(@field(rules, field.name)); 42 | if (mem.eql(u8, key, RuleConfigImpl.name)) { 43 | @field(rules, field.name) = try RuleConfigImpl.jsonParse(allocator, source, options); 44 | found = true; 45 | break; 46 | } 47 | } 48 | if (!found) return ParseError.UnknownField; 49 | } 50 | 51 | // eat '}' 52 | const end = try source.next(); 53 | assert(end == .object_end); 54 | 55 | return .{ .rules = rules }; 56 | } 57 | 58 | pub fn jsonSchema(ctx: *Schema.Context) !Schema { 59 | const info = @typeInfo(RulesConfig.Rules).@"struct"; 60 | var obj = try ctx.object(info.fields.len); 61 | inline for (info.fields) |field| { 62 | const Rule = field.type; 63 | var prop_schema: Schema = try Rule.jsonSchema(ctx); 64 | prop_schema.common().default = .{ .string = Rule.meta.default.asSlice() }; 65 | obj.properties.putAssumeCapacityNoClobber(Rule.name, prop_schema); 66 | } 67 | obj.common.description = "Configure which rules are enabled and how."; 68 | 69 | return .{ .object = obj }; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/Semantic/test/members_and_exports_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const test_util = @import("util.zig"); 3 | 4 | const Symbol = @import("../Symbol.zig"); 5 | 6 | const t = std.testing; 7 | const print = std.debug.print; 8 | const build = test_util.build; 9 | 10 | test "container exports" { 11 | const TestCase = struct { 12 | src: [:0]const u8, 13 | container_id: Symbol.Id, 14 | exports: []const []const u8, 15 | fn init(src: [:0]const u8, container_id: Symbol.Id.Repr, exports: anytype) @This() { 16 | return .{ .src = src, .container_id = Symbol.Id.from(container_id), .exports = exports }; 17 | } 18 | }; 19 | 20 | const test_cases = [_]TestCase{ 21 | .init( 22 | "const foo = 1;", 23 | 0, 24 | &[_][]const u8{"foo"}, 25 | ), 26 | .init( 27 | \\const Foo = struct { 28 | \\ a: u32, 29 | \\ b: u32, 30 | \\ const C = 1; 31 | \\ pub const D = struct {}; 32 | \\ fn e() void {} 33 | \\}; 34 | , 35 | 1, 36 | &[_][]const u8{ "C", "D", "e" }, 37 | ), 38 | }; 39 | 40 | for (test_cases) |tc| { 41 | var semantic = try build(tc.src); 42 | defer semantic.deinit(); 43 | const container = semantic.symbols.get(tc.container_id); 44 | t.expectEqual(tc.exports.len, container.exports.items.len) catch |e| { 45 | print("\nexports: ", .{}); 46 | for (container.exports.items) |symbol_id| { 47 | const symbol = semantic.symbols.get(symbol_id); 48 | print("'{s}', ", .{symbol.name}); 49 | } 50 | print("\n", .{}); 51 | return e; 52 | }; 53 | for (tc.exports) |expected_export| { 54 | var found = false; 55 | for (container.exports.items) |symbol_id| { 56 | const symbol = semantic.symbols.get(symbol_id); 57 | if (std.mem.eql(u8, symbol.name, expected_export)) { 58 | found = true; 59 | break; 60 | } 61 | } 62 | if (!found) { 63 | print("expected export {s} not found\nexports: ", .{expected_export}); 64 | for (container.exports.items) |symbol_id| { 65 | const symbol = semantic.symbols.get(symbol_id); 66 | print("'{s}'', ", .{symbol.name}); 67 | } 68 | print("\n", .{}); 69 | return error.ZigTestFailed; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | pub const RUNTIME_SAFETY = builtin.mode != .ReleaseFast; 5 | pub const IS_DEBUG = builtin.mode == .Debug; 6 | pub const IS_TEST = builtin.is_test; 7 | pub const IS_WINDOWS = builtin.target.os.tag == .windows; 8 | pub const NEWLINE = if (IS_WINDOWS) "\r\n" else "\n"; 9 | 10 | pub const @"inline": std.builtin.CallingConvention = if (IS_DEBUG) .Inline else .Unspecified; 11 | 12 | pub const env = @import("util/env.zig"); 13 | pub const NominalId = @import("util/id.zig").NominalId; 14 | pub const Cow = @import("util/cow.zig").Cow; 15 | pub const DebugOnly = @import("util/debug_only.zig").DebugOnly; 16 | pub const debugOnly = @import("util/debug_only.zig").debugOnly; 17 | pub const Bitflags = @import("util/bitflags.zig").Bitflags; 18 | pub const FeatureFlags = @import("util/feature_flags.zig"); 19 | 20 | /// remove leading and trailing whitespace characters from a string 21 | pub fn trimWhitespace(s: []const u8) []const u8 { 22 | return std.mem.trim(u8, s, &std.ascii.whitespace); 23 | } 24 | pub fn trimWhitespaceRight(s: []const u8) []const u8 { 25 | return std.mem.trimRight(u8, s, &std.ascii.whitespace); 26 | } 27 | pub fn isWhitespace(c: u8) bool { 28 | return std.mem.indexOfScalar(u8, &std.ascii.whitespace, c) != null; 29 | } 30 | 31 | /// Assert that `condition` is true, panicking if it is not. 32 | /// 33 | /// Behaves identically to `std.debug.assert`, except that assertions will fail 34 | /// with a formatted message in debug builds. `fmt` and `args` follow the same 35 | /// formatting conventions as `std.debug.print` and `std.debug.panic`. 36 | /// 37 | /// Similarly to `std.debug.assert`, undefined behavior is invoked if 38 | /// `condition` is false. In `ReleaseFast` mode, `unreachable` is stripped and 39 | /// assumed to be true by the compiler, which will lead to strange program 40 | /// behavior. 41 | pub inline fn assert(condition: bool, comptime fmt: []const u8, args: anytype) void { 42 | if (comptime IS_DEBUG) { 43 | if (!condition) std.debug.panic(fmt, args); 44 | } else { 45 | if (!condition) unreachable; 46 | } 47 | } 48 | 49 | pub inline fn debugAssert(condition: bool, comptime fmt: []const u8, args: anytype) void { 50 | if (comptime IS_DEBUG) { 51 | if (!condition) std.debug.panic(fmt, args); 52 | } 53 | } 54 | 55 | pub inline fn assertUnsafe(condition: bool) void { 56 | if (comptime IS_DEBUG) { 57 | if (!condition) @panic("assertion failed"); 58 | } else { 59 | @setRuntimeSafety(IS_DEBUG); 60 | if (!condition) unreachable; 61 | } 62 | } 63 | 64 | test { 65 | std.testing.refAllDeclsRecursive(@This()); 66 | } 67 | -------------------------------------------------------------------------------- /apps/site/src/components/RuleBanner.tsx: -------------------------------------------------------------------------------- 1 | import { type } from 'arktype' 2 | import { Rule } from '@site/src/types' 3 | import styles from './RuleBanner.module.css' 4 | import Badge from './Badge' 5 | import { Variant } from '../theme/types' 6 | import clsx from 'clsx' 7 | import Alert from './Alert' 8 | import { WrenchIcon } from 'lucide-react' 9 | import { capitalize, withAOrAn } from '../utils' 10 | 11 | const Props = type({ 12 | category: Rule.Category, 13 | fix: Rule.FixMeta.optional(), 14 | default: Rule.Severity, 15 | }) 16 | type Props = typeof Props.inferIn 17 | 18 | export default function RuleBanner(props: Props) { 19 | const { category, fix, default: defaultSeverity } = Props.assert(props) 20 | 21 | return ( 22 | 23 | 24 | {capitalize(category)} 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | const SeverityBanner = ({ severity }: { severity: Rule.Severity | null | undefined }) => { 35 | if (!severity || severity === 'off') return null 36 | let severityText: string | undefined 37 | switch (severity) { 38 | case 'err': 39 | severityText = 'an error' 40 | break 41 | case 'warning': 42 | severityText = 'a warning' 43 | break 44 | case 'notice': 45 | severityText = 'a notice' 46 | break 47 | } 48 | const variant = ( 49 | { 50 | err: 'danger', 51 | warning: 'warning', 52 | notice: 'info', 53 | } as const 54 | )[severity] 55 | return ( 56 | 57 | This rule is enabled as {severityText} by default. 58 | 59 | ) 60 | } 61 | 62 | const FixBanner = ({ fix }: { fix: Rule.FixMeta | null | undefined }) => { 63 | let fixText: string | undefined 64 | if (!fix || fix.kind === 'none') return null 65 | switch (fix.kind) { 66 | case 'fix': 67 | fixText = fix.dangerous ? 'dangerous fix' : 'auto-fix' 68 | break 69 | case 'suggestion': 70 | fixText = fix.dangerous ? 'dangerous suggestion' : 'suggestion' 71 | } 72 | return ( 73 | 74 | This rule has {withAOrAn(fixText)} available 75 | 76 | ) 77 | } 78 | 79 | const categoryVariants: Partial> = { 80 | nursery: 'danger', 81 | } 82 | const categoryVariant = (category: Rule.Category) => categoryVariants[category] ?? 'info' 83 | -------------------------------------------------------------------------------- /test/semantic/ecosystem_coverage.zig: -------------------------------------------------------------------------------- 1 | const test_runner = @import("../harness.zig"); 2 | 3 | const std = @import("std"); 4 | const fs = std.fs; 5 | const Allocator = std.mem.Allocator; 6 | const print = std.debug.print; 7 | 8 | const zlint = @import("zlint"); 9 | const Source = zlint.Source; 10 | 11 | const utils = @import("../utils.zig"); 12 | const Repo = utils.Repo; 13 | 14 | const REPOS_DIR = "zig-out/repos"; 15 | 16 | // SAFETY: globalSetup is always run before this is read 17 | var repos: std.json.Parsed([]Repo) = undefined; 18 | 19 | const SemanticError = zlint.semantic.Semantic.Builder.SemanticError; 20 | var is_tty: bool = false; 21 | 22 | pub fn globalSetup(alloc: Allocator) !void { 23 | is_tty = std.io.getStdErr().isTty(); 24 | var repos_dir_fd = fs.cwd().openDir(REPOS_DIR, .{}) catch |e| { 25 | switch (e) { 26 | error.FileNotFound => { 27 | print("Could not find git repos to test, please run `just submodules`", .{}); 28 | return e; 29 | }, 30 | else => return e, 31 | } 32 | }; 33 | repos_dir_fd.close(); 34 | repos = try Repo.load(alloc); 35 | } 36 | 37 | pub fn globalTeardown(_: Allocator) void { 38 | repos.deinit(); 39 | } 40 | 41 | fn isTTY_() bool { 42 | return std.io.getStdErr().isTTY(); 43 | } 44 | 45 | fn testSemantic(alloc: Allocator, source: *const Source) !void { 46 | { 47 | const p = source.pathname orelse ""; 48 | print("ecosystem coverage: {s}", .{p}); 49 | if (is_tty) 50 | print(" \r", .{}) 51 | else 52 | print("\n", .{}); 53 | } 54 | var builder = zlint.Semantic.Builder.init(alloc); 55 | defer builder.deinit(); 56 | var res = try builder.build(source.text()); 57 | defer res.deinit(); 58 | if (res.hasErrors()) return error.AnalysisFailed; 59 | } 60 | 61 | // const fns: test_runner.TestSuite.TestSuiteFns = .{ 62 | // .test_fn = &testSemantic, 63 | // .setup_fn = &globalSetup, 64 | // .teardown_fn = &globalTeardown, 65 | // }; 66 | 67 | pub fn run(alloc: Allocator) !void { 68 | for (repos.value) |repo| { 69 | const repo_dir = try utils.TestFolders.openRepo(alloc, repo.name); 70 | var suite = try test_runner.TestSuite.init(alloc, repo_dir, "semantic-coverage", repo.name, .{ .test_fn = &testSemantic }); 71 | defer suite.deinit(); 72 | 73 | try suite.run(); 74 | } 75 | } 76 | 77 | pub const SUITE = test_runner.TestFile{ 78 | .name = "semantic_coverage", 79 | .globalSetup = globalSetup, 80 | .deinit = globalTeardown, 81 | .run = run, 82 | }; 83 | -------------------------------------------------------------------------------- /test/snapshots/snapshot-coverage/simple/pass/top_level_struct.zig.snap: -------------------------------------------------------------------------------- 1 | { 2 | "symbols": [ 3 | { 4 | "name": "", 5 | "debugName": "@This()", 6 | "token": null, 7 | "declNode": "root", 8 | "scope": 0, 9 | "flags": ["const"], 10 | "references": [ 11 | 12 | ], 13 | "members": [1,2], 14 | "exports": [3,4], 15 | 16 | }, 17 | { 18 | "name": "a", 19 | "debugName": "", 20 | "token": 0, 21 | "declNode": "container_field_init", 22 | "scope": 0, 23 | "flags": ["member","struct"], 24 | "references": [ 25 | 26 | ], 27 | "members": [], 28 | "exports": [], 29 | 30 | }, 31 | { 32 | "name": "b", 33 | "debugName": "", 34 | "token": 4, 35 | "declNode": "container_field_init", 36 | "scope": 0, 37 | "flags": ["member","struct"], 38 | "references": [ 39 | 40 | ], 41 | "members": [], 42 | "exports": [], 43 | 44 | }, 45 | { 46 | "name": "Foo", 47 | "debugName": "", 48 | "token": 14, 49 | "declNode": "simple_var_decl", 50 | "scope": 0, 51 | "flags": ["variable","const"], 52 | "references": [ 53 | {"symbol":3,"scope":1,"node":"identifier","identifier":"Foo","flags":["type"]}, 54 | {"symbol":3,"scope":2,"node":"identifier","identifier":"Foo","flags":["read"]}, 55 | 56 | ], 57 | "members": [], 58 | "exports": [], 59 | 60 | }, 61 | { 62 | "name": "new", 63 | "debugName": "", 64 | "token": 22, 65 | "declNode": "fn_decl", 66 | "scope": 0, 67 | "flags": ["fn"], 68 | "references": [ 69 | 70 | ], 71 | "members": [], 72 | "exports": [], 73 | 74 | }, 75 | ], 76 | "unresolvedReferences": [], 77 | "modules": { 78 | "imports": [ 79 | ], 80 | }, 81 | "scopes": { 82 | "id": 0, 83 | "flags": ["top"], 84 | "bindings": { 85 | "@This()": 0, 86 | "a": 1, 87 | "b": 2, 88 | "Foo": 3, 89 | "new": 4, 90 | 91 | }, 92 | "children": [ 93 | { 94 | "id": 1, 95 | "flags": ["function"], 96 | "bindings": { 97 | 98 | }, 99 | "children": [ 100 | { 101 | "id": 2, 102 | "flags": ["function","block"], 103 | "bindings": { 104 | 105 | }, 106 | "children": [], 107 | 108 | }, 109 | ], 110 | 111 | }, 112 | ], 113 | 114 | }, 115 | } -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | env: 9 | ZIG_VERSION: 0.14.1 10 | 11 | permissions: 12 | id-token: write 13 | contents: write 14 | attestations: write 15 | 16 | jobs: 17 | build: 18 | strategy: 19 | # keep building others in case it's os-specific 20 | fail-fast: false 21 | matrix: 22 | os: [linux, windows, macos] 23 | arch: [aarch64, x86_64] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: mlugg/setup-zig@v2 28 | with: 29 | version: ${{ env.ZIG_VERSION }} 30 | 31 | - name: Get binary name 32 | id: binary_name 33 | run: | 34 | binary_name=zlint-${{ matrix.os }}-${{ matrix.arch }} 35 | if [[ "${{ matrix.os }}" == "windows" ]] ; then 36 | binary_name="${binary_name}.exe" 37 | fi 38 | echo "binary_name=$binary_name" >> "$GITHUB_OUTPUT" 39 | 40 | - name: zig build 41 | env: 42 | BINARY_NAME: ${{ steps.binary_name.outputs.binary_name }} 43 | run: | 44 | binary_name=zlint 45 | if [[ "${{ matrix.os }}" == "windows" ]] ; then 46 | binary_name="${binary_name}.exe" 47 | fi 48 | zig build --summary all --color on \ 49 | --release=safe \ 50 | -Dversion=${{ github.ref_name }} \ 51 | -Dtarget=${{ matrix.arch }}-${{ matrix.os }} 52 | mv zig-out/bin/${binary_name} zig-out/bin/${{ env.BINARY_NAME }} 53 | 54 | - name: Generate artifact attestation 55 | uses: actions/attest-build-provenance@v1 56 | env: 57 | BINARY_NAME: ${{ steps.binary_name.outputs.binary_name }} 58 | with: 59 | subject-path: zig-out/bin/${{ env.BINARY_NAME }} 60 | 61 | - name: Upload zlint binary 62 | uses: actions/upload-artifact@v4 63 | env: 64 | BINARY_NAME: ${{ steps.binary_name.outputs.binary_name }} 65 | with: 66 | path: zig-out/bin/${{ env.BINARY_NAME}} 67 | name: ${{ env.BINARY_NAME }} 68 | retention-days: 1 69 | 70 | release: 71 | # Not used. Leaving here in case we want to create canary builds 72 | if: startsWith(github.ref, 'refs/tags/') 73 | runs-on: ubuntu-latest 74 | needs: 75 | - build 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | - name: Download release artifacts 80 | uses: actions/download-artifact@v4 81 | with: 82 | pattern: zlint-* 83 | path: zig-out/dist 84 | merge-multiple: true 85 | - name: Release 86 | uses: softprops/action-gh-release@v2 87 | with: 88 | generate_release_notes: true 89 | files: zig-out/dist/* 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️ ZLint 2 | 3 | [](https://codecov.io/gh/DonIsaac/zlint) 4 | [](https://github.com/DonIsaac/zlint/actions/workflows/ci.yaml) 5 | [](https://discord.gg/UcB7HjJxcG) 6 | 7 | An opinionated linter for the Zig programming language. 8 | 9 | ## ✨ Features 10 | 11 | - 🔍 **Custom Analysis**. ZLint has its own semantic analyzer, heavily inspired 12 | by [the Oxc project](https://github.com/oxc-project/oxc), that is completely 13 | separate from the Zig compiler. This means that ZLint still checks and 14 | understands code that may otherwise be ignored by Zig due to dead code 15 | elimination. 16 | - ⚡️ **Fast**. Designed from the ground-up to be highly performant, ZLint 17 | typically takes a few hundred milliseconds to lint large projects. 18 | - 💡 **Understandable**. Error messages are pretty, detailed, and easy to understand. 19 | Most rules come with explanations on how to fix them and what _exactly_ is wrong. 20 |  21 | 22 | ## 📦 Installation 23 | Pre-built binaries for Windows, MacOS, and Linux on x64 and aarch64 are 24 | available [for each release](https://github.com/DonIsaac/zlint/releases/latest). 25 | 26 | ```sh 27 | curl -fsSL https://raw.githubusercontent.com/DonIsaac/zlint/refs/heads/main/tasks/install.sh | bash 28 | ``` 29 | 30 | > [!WARNING] 31 | > This installation script does not work on Windows. Please download Windows 32 | > binaries directly from the releases page. 33 | 34 | ### 🔨 Building from Source 35 | 36 | Clone this repo and compile the project with Zig. 37 | 38 | ```sh 39 | zig build --release=safe 40 | ``` 41 | 42 | ## ⚡️ Lint Rules 43 | 44 | All lints and what they do can be found [here](docs/rules/). 45 | 46 | ## ⚙️ Configuration 47 | 48 | Create a `zlint.json` file in the same directory as `build.zig`. This disables 49 | all default rules, only enabling the ones you choose. 50 | 51 | ```json 52 | { 53 | "rules": { 54 | "unsafe-undefined": "error", 55 | "homeless-try": "warn" 56 | } 57 | } 58 | ``` 59 | 60 | ZLint also supports [ESLint-like disable directives](https://eslint.org/docs/latest/use/configure/rules#comment-descriptions) to turn off some or all rules for a specific file. 61 | 62 | ```zig 63 | // zlint-disable unsafe-undefined -- We need to come back and fix this later 64 | const x: i32 = undefined; 65 | ``` 66 | 67 | ## 🙋♂️ Contributing 68 | 69 | If you have any rule ideas, please add them to the [rule ideas 70 | board](https://github.com/DonIsaac/zlint/issues/3). 71 | 72 | Interested in contributing code? Check out the [contributing 73 | guide](CONTRIBUTING.md). 74 | -------------------------------------------------------------------------------- /src/reporter/formatters/JSONFormatter.zig: -------------------------------------------------------------------------------- 1 | //! Formats diagnostics in a such a way that they appear as annotations in 2 | //! Github Actions. 3 | //! 4 | //! e.g. 5 | //! ``` 6 | //! ::error file={name},line={line},endLine={endLine},title={title}::{message} 7 | //! ``` 8 | 9 | const JSONFormatter = @This(); 10 | 11 | pub const meta: Meta = .{ 12 | .report_statistics = false, 13 | }; 14 | 15 | pub fn format(_: *JSONFormatter, w: *Writer, e: Error) FormatError!void { 16 | return std.json.stringify(e, .{}, w.*); 17 | } 18 | 19 | test JSONFormatter { 20 | const Source = @import("../../source.zig").Source; 21 | const json = std.json; 22 | const Value = json.Value; 23 | const expect = std.testing.expect; 24 | const expectEqual = std.testing.expectEqual; 25 | const expectEqualStrings = std.testing.expectEqualStrings; 26 | 27 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 28 | defer arena.deinit(); 29 | const allocator = arena.allocator(); 30 | 31 | const source: [:0]const u8 = "const x: u32 = 1;"; 32 | const src = try Source.fromString(allocator, @constCast(source), "test.zig"); 33 | 34 | var buf = std.ArrayList(u8).init(allocator); 35 | defer buf.deinit(); 36 | 37 | var err = Error.newStatic("oof"); 38 | err.source = src.contents; 39 | err.source_name = src.pathname; 40 | err.help = Cow.static("help pls"); 41 | err.code = "code"; 42 | try err.labels.append(allocator, LabeledSpan{ 43 | .label = Cow.static("some label"), 44 | .span = _span.Span.new(0, 4), 45 | .primary = true, 46 | }); 47 | 48 | var f = JSONFormatter{}; 49 | var w = buf.writer().any(); 50 | try f.format(&w, err); 51 | 52 | var value = try json.parseFromSlice(json.Value, allocator, buf.items, .{}); 53 | defer value.deinit(); 54 | const obj = value.value.object; 55 | 56 | try expectEqualStrings("oof", obj.get("message").?.string); 57 | try expectEqualStrings("code", obj.get("code").?.string); 58 | try expectEqualStrings("help pls", obj.get("help").?.string); 59 | const labels = obj.get("labels") orelse return error.ZigTestFailing; 60 | try expect(labels == .array); 61 | try expectEqual(1, labels.array.items.len); 62 | const label = labels.array.items[0].object; 63 | try expectEqual(Value{ .bool = true }, label.get("primary")); 64 | try expectEqualStrings("some label", label.get("label").?.string); 65 | try expect(label.get("start").? == .object); 66 | try expect(label.get("end").? == .object); 67 | } 68 | 69 | const std = @import("std"); 70 | const Cow = @import("util").Cow(false); 71 | const formatter = @import("../formatter.zig"); 72 | const Meta = formatter.Meta; 73 | const FormatError = formatter.FormatError; 74 | const Writer = std.io.AnyWriter; 75 | const Error = @import("../../Error.zig"); 76 | const _span = @import("../../span.zig"); 77 | const LabeledSpan = _span.LabeledSpan; 78 | -------------------------------------------------------------------------------- /tasks/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | platform=$(uname -ms) 5 | 6 | if [[ ${OS:-} = Windows_NT ]]; then 7 | if [[ $platform != MINGW64* ]]; then 8 | # powershell -c "irm zlint.sh/install.ps1|iex" 9 | # exit $? 10 | echo "zlint's install does not support Windows yet. Please download a copy of zlint here: https://github.com/DonIsaac/zlint/releases/latest" 11 | exit 1 12 | fi 13 | fi 14 | 15 | # Reset 16 | Color_Off='' 17 | 18 | # Regular Colors 19 | Red='' 20 | Green='' 21 | Dim='' # White 22 | 23 | # Bold 24 | Bold_White='' 25 | Bold_Green='' 26 | 27 | if [[ -t 1 ]]; then 28 | # Reset 29 | Color_Off='\033[0m' # Text Reset 30 | 31 | # Regular Colors 32 | Red='\033[0;31m' # Red 33 | Green='\033[0;32m' # Green 34 | Dim='\033[0;2m' # White 35 | 36 | # Bold 37 | Bold_Green='\033[1;32m' # Bold Green 38 | Bold_White='\033[1m' # Bold White 39 | fi 40 | 41 | error() { 42 | echo -e "${Red}error${Color_Off}:" "$@" >&2 43 | exit 1 44 | } 45 | 46 | info() { 47 | echo -e "${Dim}$@ ${Color_Off}" 48 | } 49 | 50 | info_bold() { 51 | echo -e "${Bold_White}$@ ${Color_Off}" 52 | } 53 | 54 | success() { 55 | echo -e "${Green}$@ ${Color_Off}" 56 | } 57 | 58 | case $platform in 59 | 'Darwin x86_64') 60 | target=macos-x86_64 61 | ;; 62 | 'Darwin arm64') 63 | target=macos-aarch64 64 | ;; 65 | 'Linux aarch64' | 'Linux arm64') 66 | target=linux-aarch64 67 | ;; 68 | 'MINGW64'*) 69 | target=windows-x86_64 70 | ;; 71 | 'Linux x86_64' | *) 72 | target=linux-x86_64 73 | ;; 74 | esac 75 | 76 | if [[ $target = macos-x86_64 ]]; then 77 | # Is this process running in Rosetta? 78 | # redirect stderr to devnull to avoid error message when not running in Rosetta 79 | if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then 80 | target=macos-aarch64 81 | info "Your shell is running in Rosetta 2. Downloading bun for $target instead" 82 | fi 83 | fi 84 | 85 | GITHUB=${GITHUB-"https://github.com"} 86 | github_repo="$GITHUB/DonIsaac/zlint" 87 | 88 | if [[ $# = 0 ]]; then 89 | zlint_uri=$github_repo/releases/latest/download/zlint-$target 90 | else 91 | zlint_uri=$github_repo/releases/download/$1/zlint-$target 92 | fi 93 | 94 | # macos/linux cross-compat mktemp 95 | # https://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x 96 | tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'zlint') 97 | install_dir=/usr/local/bin 98 | 99 | curl --fail --location --progress-bar --output "$tmpdir/zlint" "$zlint_uri" || 100 | error "Failed to download zlint from \"$zlint_uri\"" 101 | chmod +x "$tmpdir/zlint" 102 | 103 | # Check if user can write to install directory 104 | if [[ ! -w $install_dir ]]; then 105 | info "Saving zlint to $install_dir. You will be prompted for your password." 106 | sudo mv "$tmpdir/zlint" "$install_dir" 107 | success "zlint installed to $install_dir/zlint" 108 | else 109 | mv "$tmpdir/zlint" "$install_dir" 110 | success "zlint installed to $install_dir/zlint" 111 | fi 112 | -------------------------------------------------------------------------------- /src/linter/rules/snapshots/duplicate-case.snap: -------------------------------------------------------------------------------- 1 | 𝙭 duplicate-case: Switch statement has duplicate cases 2 | ╭─[duplicate-case.zig:3:5] 3 | 2 │ const x = switch (1) { 4 | 3 │ 1 => 1, 5 | · ────── 6 | 4 │ else => 1, 7 | · ───────── 8 | ╰──── 9 | 10 | 𝙭 duplicate-case: Switch statement has duplicate cases 11 | ╭─[duplicate-case.zig:3:5] 12 | 2 │ const x = switch (1) { 13 | 3 │ 1 => y + 1, 14 | · ────────── 15 | 4 │ else => 1 + y, 16 | · ───────────── 17 | ╰──── 18 | 19 | 𝙭 duplicate-case: Switch statement has duplicate cases 20 | ╭─[duplicate-case.zig:3:5] 21 | 2 │ const x = switch (y) { 22 | 3 │ 1 => 1 * y + 1, 23 | · ────────────── 24 | 4 │ else => 1 + y * 1, 25 | · ───────────────── 26 | ╰──── 27 | 28 | 𝙭 duplicate-case: Switch statement has duplicate cases 29 | ╭─[duplicate-case.zig:3:5] 30 | 2 │ const x = switch (1) { 31 | 3 │ 1 => y == 1, 32 | · ─────────── 33 | 4 │ else => 1 == y, 34 | · ────────────── 35 | ╰──── 36 | 37 | 𝙭 duplicate-case: Switch statement has duplicate cases 38 | ╭─[duplicate-case.zig:3:5] 39 | 2 │ const x = switch (1) { 40 | 3 │ 1 => ~y, 41 | · ─────── 42 | 4 │ else => ~y, 43 | · ────────── 44 | ╰──── 45 | 46 | 𝙭 duplicate-case: Switch statement has duplicate cases 47 | ╭─[duplicate-case.zig:5:5] 48 | 4 │ const x = switch (bar) { 49 | 5 │ 1 => f(bar), 50 | · ─────────── 51 | 6 │ 2 => f(bar), 52 | · ─────────── 53 | ╰──── 54 | 55 | 𝙭 duplicate-case: Switch statement has duplicate cases 56 | ╭─[duplicate-case.zig:4:5] 57 | 3 │ const x = switch (bar) { 58 | 4 │ 1 => thing.f(bar), 59 | · ───────────────── 60 | 5 │ 2 => thing.f(bar), 61 | · ───────────────── 62 | ╰──── 63 | 64 | 𝙭 duplicate-case: Switch statement has duplicate cases 65 | ╭─[duplicate-case.zig:3:5] 66 | 2 │ const x = switch (bar) { 67 | 3 │ 1 => if (cond) 1 else 2, 68 | · ─────────────────────── 69 | 4 │ 2 => if (cond) 1 else 2, 70 | · ─────────────────────── 71 | ╰──── 72 | 73 | 𝙭 duplicate-case: Switch statement has duplicate cases 74 | ╭─[duplicate-case.zig:3:5] 75 | 2 │ const x = switch (bar) { 76 | 3 │ 1 => { return 1; }, 77 | · ────────────────── 78 | 4 │ 2 => { return 1; }, 79 | · ────────────────── 80 | ╰──── 81 | 82 | 𝙭 duplicate-case: Switch statement has duplicate cases 83 | ╭─[duplicate-case.zig:3:5] 84 | 2 │ const x = switch (bar) { 85 | 3 │ 1 => { 86 | if (cond) { 87 | const y = x + 2; 88 | return y; 89 | } 90 | return 0; 91 | }, 92 | · ───────────────────────────────────────────────────────────────────────────────────────────────── 93 | 4 │ 2 => { 94 | 9 │ }, 95 | 10 │ 2 => { 96 | if (cond) { 97 | const y = x + 2; 98 | return y; 99 | } 100 | return 0; 101 | }, 102 | · ───────────────────────────────────────────────────────────────────────────────────────────────── 103 | 11 │ else => 0 104 | ╰──── 105 | 106 | --------------------------------------------------------------------------------
{siteConfig.tagline}
{description}