From 0a58216c4a51e3ca063a14b9e7d8f034b87c5c19 Mon Sep 17 00:00:00 2001 From: Tyler Jang Date: Wed, 14 May 2025 01:18:25 -0700 Subject: [PATCH 1/2] implement ty linter --- README.md | 107 +++++------ linters/ty/plugin.yaml | 44 +++++ linters/ty/test_data/basic.in.py | 56 ++++++ linters/ty/test_data/interface.in.pyi | 51 +++++ .../ty_v0.0.1-alpha.1_basic.check.shot | 174 ++++++++++++++++++ .../ty_v0.0.1-alpha.1_interface.check.shot | 64 +++++++ linters/ty/ty.test.ts | 3 + 7 files changed, 446 insertions(+), 53 deletions(-) create mode 100644 linters/ty/plugin.yaml create mode 100644 linters/ty/test_data/basic.in.py create mode 100644 linters/ty/test_data/interface.in.pyi create mode 100644 linters/ty/test_data/ty_v0.0.1-alpha.1_basic.check.shot create mode 100644 linters/ty/test_data/ty_v0.0.1-alpha.1_interface.check.shot create mode 100644 linters/ty/ty.test.ts diff --git a/README.md b/README.md index d0545e841..6b2c978c3 100644 --- a/README.md +++ b/README.md @@ -38,59 +38,59 @@ Enable the following tools via: trunk check enable {linter} ``` -| Technology | Linters | -| --------------- | ------------------------------------------------------------------------------------------------------------------------ | -| All | [codespell], [cspell], [gitleaks], [git-diff-check], [pre-commit-hooks], [trunk-toolbox], [vale] | -| Ansible | [ansible-lint] | -| Apex | [pmd] | -| Bash | [shellcheck], [shfmt] | -| Bazel, Starlark | [buildifier] | -| C, C++ | [clang-format], [clang-tidy], [include-what-you-use], [pragma-once] | -| C# | [dotnet-format] | -| CircleCI Config | [circleci] | -| Cloudformation | [cfnlint], [checkov] | -| CMake | [cmake-format] | -| CSS, SCSS | [stylelint], [prettier] | -| Cue | [cue-fmt] | -| Dart | [dart] | -| Docker | [hadolint], [checkov] | -| Dotenv | [dotenv-linter] | -| GitHub | [actionlint] | -| Go | [gofmt], [gofumpt], [goimports], [gokart], [golangci-lint], [golines], [semgrep] | -| GraphQL | [graphql-schema-linter], [prettier] | -| HAML | [haml-lint] | -| HTML Templates | [djlint] | -| Java | [google-java-format], [pmd], [semgrep] | -| Javascript | [biome], [deno], [eslint], [prettier], [rome], [semgrep] | -| JSON | [biome], [deno], [eslint], [prettier], [semgrep] | -| Kotlin | [detekt], [ktlint] | -| Kubernetes | [kube-linter] | -| Lua | [stylua] | -| Markdown | [deno], [markdownlint], [markdownlint-cli2], [markdown-link-check], [markdown-table-prettify], [prettier], [remark-lint] | -| Nix | [nixpkgs-fmt] | -| package.json | [sort-package-json] | -| Perl | [perlcritic], [perltidy] | -| PHP | [php-cs-fixer], [phpstan] | -| PNG | [oxipng] | -| PowerShell | [psscriptanalyzer] | -| Prisma | [prisma] | -| Protobuf | [buf] (breaking, lint, and format), [clang-format], [clang-tidy] | -| Python | [autopep8], [bandit], [black], [flake8], [isort], [mypy], [pylint], [pyright], [semgrep], [yapf], [ruff], [sourcery] | -| Rego | [regal], [opa] | -| Renovate | [renovate] | -| Ruby | [brakeman], [rubocop], [rufo], [semgrep], [standardrb] | -| Rust | [clippy], [rustfmt] | -| Scala | [scalafmt] | -| Security | [checkov], [dustilock], [nancy], [osv-scanner], [snyk], [tfsec], [trivy], [trufflehog], [terrascan] | -| SQL | [sqlfluff], [sqlfmt], [sql-formatter], [squawk] | -| SVG | [svgo] | -| Swift | [stringslint], [swiftlint], [swiftformat] | -| Terraform | [terraform] (validate and fmt), [checkov], [tflint], [tfsec], [terrascan], [tofu] | -| Terragrunt | [terragrunt] | -| Textproto | [txtpbfmt] | -| TOML | [taplo] | -| Typescript | [deno], [eslint], [prettier], [rome], [semgrep] | -| YAML | [prettier], [semgrep], [yamllint] | +| Technology | Linters | +| --------------- | -------------------------------------------------------------------------------------------------------------------------- | +| All | [codespell], [cspell], [gitleaks], [git-diff-check], [pre-commit-hooks], [trunk-toolbox], [vale] | +| Ansible | [ansible-lint] | +| Apex | [pmd] | +| Bash | [shellcheck], [shfmt] | +| Bazel, Starlark | [buildifier] | +| C, C++ | [clang-format], [clang-tidy], [include-what-you-use], [pragma-once] | +| C# | [dotnet-format] | +| CircleCI Config | [circleci] | +| Cloudformation | [cfnlint], [checkov] | +| CMake | [cmake-format] | +| CSS, SCSS | [stylelint], [prettier] | +| Cue | [cue-fmt] | +| Dart | [dart] | +| Docker | [hadolint], [checkov] | +| Dotenv | [dotenv-linter] | +| GitHub | [actionlint] | +| Go | [gofmt], [gofumpt], [goimports], [gokart], [golangci-lint], [golines], [semgrep] | +| GraphQL | [graphql-schema-linter], [prettier] | +| HAML | [haml-lint] | +| HTML Templates | [djlint] | +| Java | [google-java-format], [pmd], [semgrep] | +| Javascript | [biome], [deno], [eslint], [prettier], [rome], [semgrep] | +| JSON | [biome], [deno], [eslint], [prettier], [semgrep] | +| Kotlin | [detekt], [ktlint] | +| Kubernetes | [kube-linter] | +| Lua | [stylua] | +| Markdown | [deno], [markdownlint], [markdownlint-cli2], [markdown-link-check], [markdown-table-prettify], [prettier], [remark-lint] | +| Nix | [nixpkgs-fmt] | +| package.json | [sort-package-json] | +| Perl | [perlcritic], [perltidy] | +| PHP | [php-cs-fixer], [phpstan] | +| PNG | [oxipng] | +| PowerShell | [psscriptanalyzer] | +| Prisma | [prisma] | +| Protobuf | [buf] (breaking, lint, and format), [clang-format], [clang-tidy] | +| Python | [autopep8], [bandit], [black], [flake8], [isort], [mypy], [pylint], [pyright], [semgrep], [yapf], [ruff], [sourcery], [ty] | +| Rego | [regal], [opa] | +| Renovate | [renovate] | +| Ruby | [brakeman], [rubocop], [rufo], [semgrep], [standardrb] | +| Rust | [clippy], [rustfmt] | +| Scala | [scalafmt] | +| Security | [checkov], [dustilock], [nancy], [osv-scanner], [snyk], [tfsec], [trivy], [trufflehog], [terrascan] | +| SQL | [sqlfluff], [sqlfmt], [sql-formatter], [squawk] | +| SVG | [svgo] | +| Swift | [stringslint], [swiftlint], [swiftformat] | +| Terraform | [terraform] (validate and fmt), [checkov], [tflint], [tfsec], [terrascan], [tofu] | +| Terragrunt | [terragrunt] | +| Textproto | [txtpbfmt] | +| TOML | [taplo] | +| Typescript | [deno], [eslint], [prettier], [rome], [semgrep] | +| YAML | [prettier], [semgrep], [yamllint] | [actionlint]: https://trunk.io/linters/infra/actionlint [ansible-lint]: https://github.com/ansible/ansible-lint#readme @@ -195,6 +195,7 @@ trunk check enable {linter} [trufflehog]: https://trunk.io/linters/security/trufflehog [trunk-toolbox]: https://github.com/trunk-io/toolbox#readme [txtpbfmt]: https://github.com/protocolbuffers/txtpbfmt#readme +[ty]: https://github.com/astral-sh/ty#readme [vale]: https://vale.sh/docs/ [yamllint]: https://trunk.io/linters/yaml/yamllint [yapf]: https://github.com/google/yapf#readme diff --git a/linters/ty/plugin.yaml b/linters/ty/plugin.yaml new file mode 100644 index 000000000..e382cd725 --- /dev/null +++ b/linters/ty/plugin.yaml @@ -0,0 +1,44 @@ +version: 0.1 +downloads: + - name: ty + version: 0.0.1-alpha.1 + downloads: + - os: + linux: unknown-linux-gnu + macos: apple-darwin + cpu: + x86_64: x86_64 + arm_64: aarch64 + url: https://github.com/astral-sh/ty/releases/download/${version}/ty-${cpu}-${os}.tar.gz + strip_components: 1 + - os: + windows: windows + cpu: + x86_64: x86_64 + url: https://github.com/astral-sh/ty/releases/download/${version}/ty-x86_64-pc-windows-msvc.zip + strip_components: 1 +tools: + definitions: + - name: ty + download: ty + known_good_version: 0.0.1-alpha.1 + shims: [ty] + health_checks: + - command: ty --version + parse_regex: ${semver} +lint: + definitions: + - name: ty + files: [python, python-interface] + tools: [ty] + known_good_version: 0.0.1-alpha.1 + suggest_if: never + commands: + - name: check + output: regex + parse_regex: + ((?P.+)\[(?P.+)\] + (?P.+):(?P\d+):(?P\d+):\s*(?P.*)) + run: ty check --output-format=concise ${target} + success_codes: [0, 1] + batch: true diff --git a/linters/ty/test_data/basic.in.py b/linters/ty/test_data/basic.in.py new file mode 100644 index 000000000..65d12645d --- /dev/null +++ b/linters/ty/test_data/basic.in.py @@ -0,0 +1,56 @@ +from typing import Callable, Iterator, Union, Optional, Enum + + +def wrong_type(x: int) -> str: + return x # error: Incompatible return value type (got "int", expected "str") + +class A: + def method1(self) -> None: + self.x = 1 + + def method2(self) -> None: + self.x = "" + +a = A() +reveal_type(a.x) + +a.x = "" +a.x = 3.0 + + + +class A: + x: int = 0 # Regular class variable + y: ClassVar[int] = 0 # Pure class variable + + def __init__(self): + self.z = 0 # Pure instance variable + +print(A.x) +print(A.y) +print(A.z) + + + +class Color(Enum): + RED = 1 + BLUE = 2 + +def is_red(color: Color) -> bool: + if color == Color.RED: + return True + elif color == Color.BLUE: + return False + + +def func(val: int | None): + if val is not None: + + def inner_1() -> None: + reveal_type(val) + print(val + 1) + + inner_2 = lambda: reveal_type(val) + 1 + + inner_1() + inner_2() diff --git a/linters/ty/test_data/interface.in.pyi b/linters/ty/test_data/interface.in.pyi new file mode 100644 index 000000000..ac27d5d56 --- /dev/null +++ b/linters/ty/test_data/interface.in.pyi @@ -0,0 +1,51 @@ +# Based on test file input from astral-sh/ruff +import json + +from typing import Any, Sequence + +class MissingCommand(TypeError): ... +class AnoherClass: ... + +def a(): ... + +@overload +def a(arg: int): ... + +@overload +def a(arg: int, name: str): ... + + +def grouped1(): ... +def grouped2(): ... +def grouped3( ): ... + + +class BackendProxy: + backend_module: str + backend_object: str | None + backend: Any + + def grouped1(): ... + def grouped2(): ... + def grouped3( ): ... + @decorated + + def with_blank_line(): ... + + + def ungrouped(): ... +a = "test" + +def function_def(): + pass +b = "test" + + +def outer(): + def inner(): + pass + def inner2(): + pass + +class Foo: ... +class Bar: ... diff --git a/linters/ty/test_data/ty_v0.0.1-alpha.1_basic.check.shot b/linters/ty/test_data/ty_v0.0.1-alpha.1_basic.check.shot new file mode 100644 index 000000000..3cab03982 --- /dev/null +++ b/linters/ty/test_data/ty_v0.0.1-alpha.1_basic.check.shot @@ -0,0 +1,174 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing linter ty test basic 1`] = ` +{ + "issues": [ + { + "code": "unresolved-import", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "1", + "linter": "ty", + "message": "Module \`typing\` has no member \`Enum\`", + "targetType": "python", + }, + { + "code": "revealed-type", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_LOW", + "line": "15", + "linter": "ty", + "message": "Revealed type: \`Unknown | Literal[1, ""]\`", + "targetType": "python", + }, + { + "code": "undefined-reveal", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_MEDIUM", + "line": "15", + "linter": "ty", + "message": "\`reveal_type\` used without importing it", + "targetType": "python", + }, + { + "code": "unresolved-reference", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "24", + "linter": "ty", + "message": "Name \`ClassVar\` used when not defined", + "targetType": "python", + }, + { + "code": "unresolved-attribute", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "31", + "linter": "ty", + "message": "Attribute \`z\` can only be accessed on instances, not on the class object \`\` itself.", + "targetType": "python", + }, + { + "code": "invalid-return-type", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "39", + "linter": "ty", + "message": "Function can implicitly return \`None\`, which is not assignable to return type \`bool\`", + "targetType": "python", + }, + { + "code": "invalid-return-type", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "5", + "linter": "ty", + "message": "Return type does not match returned value: Expected \`str\`, found \`int\`", + "targetType": "python", + }, + { + "code": "revealed-type", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_LOW", + "line": "50", + "linter": "ty", + "message": "Revealed type: \`int | None\`", + "targetType": "python", + }, + { + "code": "undefined-reveal", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_MEDIUM", + "line": "50", + "linter": "ty", + "message": "\`reveal_type\` used without importing it", + "targetType": "python", + }, + { + "code": "unsupported-operator", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "51", + "linter": "ty", + "message": "Operator \`+\` is unsupported between objects of type \`int | None\` and \`Literal[1]\`", + "targetType": "python", + }, + { + "code": "revealed-type", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_LOW", + "line": "53", + "linter": "ty", + "message": "Revealed type: \`int | None\`", + "targetType": "python", + }, + { + "code": "undefined-reveal", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_MEDIUM", + "line": "53", + "linter": "ty", + "message": "\`reveal_type\` used without importing it", + "targetType": "python", + }, + { + "code": "unsupported-operator", + "column": "1", + "file": "test_data/basic.in.py", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "53", + "linter": "ty", + "message": "Operator \`+\` is unsupported between objects of type \`int | None\` and \`Literal[1]\`", + "targetType": "python", + }, + ], + "lintActions": [ + { + "command": "check", + "fileGroupName": "python", + "linter": "ty", + "paths": [ + "test_data/basic.in.py", + ], + "verb": "TRUNK_VERB_CHECK", + }, + { + "command": "check", + "fileGroupName": "python", + "linter": "ty", + "paths": [ + "test_data/basic.in.py", + ], + "upstream": true, + "verb": "TRUNK_VERB_CHECK", + }, + ], + "taskFailures": [], + "unformattedFiles": [], +} +`; diff --git a/linters/ty/test_data/ty_v0.0.1-alpha.1_interface.check.shot b/linters/ty/test_data/ty_v0.0.1-alpha.1_interface.check.shot new file mode 100644 index 000000000..8a10c0ddf --- /dev/null +++ b/linters/ty/test_data/ty_v0.0.1-alpha.1_interface.check.shot @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing linter ty test interface 1`] = ` +{ + "issues": [ + { + "code": "unresolved-reference", + "column": "1", + "file": "test_data/interface.in.pyi", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "11", + "linter": "ty", + "message": "Name \`overload\` used when not defined", + "targetType": "python-interface", + }, + { + "code": "unresolved-reference", + "column": "1", + "file": "test_data/interface.in.pyi", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "14", + "linter": "ty", + "message": "Name \`overload\` used when not defined", + "targetType": "python-interface", + }, + { + "code": "unresolved-reference", + "column": "1", + "file": "test_data/interface.in.pyi", + "issueClass": "ISSUE_CLASS_EXISTING", + "level": "LEVEL_HIGH", + "line": "31", + "linter": "ty", + "message": "Name \`decorated\` used when not defined", + "targetType": "python-interface", + }, + ], + "lintActions": [ + { + "command": "check", + "fileGroupName": "python-interface", + "linter": "ty", + "paths": [ + "test_data/interface.in.pyi", + ], + "verb": "TRUNK_VERB_CHECK", + }, + { + "command": "check", + "fileGroupName": "python-interface", + "linter": "ty", + "paths": [ + "test_data/interface.in.pyi", + ], + "upstream": true, + "verb": "TRUNK_VERB_CHECK", + }, + ], + "taskFailures": [], + "unformattedFiles": [], +} +`; diff --git a/linters/ty/ty.test.ts b/linters/ty/ty.test.ts new file mode 100644 index 000000000..2c45bb5e7 --- /dev/null +++ b/linters/ty/ty.test.ts @@ -0,0 +1,3 @@ +import { linterCheckTest } from "tests"; + +linterCheckTest({ linterName: "ty" }); From ce1f8611b71b962f2a97f87ed3cf72f417307914 Mon Sep 17 00:00:00 2001 From: Tyler Jang Date: Wed, 14 May 2025 01:25:11 -0700 Subject: [PATCH 2/2] description --- linters/ty/plugin.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/linters/ty/plugin.yaml b/linters/ty/plugin.yaml index e382cd725..264421a14 100644 --- a/linters/ty/plugin.yaml +++ b/linters/ty/plugin.yaml @@ -29,6 +29,7 @@ tools: lint: definitions: - name: ty + description: A Python type checker files: [python, python-interface] tools: [ty] known_good_version: 0.0.1-alpha.1