Skip to content

Commit 2075286

Browse files
committed
Address SonarCloud + Codacy findings on PR #196
Locator/runner/LSP/stub housekeeping - ab_locator/runner.py:71 use dict() instead of comprehension (S7500) - dag/runner.py:221 drop unnecessary list() (S7504) - autocontrol-lsp documents.py:65 extract _resolve_next_version (S3358) - autocontrol-lsp server.py:169 split run() into _process_one_message to cut cognitive complexity from 17 to ≤15 (S3776) - stubs/generator.py:182 add error-return path so _cli() no longer always returns 0 (S3516); hoist sys import; type stub falls back to Any for non-builtin classes and dotted module paths Tests: use pytest.approx for every == on floats (S1244, 8 sites); move /tmp literals to tmp_path / project-relative paths (S5443); NOSONAR-tag two test names that mirror AC_drag / AgentBackendError Wayland: NOSONAR-tag the resolution regex (S5852 — anchored \d+, no nested quantifiers, not vulnerable to ReDoS) Language wrappers (en / zh-TW / zh-CN / ja): extract _BROWSE, _CLEAR_LOG, _OUTPUT_LABEL, _MODEL_LABEL, _LOCATE_CLICK constants so duplicated UI button labels stop tripping S1192 Slack example: - render_report path-traversal hardening: basename + resolve check so a malicious ``today`` can't escape the output dir (S2083) - email_report pins TLS minimum to 1.2 (S4423) VS Code extension (extension.ts): - import * as http from "node:http" / "node:https" / "node:url" (S7772) - consolidate context.subscriptions.push calls into one (S7778) - mark ScriptStepProvider.emitter readonly (S2933) - use optional chaining on editor?.document.languageId (S6582) Browser extension: - background.js loadState() no longer spreads an empty literal (S7744) - content_script.js uses optional chaining (S6582), globalThis instead of window (S7764), String.raw for the regex escape pattern (S7780) - popup.js optional chain on tab?.url (S6582); void refresh() at bottom matches S7785 expectations Hotspots - failure_hooks/backends.py:124 NOSONAR comment on the http:// scheme allow-list (S5332 — guard rejects, never emits) - test_failure_hooks.py:196 NOSONAR on the ftp:// negative-test literal (S5332) - Dockerfile.xfce:50 NOSONAR comment on the documented VNC port exposure (S6473) - docker.yml NOSONAR comments on action major-version pins (S7637 — matches project convention across dev/stable/quality workflows)
1 parent 7ccf25d commit 2075286

27 files changed

Lines changed: 169 additions & 113 deletions

.github/workflows/docker.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ jobs:
2828
- uses: actions/checkout@v4
2929

3030
- name: Set up Docker Buildx
31-
uses: docker/setup-buildx-action@v3
31+
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637 — project convention pins to vendored major version tags (matches dev.yml / stable.yml / quality.yml)
3232

3333
- name: Build image (no push)
34-
uses: docker/build-push-action@v5
34+
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637 — project convention pins to vendored major version tags
3535
with:
3636
context: .
3737
file: docker/Dockerfile
@@ -52,10 +52,10 @@ jobs:
5252
- uses: actions/checkout@v4
5353

5454
- name: Set up Docker Buildx
55-
uses: docker/setup-buildx-action@v3
55+
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637 — project convention pins to vendored major version tags (matches dev.yml / stable.yml / quality.yml)
5656

5757
- name: Rebuild image (cached)
58-
uses: docker/build-push-action@v5
58+
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637 — project convention pins to vendored major version tags
5959
with:
6060
context: .
6161
file: docker/Dockerfile

autocontrol-lsp/autocontrol_lsp/server/documents.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,7 @@ def replace(self, uri: str, text: str,
6060
version: Optional[int] = None) -> TextDocument:
6161
with self._lock:
6262
existing = self._docs.get(uri)
63-
next_version = (
64-
int(version) if version is not None
65-
else (existing.version + 1 if existing else 0)
66-
)
63+
next_version = _resolve_next_version(version, existing)
6764
return self.open(uri, text, next_version)
6865

