Repository: qltysh/qlty
Pull request #1987: Add stringslint plugin
Author: login: marschattha
Created: 2025-04-29T16:25:16Z | Merged: 2025-04-30T18:22:20Z



Diff:
diff --git a/qlty-config/default.toml b/qlty-config/default.toml
index caebeb4b7..f58764d61 100644
--- a/qlty-config/default.toml
+++ b/qlty-config/default.toml
@@ -312,6 +312,18 @@ globs = ["openapi.yaml", "openapi.*.yaml"]
 [file_types.scala]
 globs = ["*.scala"]
 
+[file_types.objective-c-source]
+globs = ["*.m"]
+
+[file_types.strings]
+globs = ["*.strings", "*.stringsdict"]
+
+[file_types.xib]
+globs = ["*.xib"]
+
+[file_types.storyboard]
+globs = ["*.storyboard"]
+
 [language.kotlin]
 globs = ["*.kt"]
 
diff --git a/qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_strings_v0.1.9.shot b/qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_strings_v0.1.9.shot
new file mode 100644
index 000000000..5c5e83e7c
--- /dev/null
+++ b/qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_strings_v0.1.9.shot
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`linter=stringslint fixture=basic_strings version=0.1.9 1`] = `
+{
+  "issues": [
+    {
+      "category": "CATEGORY_LINT",
+      "level": "LEVEL_MEDIUM",
+      "location": {
+        "path": "basic_strings.in.strings",
+        "range": {
+          "startLine": 1,
+        },
+      },
+      "message": "Comment for Localized string "abc" is missing (missing_comment)",
+      "mode": "MODE_BLOCK",
+      "ruleKey": "MissingComment Violation",
+      "snippet": ""abc" = "A B C";",
+      "snippetWithContext": ""abc" = "A B C";",
+      "tool": "stringslint",
+    },
+    {
+      "category": "CATEGORY_LINT",
+      "level": "LEVEL_MEDIUM",
+      "location": {
+        "path": "basic_strings.in.strings",
+        "range": {
+          "startLine": 1,
+        },
+      },
+      "message": "Localized string "abc" is unused (unused)",
+      "mode": "MODE_BLOCK",
+      "ruleKey": "Unused Violation",
+      "snippet": ""abc" = "A B C";",
+      "snippetWithContext": ""abc" = "A B C";",
+      "tool": "stringslint",
+    },
+  ],
+}
+`;
diff --git a/qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_v0.1.9.shot b/qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_v0.1.9.shot
new file mode 100644
index 000000000..e49828226
--- /dev/null
+++ b/qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_v0.1.9.shot
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`linter=stringslint fixture=basic version=0.1.9 1`] = `
+{
+  "issues": [
+    {
+      "category": "CATEGORY_LINT",
+      "level": "LEVEL_MEDIUM",
+      "location": {
+        "path": "basic.in.swift",
+        "range": {
+          "startLine": 3,
+        },
+      },
+      "message": "Localized string "test" is missing (missing)",
+      "mode": "MODE_BLOCK",
+      "ruleKey": "Missing Violation",
+      "snippet": "        baz.typeText("test")",
+      "snippetWithContext": "class Foo {
+    func Bar() {
+        baz.typeText("test")
+    }
+}
+
+NSLocalizedString("def", comment:"")",
+      "tool": "stringslint",
+    },
+  ],
+}
+`;
diff --git a/qlty-plugins/plugins/linters/stringslint/fixtures/basic.in.swift b/qlty-plugins/plugins/linters/stringslint/fixtures/basic.in.swift
new file mode 100644
index 000000000..acf32dfc8
--- /dev/null
+++ b/qlty-plugins/plugins/linters/stringslint/fixtures/basic.in.swift
@@ -0,0 +1,7 @@
+class Foo {
+    func Bar() {
+        baz.typeText("test")
+    }
+}
+
+NSLocalizedString("def", comment:"")
diff --git a/qlty-plugins/plugins/linters/stringslint/fixtures/basic_strings.in.strings b/qlty-plugins/plugins/linters/stringslint/fixtures/basic_strings.in.strings
new file mode 100644
index 000000000..cfb08314d
--- /dev/null
+++ b/qlty-plugins/plugins/linters/stringslint/fixtures/basic_strings.in.strings
@@ -0,0 +1 @@
+"abc" = "A B C";
diff --git a/qlty-plugins/plugins/linters/stringslint/plugin.toml b/qlty-plugins/plugins/linters/stringslint/plugin.toml
new file mode 100644
index 000000000..fd55321b4
--- /dev/null
+++ b/qlty-plugins/plugins/linters/stringslint/plugin.toml
@@ -0,0 +1,31 @@
+config_version = "0"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "x86_64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "aarch64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[plugins.definitions.stringslint]
+downloads = ["stringslint"]
+config_files = [".stringslint", ".stringslint.yml", ".stringslint.yaml"]
+file_types = ["swift", "objective-c-source", "strings", "xib", "storyboard"]
+latest_version = "0.1.9"
+known_good_version = "0.1.9"
+version_command = "stringslint --version"
+description = "A tool to ensure localized strings are complete and never unused."
+supported_platforms = ["macos"]
+
+[plugins.definitions.stringslint.drivers.lint]
+script = "stringslint lint ${target}"
+success_codes = [0]
+output = "stdout"
+output_format = "regex"
+output_regex = "(?P<path>.*?):(?P<line>\\d+): (?P<severity>.*?): (?P<code>.*?): (?P<message>.*)"
+batch = true
+cache_results = true
+suggested = "config"
diff --git a/qlty-plugins/plugins/linters/stringslint/stringslint.test.ts b/qlty-plugins/plugins/linters/stringslint/stringslint.test.ts
new file mode 100644
index 000000000..50a7ac86e
--- /dev/null
+++ b/qlty-plugins/plugins/linters/stringslint/stringslint.test.ts
@@ -0,0 +1,3 @@
+import { linterCheckTest } from "tests";
+
+linterCheckTest("stringslint", __dirname);
diff --git a/qlty-plugins/plugins/tests/runLinterTest.ts b/qlty-plugins/plugins/tests/runLinterTest.ts
index d244b2085..ed6c2d95e 100644
--- a/qlty-plugins/plugins/tests/runLinterTest.ts
+++ b/qlty-plugins/plugins/tests/runLinterTest.ts
@@ -12,8 +12,8 @@ Debug.inspectOpts!.hideDate = true;
 
 // Currently unsupported tools on Windows
 const SKIP_LINTERS = {
-  win32: ["semgrep", "swiftlint", "dockerfmt"],
-  linux: ["swiftlint"],
+  win32: ["semgrep", "swiftlint", "dockerfmt", "stringslint"],
+  linux: ["stringslint", "swiftlint"],
 } as { [key in NodeJS.Platform]: string[] };
 
 const FIXTURES_DIR = "fixtures";


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request adds support for a new stringslint plugin.  
- Introduces a test file to run the plugin check.  
- Provides a plugin configuration file with download and lint definitions.  
- Adds a Swift fixture file to exercise the plugin and updates file type globs in the config.

### Reviewed Changes

Copilot reviewed 4 out of 7 changed files in this pull request and generated no comments.

| File | Description |
| ---- | ----------- |
| qlty-plugins/plugins/linters/stringslint/stringslint.test.ts | Adds a test invocation for the stringslint plugin. |
| qlty-plugins/plugins/linters/stringslint/plugin.toml | Configures plugin downloads and lint driver definitions. |
| qlty-plugins/plugins/linters/stringslint/fixtures/basic.in.swift | Provides a basic Swift fixture to test the plugin’s string detection. |
| qlty-config/default.toml | Updates file type globs to include relevant extensions for the plugin. |

<details>
<summary>Files not reviewed (3)</summary>

* **qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_strings_v0.1.9.shot**: Language not supported
* **qlty-plugins/plugins/linters/stringslint/fixtures/__snapshots__/basic_v0.1.9.shot**: Language not supported
* **qlty-plugins/plugins/linters/stringslint/fixtures/basic_strings.in.strings**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**qlty-plugins/plugins/linters/stringslint/fixtures/basic.in.swift:2**
* [nitpick] Consider renaming the method 'Bar' to 'bar' to align with Swift's lowerCamelCase convention for function names.
```
    func Bar() {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T16:25:31Z
user: login: brynary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T23:16:00Z
user: login: marschattha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T23:32:35Z
user: login: brynary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T13:38:43Z
user: login: marschattha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T16:33:10Z
user: login: brynary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T18:21:11Z

Review comments:
diff_hunk: @@ -12,7 +12,8 @@ Debug.inspectOpts!.hideDate = true;
 
 // Currently unsupported tools on Windows
 const SKIP_LINTERS = {
-  win32: ["semgrep", "swiftlint", "dockerfmt"],
+  win32: ["semgrep", "swiftlint", "dockerfmt", "stringslint"],
+  linux: ["stringslint", "swiftlint"],
path: qlty-plugins/plugins/tests/runLinterTest.ts
user: login: brynary
body: Not for this PR, but I wonder if we can infer SKIP_LINTERS behavior from the `supported_platforms` data someday
created_at: 2025-04-29T23:13:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+config_version = "0"
+
+[plugins.downloads.stringslint]
+github = "dral3x/StringsLint"
+download_type = "zip"
+strip_components = 0
+
+[[plugins.downloads.stringslint.system]]
+cpu = "x86_64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "aarch64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
path: qlty-plugins/plugins/linters/stringslint/plugin.toml
user: login: brynary
body: @marschattha I might be mis-remembering but I thought that the `github = ...` syntax was an _alternative_ to specifying per-cpu/os download URLs. Don't we typically use one or the other?

For example, we have `dral3x/StringsLint` in both the `github =` line and in the URLs... It seems like this creates a risk of them being out of sync.
created_at: 2025-04-29T23:15:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+config_version = "0"
+
+[plugins.downloads.stringslint]
+github = "dral3x/StringsLint"
+download_type = "zip"
+strip_components = 0
+
+[[plugins.downloads.stringslint.system]]
+cpu = "x86_64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "aarch64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
path: qlty-plugins/plugins/linters/stringslint/plugin.toml
user: login: marschattha
body: Since the Github release syntax tries to match platform and architecture in the assets list, generic name like `portable_stringslint.zip` doesn't get matched and it fails.
created_at: 2025-04-29T23:32:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+config_version = "0"
+
+[plugins.downloads.stringslint]
+github = "dral3x/StringsLint"
+download_type = "zip"
+strip_components = 0
+
+[[plugins.downloads.stringslint.system]]
+cpu = "x86_64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "aarch64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
path: qlty-plugins/plugins/linters/stringslint/plugin.toml
user: login: brynary
body: @marschattha right, that makes sense, but what I'm saying is that do we need the `[plugins.downloads.stringslint]` block at all? It seems redundant.

I think we have two, mutually exclusive ways to specify downloads:

1. Automatic detection of download URLs GitHub releases; OR
2. Download URLs (which may be on GitHub, but the system just understands them as a URL)

So I think the issue with the generic name `portable_stringslint.zip` means we would avoid the GitHub release approach entirely.
created_at: 2025-04-30T13:38:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+config_version = "0"
+
+[plugins.downloads.stringslint]
+github = "dral3x/StringsLint"
+download_type = "zip"
+strip_components = 0
+
+[[plugins.downloads.stringslint.system]]
+cpu = "x86_64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "aarch64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
path: qlty-plugins/plugins/linters/stringslint/plugin.toml
user: login: marschattha
body: Actually the downloads/DownloadDef doesn't even have github, that is the `release`, removed the block
created_at: 2025-04-30T16:33:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+config_version = "0"
+
+[plugins.downloads.stringslint]
+github = "dral3x/StringsLint"
+download_type = "zip"
+strip_components = 0
+
+[[plugins.downloads.stringslint.system]]
+cpu = "x86_64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
+
+[[plugins.downloads.stringslint.system]]
+cpu = "aarch64"
+os = "macos"
+url = "https://github.com/dral3x/StringsLint/releases/download/${version}/portable_stringslint.zip"
path: qlty-plugins/plugins/linters/stringslint/plugin.toml
user: login: brynary
body: Ah ok. Maybe we need warnings for invalid config vars, or a "strict" mode we can use in development. I've been caught by that also
created_at: 2025-04-30T18:21:11Z
author_association: MEMBER

PR comments:
user: login: qltysh[bot]
created_at: 2025-04-29T16:27:29Z
body: <p><strong>Diff Coverage for macos-15:</strong> Not applicable. There was no coverage data reported for the files in this diff.</p>
<p><strong>Total Coverage for macos-15:</strong> This PR will not change total coverage.</p>

  <details>
  <summary>File Coverage Changes</summary>

  | Path  | File Coverage Δ | Indirect |
  | ------------- | ------------- | ------------- |
 | qlty-check/src/executor/driver.rs | 0.1 |  √ |
 | qlty-check/src/tool/github.rs | 10.3 |  √ |
 | qlty-cli/src/auth/auth_flow.rs | 0.5 |  √ |
 | qlty-cli/src/commands/coverage.rs | 20.0 |  √ |
 | qlty-config/src/library.rs | -0.5 |  √ |
 | qlty-config/src/toml_merge.rs | -14.1 |  √ |
</details>

<details>
  <summary>🛟 Help</summary>
  <ul>
    <li>
      <p>
        <strong>Diff Coverage:</strong> Coverage for added or modified lines of code (excludes deleted files). <a href="https://qlty.sh/d/diff-coverage">
          Learn more.
          </a>
      </p>
    </li>
    <li>
      <p>
        <strong>Total Coverage:</strong> Coverage for the whole repository, calculated as the sum of all File Coverage.
        <a href="https://qlty.sh/d/total-coverage">
           Learn more.
        </a>
      </p>
    </li>
    <li>
      <p>
        <strong>File Coverage:</strong> Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)
      </p>
      <ul>
        <li>
        <p>
          <strong>Indirect Changes:</strong> Changes to File Coverage for files that were not modified in this PR. <a href="https://qlty.sh/d/indirect-coverage-changes">
            Learn more.
            </a>
        </p>
      </li>
      </ul>
    </li>
  </ul>
</details>
  

<a href="#coverage-summary-comment-macos-15"></a>
author_association: CONTRIBUTOR
user: login: qltysh[bot]
created_at: 2025-04-29T16:28:00Z
body: <p><strong>Diff Coverage for ubuntu-latest:</strong> Not applicable. There was no coverage data reported for the files in this diff.</p>
<p><strong>Total Coverage for ubuntu-latest:</strong> This PR will not change total coverage.</p>

  <details>
  <summary>File Coverage Changes</summary>

  | Path  | File Coverage Δ | Indirect |
  | ------------- | ------------- | ------------- |
 | qlty-config/src/library.rs | -0.5 |  √ |
</details>

<details>
  <summary>🛟 Help</summary>
  <ul>
    <li>
      <p>
        <strong>Diff Coverage:</strong> Coverage for added or modified lines of code (excludes deleted files). <a href="https://qlty.sh/d/diff-coverage">
          Learn more.
          </a>
      </p>
    </li>
    <li>
      <p>
        <strong>Total Coverage:</strong> Coverage for the whole repository, calculated as the sum of all File Coverage.
        <a href="https://qlty.sh/d/total-coverage">
           Learn more.
        </a>
      </p>
    </li>
    <li>
      <p>
        <strong>File Coverage:</strong> Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)
      </p>
      <ul>
        <li>
        <p>
          <strong>Indirect Changes:</strong> Changes to File Coverage for files that were not modified in this PR. <a href="https://qlty.sh/d/indirect-coverage-changes">
            Learn more.
            </a>
        </p>
      </li>
      </ul>
    </li>
  </ul>
</details>
  

<a href="#coverage-summary-comment-ubuntu-latest"></a>
author_association: CONTRIBUTOR