6966
def close(self, uri: str) -> bool:
@@ -103,6 +100,16 @@ def apply_change(self, uri: str,
103100

104101
# --- helpers -------------------------------------------------
105102

103+
def _resolve_next_version(override: Optional[int],
104+
existing: Optional[TextDocument]) -> int:
105+
"""Pick the next document version: explicit override → existing+1 → 0."""
106+
if override is not None:
107+
return int(override)
108+
if existing is not None:
109+
return existing.version + 1
110+
return 0
111+
112+
106113
_WORD_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
107114

108115

autocontrol-lsp/autocontrol_lsp/server/server.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -172,26 +172,30 @@ def run(input_stream=None, output_stream=None) -> int:
172172
out = output_stream or sys.stdout.buffer
173173
server = LspServer()
174174
try:
175-
while True:
176-
request = _read_message(inp)
177-
if request is None or request.get("method") == "exit":
178-
return 0
179-
method = request.get("method")
180-
params = request.get("params") or {}
181-
if not isinstance(method, str):
182-
continue
183-
request_id = request.get("id")
184-
if request_id is None:
185-
server.handle_notification(method, params)
186-
for payload in server.drain_diagnostics():
187-
_publish_diagnostics(out, payload)
188-
continue
189-
result = server.dispatch(method, params)
190-
_write_message(out, _build_reply(method, request_id, result))
191-
for payload in server.drain_diagnostics():
192-
_publish_diagnostics(out, payload)
175+
while _process_one_message(inp, out, server):
176+
pass
193177
except (OSError, ValueError):
194178
return 1
179+
return 0
180+
181+
182+
def _process_one_message(inp, out, server: LspServer) -> bool:
183+
"""Read one LSP message and dispatch it. Returns False to end the loop."""
184+
request = _read_message(inp)
185+
if request is None or request.get("method") == "exit":
186+
return False
187+
method = request.get("method")
188+
params = request.get("params") or {}
189+
if not isinstance(method, str):
190+
return True
191+
if request.get("id") is None:
192+
server.handle_notification(method, params)
193+
else:
194+
result = server.dispatch(method, params)
195+
_write_message(out, _build_reply(method, request["id"], result))
196+
for payload in server.drain_diagnostics():
197+
_publish_diagnostics(out, payload)
198+
return True
195199

196200

197201
if __name__ == "__main__": # pragma: no cover - entry point

autocontrol-lsp/vscode/src/extension.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// and exposes a tree view of the current script's steps.
44

55
import * as vscode from "vscode";
6-
import * as http from "http";
7-
import * as https from "https";
8-
import { URL } from "url";
6+
import * as http from "node:http";
7+
import * as https from "node:https";
8+
import { URL } from "node:url";
99
import {
1010
LanguageClient,
1111
LanguageClientOptions,
@@ -18,20 +18,17 @@ let stepProvider: ScriptStepProvider | undefined;
1818

1919
export function activate(context: vscode.ExtensionContext): void {
2020
client = startLanguageClient();
21-
context.subscriptions.push({ dispose: () => client?.stop() });
22-
2321
stepProvider = new ScriptStepProvider();
22+
// Consolidate every subscription into a single push() call (S7778):
23+
// VS Code accepts a varargs subscriber list, so building the array
24+
// up front keeps the call site flat and avoids repeated diffs.
2425
context.subscriptions.push(
26+
{ dispose: () => client?.stop() },
2527
vscode.window.registerTreeDataProvider(
2628
"autocontrolScriptSteps", stepProvider,
2729
),
28-
);
29-
context.subscriptions.push(
3030
vscode.window.onDidChangeActiveTextEditor(() => stepProvider?.refresh()),
3131
vscode.workspace.onDidChangeTextDocument(() => stepProvider?.refresh()),
32-
);
33-
34-
context.subscriptions.push(
3532
vscode.commands.registerCommand(
3633
"autocontrol.runScript", runCurrentScript,
3734
),
@@ -193,7 +190,7 @@ async function takeScreenshot(): Promise<void> {
193190
// --- Tree view ----------------------------------------------------
194191

195192
class ScriptStepProvider implements vscode.TreeDataProvider<StepItem> {
196-
private emitter = new vscode.EventEmitter<StepItem | undefined>();
193+
private readonly emitter = new vscode.EventEmitter<StepItem | undefined>();
197194
readonly onDidChangeTreeData = this.emitter.event;
198195

199196
refresh(): void { this.emitter.fire(undefined); }
@@ -202,7 +199,7 @@ class ScriptStepProvider implements vscode.TreeDataProvider<StepItem> {
202199

203200
getChildren(): vscode.ProviderResult<StepItem[]> {
204201
const editor = vscode.window.activeTextEditor;
205-
if (!editor || editor.document.languageId !== "json") {
202+
if (editor?.document.languageId !== "json") {
206203
return [];
207204
}
208205
let parsed: unknown;

browser-extension/background.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const DEFAULT_STATE = {
2020

2121
async function loadState() {
2222
const stored = await chrome.storage.local.get(STATE_KEY);
23-
return { ...DEFAULT_STATE, ...(stored[STATE_KEY] || {}) };
23+
const saved = stored[STATE_KEY];
24+
return saved ? { ...DEFAULT_STATE, ...saved } : { ...DEFAULT_STATE };
2425
}
2526

2627
async function saveState(state) {

browser-extension/content_script.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* data-name > nth-of-type fallback.
1313
*/
1414
function selectorFor(element) {
15-
if (!element || element.nodeType !== 1) { return ""; }
15+
if (element?.nodeType !== 1) { return ""; }
1616
if (element.id) {
1717
return "#" + cssEscape(element.id);
1818
}
@@ -29,7 +29,7 @@
2929
function nthOfTypeSelector(element) {
3030
const path = [];
3131
let node = element;
32-
while (node && node.nodeType === 1 && node !== document.documentElement) {
32+
while (node?.nodeType === 1 && node !== document.documentElement) {
3333
const tag = node.tagName.toLowerCase();
3434
const parent = node.parentElement;
3535
if (!parent) {
@@ -48,11 +48,15 @@
4848
}
4949

5050
function cssEscape(value) {
51-
if (window.CSS && typeof window.CSS.escape === "function") {
52-
return window.CSS.escape(value);
51+
if (typeof globalThis.CSS?.escape === "function") {
52+
return globalThis.CSS.escape(value);
5353
}
54-
// Bare-bones fallback for browsers without CSS.escape.
55-
return String(value).replace(/(["\\\]])/g, "\\$1");
54+
// Bare-bones fallback for browsers without CSS.escape. Use
55+
// ``String.raw`` so the regex escape literal reads as written.
56+
return String(value).replace(
57+
new RegExp(String.raw`(["\\\]])`, "g"),
58+
String.raw`\$1`,
59+
);
5660
}
5761

5862
function send(event) {
@@ -94,7 +98,7 @@
9498
});
9599
}, true);
96100

97-
window.addEventListener("popstate", () => {
101+
globalThis.addEventListener("popstate", () => {
98102
send({ type: "navigate", url: location.href });
99103
});
100104

browser-extension/popup.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ document.getElementById("start").addEventListener("click", async () => {
2222
const [tab] = await chrome.tabs.query({
2323
active: true, currentWindow: true,
2424
});
25-
await send("start", { startUrl: tab && tab.url });
25+
await send("start", { startUrl: tab?.url });
2626
refresh();
2727
});
2828

@@ -50,4 +50,8 @@ document.getElementById("export").addEventListener("click", async () => {
5050
});
5151
});
5252

53-
refresh();
53+
// Popup HTML loads this script as a classic script (not a module), so
54+
// top-level await isn't legal here. ``void refresh()`` is the
55+
// equivalent fire-and-forget that Sonar's S7785 accepts in this
56+
// context (we don't await because the initial fetch is best-effort).
57+
void refresh();

docker/Dockerfile.xfce

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ ENV DISPLAY=:99 \
4747
AUTOCONTROL_HEADLESS=0 \
4848
AUTOCONTROL_VNC_PORT=5900
4949

50+
# hadolint ignore=DL3018
51+
# NOSONAR docker:S6473 — exposing 5900 (VNC) is the documented purpose
52+
# of this variant; operators bind it behind a firewall / VPN, with the
53+
# optional AUTOCONTROL_VNC_PASSWORD providing TightVNC-level auth.
5054
EXPOSE 9939 9940 8765 5900
5155

5256
COPY docker/entrypoint-xfce.sh /usr/local/bin/autocontrol-entrypoint

examples/18_slack_daily_report.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,15 @@ def render_report(summary: str, raw_messages: Sequence[SlackMessage],
176176
<ul>{rows}</ul>
177177
</body></html>
178178
"""
179-
html_path = output_dir / f"digest-{today}.html"
179+
# Anchor the filename inside output_dir's resolved path so a
180+
# malicious ``today`` (e.g. ``../etc/passwd``) can't escape — even
181+
# though ``today`` is internally generated, validating here keeps
182+
# Sonar's S2083 happy and protects future callers.
183+
safe_name = os.path.basename(f"digest-{today}.html")
184+
safe_root = output_dir.resolve()
185+
html_path = (safe_root / safe_name).resolve()
186+
if safe_root not in html_path.parents:
187+
raise ValueError(f"refusing path-traversal name: {today!r}")
180188
html_path.write_text(html, encoding="utf-8")
181189
try:
182190
from weasyprint import HTML
@@ -212,6 +220,10 @@ def email_report(artefact: Path, *,
212220
filename=artefact.name,
213221
)
214222
context = ssl.create_default_context()
223+
# Pin the minimum to TLS 1.2 even on older Pythons whose defaults
224+
# may still allow TLS 1.0/1.1 (S4423). Python 3.10+ already does
225+
# this by default; the explicit assignment is a belt-and-braces.
226+
context.minimum_version = ssl.TLSVersion.TLSv1_2
215227
with smtplib.SMTP(smtp_host, smtp_port, timeout=30.0) as server:
216228
server.starttls(context=context)
217229
if smtp_user and smtp_pass:

0 commit comments

Comments
 (0)