code-analyser 1.0.2__tar.gz → 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_analyser-1.0.2 → code_analyser-1.0.3}/PKG-INFO +1 -1
- {code_analyser-1.0.2 → code_analyser-1.0.3}/pyproject.toml +5 -2
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/__init__.py +5 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/api.py +6 -2
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/javascript_.py +7 -0
- code_analyser-1.0.3/src/code_analyser/core/sql_.py +91 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/typescript_.py +19 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/models.py +7 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/api/test_api.py +3 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/conftest.py +12 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/integration/test_full_pipeline.py +1 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/integration/test_pipeline.py +17 -8
- code_analyser-1.0.3/tests/test_invariants.py +57 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_css_.py +0 -6
- code_analyser-1.0.3/tests/unit/test_javascript_.py +59 -0
- code_analyser-1.0.3/tests/unit/test_models.py +144 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_python_.py +12 -1
- code_analyser-1.0.3/tests/unit/test_sql_.py +73 -0
- code_analyser-1.0.3/tests/unit/test_typescript_.py +59 -0
- code_analyser-1.0.2/src/code_analyser/core/sql_.py +0 -55
- code_analyser-1.0.2/tests/unit/test_javascript_.py +0 -46
- code_analyser-1.0.2/tests/unit/test_models.py +0 -116
- code_analyser-1.0.2/tests/unit/test_sql_.py +0 -38
- code_analyser-1.0.2/tests/unit/test_typescript_.py +0 -37
- {code_analyser-1.0.2 → code_analyser-1.0.3}/.dockerignore +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/.env.example +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/.gitignore +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/LICENSE +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/README.md +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/docs/superpowers/plans/2026-05-06-code-analyser-rewrite.md +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/docs/superpowers/specs/2026-05-05-code-analyser-design.md +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/cli.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/__init__.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/css_.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/html_.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/notebook_.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/core/python_.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/detect.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/llm.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/pipeline.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/src/code_analyser/settings.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/__init__.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/api/__init__.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/cli/__init__.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/cli/test_cli.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/integration/__init__.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/__init__.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_detect.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_html_.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_llm.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_notebook_.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/tests/unit/test_scaffold.py +0 -0
- {code_analyser-1.0.2 → code_analyser-1.0.3}/uv.lock +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-analyser"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.3"
|
|
8
8
|
description = "Source code analyser — part of the analyser family"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -44,7 +44,10 @@ python_files = ["test_*.py"]
|
|
|
44
44
|
python_classes = ["Test*"]
|
|
45
45
|
python_functions = ["test_*"]
|
|
46
46
|
pythonpath = ["src", "tests"]
|
|
47
|
-
addopts =
|
|
47
|
+
addopts = "--cov=src/code_analyser --cov-report=term-missing --strict-markers -m 'not slow'"
|
|
48
|
+
markers = [
|
|
49
|
+
"slow: tests that invoke external tools (ruff, html5lib parse, w3c) — opt-in with `pytest -m slow`",
|
|
50
|
+
]
|
|
48
51
|
|
|
49
52
|
[tool.ruff]
|
|
50
53
|
target-version = "py310"
|
|
@@ -11,14 +11,18 @@ from .pipeline import CodeAnalyser
|
|
|
11
11
|
|
|
12
12
|
_start_time = time.time()
|
|
13
13
|
|
|
14
|
-
app = FastAPI(title="code-analyser", version="
|
|
14
|
+
app = FastAPI(title="code-analyser", version=version("code-analyser"))
|
|
15
15
|
|
|
16
16
|
_analyser = CodeAnalyser()
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@app.get("/health")
|
|
20
20
|
def health() -> dict:
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
"status": "ok",
|
|
23
|
+
"uptime": round(time.time() - _start_time, 1),
|
|
24
|
+
"version": version("code-analyser"),
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
@app.post("/analyse", response_model=CodeAnalysis)
|
|
@@ -14,10 +14,17 @@ _COMMENT_LINE_RE = re.compile(r"^\s*(?://|/\*|\*)", re.MULTILINE)
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def analyse_javascript(source: str, *, jsx: bool = False) -> JSMetrics:
|
|
17
|
+
"""Analyse a JavaScript source string and return structural metrics.
|
|
18
|
+
|
|
19
|
+
When ``jsx=True``, esprima parses JSX expressions (e.g.
|
|
20
|
+
``<Component prop={value}/>``).
|
|
21
|
+
"""
|
|
17
22
|
if not _ESPRIMA_AVAILABLE:
|
|
18
23
|
return _fallback_metrics(source)
|
|
19
24
|
|
|
20
25
|
opts = {"tolerant": True, "comment": True}
|
|
26
|
+
if jsx:
|
|
27
|
+
opts["jsx"] = True
|
|
21
28
|
tree = None
|
|
22
29
|
parse_failed = False
|
|
23
30
|
try:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# src/code_analyser/core/sql_.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import sqlparse
|
|
6
|
+
from sqlparse.sql import Parenthesis, Where
|
|
7
|
+
from sqlparse.tokens import DML, Punctuation
|
|
8
|
+
|
|
9
|
+
from ..models import SQLMetrics
|
|
10
|
+
|
|
11
|
+
_JOIN_RE = re.compile(r"\bJOIN\b", re.IGNORECASE)
|
|
12
|
+
_SELECT_STAR_RE = re.compile(r"\bSELECT\s+\*", re.IGNORECASE)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def analyse_sql(source: str) -> SQLMetrics:
|
|
16
|
+
statements = [s for s in sqlparse.parse(source) if str(s).strip()]
|
|
17
|
+
|
|
18
|
+
query_types: dict[str, int] = {}
|
|
19
|
+
join_count = 0
|
|
20
|
+
unsafe: list[str] = []
|
|
21
|
+
|
|
22
|
+
for stmt in statements:
|
|
23
|
+
stype = (stmt.get_type() or "UNKNOWN").upper()
|
|
24
|
+
query_types[stype] = query_types.get(stype, 0) + 1
|
|
25
|
+
|
|
26
|
+
stmt_str = str(stmt)
|
|
27
|
+
join_count += len(_JOIN_RE.findall(stmt_str))
|
|
28
|
+
|
|
29
|
+
if stype == "UPDATE" and not any(isinstance(tok, Where) for tok in stmt.tokens):
|
|
30
|
+
unsafe.append("UPDATE without WHERE")
|
|
31
|
+
elif stype == "DELETE" and not any(isinstance(tok, Where) for tok in stmt.tokens):
|
|
32
|
+
unsafe.append("DELETE without WHERE")
|
|
33
|
+
if _SELECT_STAR_RE.search(stmt_str):
|
|
34
|
+
unsafe.append("SELECT *")
|
|
35
|
+
|
|
36
|
+
subquery_depth = _max_subquery_depth(source)
|
|
37
|
+
|
|
38
|
+
return SQLMetrics(
|
|
39
|
+
statement_count=len(statements),
|
|
40
|
+
query_types=query_types,
|
|
41
|
+
join_count=join_count,
|
|
42
|
+
subquery_depth=subquery_depth,
|
|
43
|
+
unsafe_patterns=list(dict.fromkeys(unsafe)),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _paren_starts_with_select(token: Parenthesis) -> bool:
|
|
48
|
+
"""True iff the first non-whitespace token after ``(`` is a SELECT DML."""
|
|
49
|
+
for t in token.flatten():
|
|
50
|
+
if t.is_whitespace:
|
|
51
|
+
continue
|
|
52
|
+
if t.ttype is Punctuation and t.value == "(":
|
|
53
|
+
continue
|
|
54
|
+
return t.ttype is DML and t.value.upper() == "SELECT"
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _walk(token, current_depth: int, current_max: int) -> int:
|
|
59
|
+
if isinstance(token, Parenthesis) and _paren_starts_with_select(token):
|
|
60
|
+
current_depth += 1
|
|
61
|
+
current_max = max(current_max, current_depth)
|
|
62
|
+
if hasattr(token, "tokens"):
|
|
63
|
+
for child in token.tokens:
|
|
64
|
+
current_max = _walk(child, current_depth, current_max)
|
|
65
|
+
return current_max
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _max_subquery_depth(source: str) -> int:
|
|
69
|
+
"""Return the maximum nesting depth of SELECT statements.
|
|
70
|
+
|
|
71
|
+
A top-level SELECT is depth 1. A subquery (a ``Parenthesis`` whose
|
|
72
|
+
first non-whitespace token is a SELECT keyword) inside another
|
|
73
|
+
SELECT is depth 2. Subquery-in-subquery is depth 3. Etc.
|
|
74
|
+
|
|
75
|
+
Walks the sqlparse token tree so that non-subquery parens — e.g.
|
|
76
|
+
``VALUES (...)``, ``CAST(x AS y)``, or arithmetic ``(a + (b + c))`` —
|
|
77
|
+
do NOT inflate the count.
|
|
78
|
+
"""
|
|
79
|
+
parsed = sqlparse.parse(source)
|
|
80
|
+
max_depth = 0
|
|
81
|
+
for stmt in parsed:
|
|
82
|
+
# Top-level SELECT counts as depth 1.
|
|
83
|
+
for t in stmt.flatten():
|
|
84
|
+
if t.is_whitespace:
|
|
85
|
+
continue
|
|
86
|
+
if t.ttype is DML and t.value.upper() == "SELECT":
|
|
87
|
+
max_depth = max(max_depth, 1)
|
|
88
|
+
break
|
|
89
|
+
for tok in stmt.tokens:
|
|
90
|
+
max_depth = max(max_depth, _walk(tok, 1, max_depth))
|
|
91
|
+
return max_depth
|
|
@@ -26,6 +26,25 @@ _RETURN_TYPE_RE = re.compile(
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def analyse_typescript(source: str, *, tsx: bool = False) -> TSMetrics:
|
|
29
|
+
"""Extract regex-based stats from a TypeScript source string.
|
|
30
|
+
|
|
31
|
+
This is a HEURISTIC analyser, not a real TS parser. It counts
|
|
32
|
+
declarations (functions, interfaces, type aliases, imports), notes
|
|
33
|
+
annotations and arrow expressions, and reports a coarse "looks like
|
|
34
|
+
syntactically balanced" check via brace counting.
|
|
35
|
+
|
|
36
|
+
For real TypeScript semantic analysis (proper type checking,
|
|
37
|
+
accurate syntax validity, JSX/TSX-aware parsing), a future release
|
|
38
|
+
will offer an optional [parser] extra using tree-sitter-typescript.
|
|
39
|
+
For now, the regex stats are useful as stats — they're not pretending
|
|
40
|
+
to be parsing.
|
|
41
|
+
|
|
42
|
+
The ``tsx`` parameter is currently unused (TSX requires a real
|
|
43
|
+
parser; the brace heuristic doesn't change with TSX).
|
|
44
|
+
"""
|
|
45
|
+
# Heuristic: source is "structurally balanced" if open/close braces match
|
|
46
|
+
# within ±3. Not a real parse — a real parser would catch many things this
|
|
47
|
+
# misses (e.g. `function foo() { if (x { return; } }` passes).
|
|
29
48
|
syntax_valid = abs(source.count("{") - source.count("}")) <= 3
|
|
30
49
|
|
|
31
50
|
function_count = len(_FUNC_RE.findall(source))
|
|
@@ -128,6 +128,13 @@ class JSMetrics(BaseModel):
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
class TSMetrics(JSMetrics):
|
|
131
|
+
syntax_valid: bool = Field(
|
|
132
|
+
description=(
|
|
133
|
+
"For TypeScript, this is a brace-balance heuristic "
|
|
134
|
+
"(open vs close `{}` within ±3), NOT parser-based. "
|
|
135
|
+
"A real TS parser would catch syntax errors this misses."
|
|
136
|
+
),
|
|
137
|
+
)
|
|
131
138
|
type_annotation_coverage: Annotated[float, Field(ge=0.0)]
|
|
132
139
|
interface_count: Annotated[int, Field(ge=0)]
|
|
133
140
|
type_alias_count: Annotated[int, Field(ge=0)]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import zipfile
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
3
5
|
import pytest
|
|
4
6
|
from fastapi.testclient import TestClient
|
|
5
7
|
from conftest import VALID_PYTHON, VALID_HTML
|
|
@@ -17,6 +19,7 @@ def test_health(client):
|
|
|
17
19
|
data = r.json()
|
|
18
20
|
assert data["status"] == "ok"
|
|
19
21
|
assert "uptime" in data
|
|
22
|
+
assert data["version"] == version("code-analyser")
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def test_analyse_python(client):
|
|
@@ -77,6 +77,18 @@ const double = (x) => x * 2;
|
|
|
77
77
|
const asyncLoad = async () => { return await fetch('/api'); };
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
+
VALID_JSX = """\
|
|
81
|
+
import React from 'react';
|
|
82
|
+
|
|
83
|
+
const App = () => (
|
|
84
|
+
<div className="app">
|
|
85
|
+
<h1>Hello, world</h1>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
export default App;
|
|
90
|
+
"""
|
|
91
|
+
|
|
80
92
|
VALID_TS = """\
|
|
81
93
|
import { Component } from '@angular/core';
|
|
82
94
|
|
|
@@ -2,7 +2,7 @@ import io
|
|
|
2
2
|
import zipfile
|
|
3
3
|
import pytest
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from conftest import VALID_PYTHON, VALID_HTML, VALID_CSS
|
|
5
|
+
from conftest import VALID_PYTHON, VALID_HTML, VALID_CSS, VALID_JSX
|
|
6
6
|
from code_analyser.pipeline import CodeAnalyser
|
|
7
7
|
from code_analyser.models import CodeAnalysis
|
|
8
8
|
|
|
@@ -66,6 +66,22 @@ def test_unsupported_single_file_raises(tmp_path):
|
|
|
66
66
|
CodeAnalyser().analyse(p)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def test_jsx_file_analyses_cleanly(tmp_path):
|
|
70
|
+
"""End-to-end: a .jsx file routes through the pipeline with jsx=True
|
|
71
|
+
so esprima parses the JSX without error."""
|
|
72
|
+
p = tmp_path / "App.jsx"
|
|
73
|
+
p.write_text(VALID_JSX)
|
|
74
|
+
result = CodeAnalyser().analyse(p)
|
|
75
|
+
assert result.file_count == 1
|
|
76
|
+
f = result.files[0]
|
|
77
|
+
assert f.language == "javascript"
|
|
78
|
+
assert f.metrics is not None
|
|
79
|
+
assert f.metrics.syntax_valid is True
|
|
80
|
+
assert f.metrics.parse_error_count == 0
|
|
81
|
+
# The arrow `() => (...)` should be counted.
|
|
82
|
+
assert f.metrics.arrow_function_count >= 1
|
|
83
|
+
|
|
84
|
+
|
|
69
85
|
def test_cross_file_has_package_json(tmp_path):
|
|
70
86
|
zip_bytes = _make_zip(
|
|
71
87
|
("app.js", "console.log('hi')"),
|
|
@@ -75,10 +91,3 @@ def test_cross_file_has_package_json(tmp_path):
|
|
|
75
91
|
p.write_bytes(zip_bytes)
|
|
76
92
|
result = CodeAnalyser().analyse(p)
|
|
77
93
|
assert result.cross_file.has_package_json is True
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_languages_detected(tmp_path):
|
|
81
|
-
p = tmp_path / "app.py"
|
|
82
|
-
p.write_text(VALID_PYTHON)
|
|
83
|
-
result = CodeAnalyser().analyse(p)
|
|
84
|
-
assert "python" in result.cross_file.languages_detected
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Invariant tests — fast, run by default."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_package_imports_cleanly() -> None:
|
|
9
|
+
"""The package must import. Smoke alarm for packaging bugs."""
|
|
10
|
+
import code_analyser # noqa: F401
|
|
11
|
+
from code_analyser.api import app # noqa: F401
|
|
12
|
+
from code_analyser.cli import main # noqa: F401
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_package_exposes_version() -> None:
|
|
16
|
+
"""code_analyser.__version__ must equal the installed package metadata."""
|
|
17
|
+
import code_analyser
|
|
18
|
+
assert code_analyser.__version__ == version("code-analyser")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_health_version_matches_installed_package() -> None:
|
|
22
|
+
"""/health must report the actual installed package version."""
|
|
23
|
+
from fastapi.testclient import TestClient
|
|
24
|
+
from code_analyser.api import app
|
|
25
|
+
|
|
26
|
+
client = TestClient(app)
|
|
27
|
+
response = client.get("/health")
|
|
28
|
+
assert response.status_code == 200
|
|
29
|
+
assert response.json()["version"] == version("code-analyser")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_unsupported_extension_raises_loudly(tmp_path) -> None:
|
|
33
|
+
"""Pipeline must raise on unsupported single-file input — not silently skip."""
|
|
34
|
+
from code_analyser.pipeline import CodeAnalyser
|
|
35
|
+
|
|
36
|
+
p = tmp_path / "image.png"
|
|
37
|
+
p.write_bytes(b"\x89PNG fake")
|
|
38
|
+
with pytest.raises(ValueError, match="Unsupported"):
|
|
39
|
+
CodeAnalyser().analyse(p)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_llm_signals_none_without_extra(monkeypatch) -> None:
|
|
43
|
+
"""Without [llm] extra installed, LLM signal calls return None — not crash.
|
|
44
|
+
|
|
45
|
+
Family pattern: optional features fail gracefully (return None) rather
|
|
46
|
+
than raising ImportError when the user didn't ask for them.
|
|
47
|
+
|
|
48
|
+
The local analyse_llm signature takes ``list[tuple[str, str]]`` and short-
|
|
49
|
+
circuits to ``([None]*N, None)`` when either anthropic isn't importable
|
|
50
|
+
or ``ANTHROPIC_API_KEY`` is missing — exercise the env-missing path.
|
|
51
|
+
"""
|
|
52
|
+
from code_analyser.llm import analyse_llm
|
|
53
|
+
|
|
54
|
+
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
55
|
+
file_signals, top_signal = analyse_llm([("app.py", "print('hi')")])
|
|
56
|
+
assert file_signals == [None]
|
|
57
|
+
assert top_signal is None
|
|
@@ -59,9 +59,3 @@ def test_important_count(monkeypatch):
|
|
|
59
59
|
css = "a{color:red!important}.b{font-size:12px!important}"
|
|
60
60
|
m = analyse_css(css)
|
|
61
61
|
assert m.important_count == 2
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_w3c_fallback(monkeypatch):
|
|
65
|
-
_no_network(monkeypatch)
|
|
66
|
-
m = analyse_css(VALID_CSS, timeout=1.0)
|
|
67
|
-
assert m.validator == "local"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from conftest import VALID_JS
|
|
2
|
+
from code_analyser.core.javascript_ import analyse_javascript
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_function_count():
|
|
6
|
+
m = analyse_javascript(VALID_JS)
|
|
7
|
+
assert m.function_count == 1 # greet
|
|
8
|
+
assert m.arrow_function_count == 2 # double, asyncLoad
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_async_count():
|
|
12
|
+
m = analyse_javascript(VALID_JS)
|
|
13
|
+
assert m.async_function_count == 1 # asyncLoad
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_console_log():
|
|
17
|
+
m = analyse_javascript(VALID_JS)
|
|
18
|
+
assert m.console_log_count == 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_import_count():
|
|
22
|
+
m = analyse_javascript(VALID_JS)
|
|
23
|
+
assert m.import_count == 1 # the single `import { helper } from './utils.js';`
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_todo_count():
|
|
27
|
+
js = "// TODO: fix this\nfunction foo(){}"
|
|
28
|
+
m = analyse_javascript(js)
|
|
29
|
+
assert m.todo_count == 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_invalid_js():
|
|
33
|
+
m = analyse_javascript("function foo( {")
|
|
34
|
+
assert m.syntax_valid is False
|
|
35
|
+
assert m.parse_error_count == 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_comment_coverage():
|
|
39
|
+
# VALID_JS has 1 function (greet) + 2 arrows (double, asyncLoad) = 3 callable units.
|
|
40
|
+
# Exactly 1 line matches the comment-line regex (`// say hello`), so
|
|
41
|
+
# coverage = 1/3 ≈ 0.333. Anchor against the actual ratio rather than `>= 0.0`.
|
|
42
|
+
m = analyse_javascript(VALID_JS)
|
|
43
|
+
assert 0.30 < m.comment_coverage < 0.40
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_jsx_parsing_when_enabled():
|
|
47
|
+
"""JSX expressions parse cleanly when jsx=True."""
|
|
48
|
+
src = "const App = () => <div>hello</div>;"
|
|
49
|
+
result = analyse_javascript(src, jsx=True)
|
|
50
|
+
assert result.syntax_valid is True
|
|
51
|
+
assert result.parse_error_count == 0
|
|
52
|
+
assert result.arrow_function_count == 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_jsx_fails_without_flag():
|
|
56
|
+
"""JSX without jsx=True fails to parse (proves the flag actually does something)."""
|
|
57
|
+
src = "const App = () => <div>hello</div>;"
|
|
58
|
+
result = analyse_javascript(src, jsx=False)
|
|
59
|
+
assert result.syntax_valid is False
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Validation-focused tests for the public Pydantic models.
|
|
2
|
+
|
|
3
|
+
This file used to be wall-to-wall echo-constructor tests ("construct with
|
|
4
|
+
literal X, assert literal X back") — those are tautological because Pydantic
|
|
5
|
+
already round-trips literals by definition. The trimmed file below keeps a
|
|
6
|
+
couple of shape-validation cases and adds two real validator tests that
|
|
7
|
+
exercise the constraints in models.py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from code_analyser.models import (
|
|
14
|
+
CSSMetrics,
|
|
15
|
+
HTMLMetrics,
|
|
16
|
+
NotebookMetrics,
|
|
17
|
+
PythonMetrics,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _python_metrics_kwargs(**overrides) -> dict:
|
|
22
|
+
"""Minimal valid PythonMetrics kwargs; override any field per-test."""
|
|
23
|
+
base = dict(
|
|
24
|
+
syntax_valid=True,
|
|
25
|
+
lint_error_count=0,
|
|
26
|
+
lint_warning_count=0,
|
|
27
|
+
lint_violations=[],
|
|
28
|
+
cyclomatic_complexity=1.0,
|
|
29
|
+
max_nesting_depth=0,
|
|
30
|
+
loc=10,
|
|
31
|
+
comment_lines=2,
|
|
32
|
+
blank_lines=1,
|
|
33
|
+
function_count=1,
|
|
34
|
+
class_count=0,
|
|
35
|
+
docstring_coverage=1.0,
|
|
36
|
+
naming_convention="snake_case",
|
|
37
|
+
imports=["os"],
|
|
38
|
+
todo_count=0,
|
|
39
|
+
print_count=0,
|
|
40
|
+
type_annotation_coverage=1.0,
|
|
41
|
+
has_main_guard=False,
|
|
42
|
+
bare_except_count=0,
|
|
43
|
+
comprehension_count=0,
|
|
44
|
+
)
|
|
45
|
+
base.update(overrides)
|
|
46
|
+
return base
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _html_metrics_kwargs(**overrides) -> dict:
|
|
50
|
+
base = dict(
|
|
51
|
+
syntax_valid=True,
|
|
52
|
+
parse_error_count=0,
|
|
53
|
+
validator="local",
|
|
54
|
+
w3c_errors=[],
|
|
55
|
+
has_doctype=True,
|
|
56
|
+
semantic_elements_used=[],
|
|
57
|
+
semantic_element_count=0,
|
|
58
|
+
div_count=0,
|
|
59
|
+
span_count=0,
|
|
60
|
+
div_to_semantic_ratio=None,
|
|
61
|
+
inline_script_count=0,
|
|
62
|
+
inline_style_count=0,
|
|
63
|
+
inline_event_handler_count=0,
|
|
64
|
+
comment_count=0,
|
|
65
|
+
external_scripts=[],
|
|
66
|
+
external_stylesheets=[],
|
|
67
|
+
cdn_count=0,
|
|
68
|
+
frameworks_detected=[],
|
|
69
|
+
img_alt_coverage=1.0,
|
|
70
|
+
form_label_coverage=1.0,
|
|
71
|
+
has_lang_attr=True,
|
|
72
|
+
has_title=True,
|
|
73
|
+
heading_hierarchy_valid=True,
|
|
74
|
+
aria_attribute_count=0,
|
|
75
|
+
ambiguous_link_count=0,
|
|
76
|
+
)
|
|
77
|
+
base.update(overrides)
|
|
78
|
+
return base
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_python_metrics_default_values():
|
|
82
|
+
"""A valid construction round-trips and naming_convention enum holds."""
|
|
83
|
+
m = PythonMetrics(**_python_metrics_kwargs())
|
|
84
|
+
# Spot-check a handful of fields — not the full echo battery.
|
|
85
|
+
assert m.syntax_valid is True
|
|
86
|
+
assert m.naming_convention == "snake_case"
|
|
87
|
+
assert m.lint_violations == [] # list default preserved
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_notebook_metrics_default_python_metrics_is_none():
|
|
91
|
+
"""NotebookMetrics.python_metrics defaults to None when omitted."""
|
|
92
|
+
n = NotebookMetrics(
|
|
93
|
+
code_cell_count=2,
|
|
94
|
+
markdown_cell_count=1,
|
|
95
|
+
has_outputs=False,
|
|
96
|
+
output_cell_count=0,
|
|
97
|
+
execution_order_valid=True,
|
|
98
|
+
magic_command_count=1,
|
|
99
|
+
)
|
|
100
|
+
assert n.python_metrics is None
|
|
101
|
+
assert n.code_cell_count == 2
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_html_metrics_default_construction():
|
|
105
|
+
"""HTMLMetrics constructs with a valid 'local' validator and minimal fields."""
|
|
106
|
+
m = HTMLMetrics(**_html_metrics_kwargs())
|
|
107
|
+
assert m.validator == "local"
|
|
108
|
+
assert m.div_to_semantic_ratio is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_python_metrics_rejects_negative_loc():
|
|
112
|
+
"""PythonMetrics.loc has Field(ge=0); negative values must be rejected."""
|
|
113
|
+
with pytest.raises(ValidationError):
|
|
114
|
+
PythonMetrics(**_python_metrics_kwargs(loc=-1))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_html_metrics_rejects_invalid_validator_literal():
|
|
118
|
+
"""HTMLMetrics.validator is Literal["w3c", "local"]; bogus values rejected."""
|
|
119
|
+
with pytest.raises(ValidationError):
|
|
120
|
+
HTMLMetrics(**_html_metrics_kwargs(validator="bogus"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_css_metrics_rejects_invalid_dominant_layout():
|
|
124
|
+
"""CSSMetrics.dominant_layout is a closed Literal; bogus values rejected."""
|
|
125
|
+
with pytest.raises(ValidationError):
|
|
126
|
+
CSSMetrics(
|
|
127
|
+
syntax_valid=True,
|
|
128
|
+
parse_error_count=0,
|
|
129
|
+
validator="local",
|
|
130
|
+
w3c_errors=[],
|
|
131
|
+
w3c_warnings=[],
|
|
132
|
+
rule_count=0,
|
|
133
|
+
selector_count=0,
|
|
134
|
+
important_count=0,
|
|
135
|
+
duplicate_selector_count=0,
|
|
136
|
+
media_query_count=0,
|
|
137
|
+
custom_property_count=0,
|
|
138
|
+
comment_count=0,
|
|
139
|
+
float_count=0,
|
|
140
|
+
flexbox_count=0,
|
|
141
|
+
grid_count=0,
|
|
142
|
+
dominant_layout="bogus", # invalid Literal
|
|
143
|
+
float_used_for_layout=False,
|
|
144
|
+
)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# tests/unit/test_python_.py
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
2
4
|
import pytest
|
|
3
5
|
from conftest import VALID_PYTHON, PYTHON_WITH_ISSUES
|
|
4
6
|
from code_analyser.core.python_ import analyse_python
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
@pytest.mark.slow
|
|
7
10
|
def test_valid_python_signals():
|
|
8
11
|
m = analyse_python(VALID_PYTHON)
|
|
9
12
|
assert m.syntax_valid is True
|
|
@@ -66,9 +69,17 @@ def test_comprehensions():
|
|
|
66
69
|
assert m.comprehension_count == 3
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
@pytest.mark.slow
|
|
69
73
|
def test_ruff_violations_shape():
|
|
70
|
-
|
|
74
|
+
"""ruff is invoked as a subprocess in python_._run_ruff; skip when the
|
|
75
|
+
binary is not on PATH. PYTHON_WITH_ISSUES has bare except / unused
|
|
76
|
+
imports / etc., so when ruff IS available we expect at least one
|
|
77
|
+
violation — without that assertion the for-loop is vacuous.
|
|
78
|
+
"""
|
|
79
|
+
if shutil.which("ruff") is None:
|
|
80
|
+
pytest.skip("ruff binary not on PATH; cannot exercise lint subprocess")
|
|
71
81
|
m = analyse_python(PYTHON_WITH_ISSUES)
|
|
82
|
+
assert len(m.lint_violations) > 0
|
|
72
83
|
for v in m.lint_violations:
|
|
73
84
|
assert isinstance(v.code, str)
|
|
74
85
|
assert isinstance(v.line, int)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from conftest import VALID_SQL, UNSAFE_SQL
|
|
2
|
+
from code_analyser.core.sql_ import analyse_sql
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_statement_count():
|
|
6
|
+
m = analyse_sql(VALID_SQL)
|
|
7
|
+
assert m.statement_count == 5
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_query_types():
|
|
11
|
+
m = analyse_sql(VALID_SQL)
|
|
12
|
+
assert m.query_types.get("SELECT", 0) >= 1
|
|
13
|
+
assert m.query_types.get("INSERT", 0) == 1
|
|
14
|
+
assert m.query_types.get("UPDATE", 0) == 1
|
|
15
|
+
assert m.query_types.get("DELETE", 0) == 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_join_count():
|
|
19
|
+
m = analyse_sql(VALID_SQL)
|
|
20
|
+
assert m.join_count == 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_no_unsafe_patterns():
|
|
24
|
+
m = analyse_sql(VALID_SQL)
|
|
25
|
+
assert m.unsafe_patterns == []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_unsafe_patterns():
|
|
29
|
+
"""UNSAFE_SQL fixture: UPDATE-without-WHERE, DELETE-without-WHERE, SELECT *
|
|
30
|
+
— assert each pattern fired explicitly rather than hiding behind an or-chain.
|
|
31
|
+
"""
|
|
32
|
+
m = analyse_sql(UNSAFE_SQL)
|
|
33
|
+
assert len(m.unsafe_patterns) == 3
|
|
34
|
+
assert "UPDATE without WHERE" in m.unsafe_patterns
|
|
35
|
+
assert "DELETE without WHERE" in m.unsafe_patterns
|
|
36
|
+
assert "SELECT *" in m.unsafe_patterns
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_subquery_depth():
|
|
40
|
+
"""`subquery_depth` counts real SELECT-statement nesting via sqlparse.
|
|
41
|
+
Three nested SELECTs -> depth 3.
|
|
42
|
+
"""
|
|
43
|
+
sql = "SELECT * FROM (SELECT id FROM (SELECT id FROM users) t1) t2;"
|
|
44
|
+
m = analyse_sql(sql)
|
|
45
|
+
assert m.subquery_depth == 3
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_no_subquery_with_nested_parens():
|
|
49
|
+
"""Nested parens that DO NOT contain SELECT must not inflate depth.
|
|
50
|
+
|
|
51
|
+
Pins the bug in the previous paren-counting implementation:
|
|
52
|
+
`SELECT (a + (b + c)) FROM t` had three levels of parens but zero
|
|
53
|
+
nested subqueries, so the correct depth is 1.
|
|
54
|
+
"""
|
|
55
|
+
m = analyse_sql("SELECT (a + (b + c)) FROM t")
|
|
56
|
+
assert m.subquery_depth == 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_three_level_subquery():
|
|
60
|
+
"""Three real SELECTs nested via IN (...) clauses -> depth 3."""
|
|
61
|
+
sql = (
|
|
62
|
+
"SELECT id FROM users WHERE x IN "
|
|
63
|
+
"(SELECT y FROM t WHERE z IN (SELECT a FROM u))"
|
|
64
|
+
)
|
|
65
|
+
m = analyse_sql(sql)
|
|
66
|
+
assert m.subquery_depth == 3
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_unsafe_sql_subquery_depth():
|
|
70
|
+
"""UNSAFE_SQL fixture is three flat statements (UPDATE, DELETE, SELECT *) —
|
|
71
|
+
no nested SELECTs, so depth is 1 (the top-level SELECT)."""
|
|
72
|
+
m = analyse_sql(UNSAFE_SQL)
|
|
73
|
+
assert m.subquery_depth == 1
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from conftest import VALID_TS
|
|
2
|
+
from code_analyser.core.typescript_ import analyse_typescript
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_function_count():
|
|
6
|
+
m = analyse_typescript(VALID_TS)
|
|
7
|
+
assert m.function_count == 1 # greet
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_interface_count():
|
|
11
|
+
m = analyse_typescript(VALID_TS)
|
|
12
|
+
assert m.interface_count == 1 # User
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_type_alias_count():
|
|
16
|
+
m = analyse_typescript(VALID_TS)
|
|
17
|
+
assert m.type_alias_count == 1 # ID
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_type_annotation_coverage_positive():
|
|
21
|
+
m = analyse_typescript(VALID_TS)
|
|
22
|
+
assert m.type_annotation_coverage > 0.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_import_count():
|
|
26
|
+
m = analyse_typescript(VALID_TS)
|
|
27
|
+
assert m.import_count == 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_arrow_function_count():
|
|
31
|
+
m = analyse_typescript(VALID_TS)
|
|
32
|
+
assert m.arrow_function_count == 1 # add
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_typescript_unmatched_braces():
|
|
36
|
+
"""Brace-balance heuristic flags clearly unbalanced braces as not-balanced."""
|
|
37
|
+
# typescript_.analyse_typescript: syntax_valid = abs(open - close) <= 3.
|
|
38
|
+
# Source below has 5 `{` and 0 `}`, so the diff is 5 → syntax_valid is False.
|
|
39
|
+
result = analyse_typescript(
|
|
40
|
+
"function foo() { if (a) { if (b) { if (c) { if (d) { return 42"
|
|
41
|
+
)
|
|
42
|
+
assert result.syntax_valid is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_brace_heuristic_misses_real_syntax_errors():
|
|
46
|
+
"""The brace heuristic is NOT a real TS parser.
|
|
47
|
+
|
|
48
|
+
This source is syntactically invalid TypeScript (missing `)` after `(x`)
|
|
49
|
+
but the open and close braces are balanced, so the heuristic reports
|
|
50
|
+
`syntax_valid=True`. This test pins the documented limitation: brace
|
|
51
|
+
balance is not parsing. Its existence is the contract — when (if) we
|
|
52
|
+
ever add a real parser via the [parser] extra, this test will need
|
|
53
|
+
to be updated, and that's the signal.
|
|
54
|
+
"""
|
|
55
|
+
src = "function foo() { if (x { return; } }"
|
|
56
|
+
# Sanity: braces ARE balanced (3 open, 3 close).
|
|
57
|
+
assert src.count("{") == src.count("}")
|
|
58
|
+
result = analyse_typescript(src)
|
|
59
|
+
assert result.syntax_valid is True
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# src/code_analyser/core/sql_.py
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
import sqlparse
|
|
6
|
-
from sqlparse.sql import Where
|
|
7
|
-
|
|
8
|
-
from ..models import SQLMetrics
|
|
9
|
-
|
|
10
|
-
_JOIN_RE = re.compile(r"\bJOIN\b", re.IGNORECASE)
|
|
11
|
-
_SELECT_STAR_RE = re.compile(r"\bSELECT\s+\*", re.IGNORECASE)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def analyse_sql(source: str) -> SQLMetrics:
|
|
15
|
-
statements = [s for s in sqlparse.parse(source) if str(s).strip()]
|
|
16
|
-
|
|
17
|
-
query_types: dict[str, int] = {}
|
|
18
|
-
join_count = 0
|
|
19
|
-
unsafe: list[str] = []
|
|
20
|
-
|
|
21
|
-
for stmt in statements:
|
|
22
|
-
stype = (stmt.get_type() or "UNKNOWN").upper()
|
|
23
|
-
query_types[stype] = query_types.get(stype, 0) + 1
|
|
24
|
-
|
|
25
|
-
stmt_str = str(stmt)
|
|
26
|
-
join_count += len(_JOIN_RE.findall(stmt_str))
|
|
27
|
-
|
|
28
|
-
if stype == "UPDATE" and not any(isinstance(tok, Where) for tok in stmt.tokens):
|
|
29
|
-
unsafe.append("UPDATE without WHERE")
|
|
30
|
-
elif stype == "DELETE" and not any(isinstance(tok, Where) for tok in stmt.tokens):
|
|
31
|
-
unsafe.append("DELETE without WHERE")
|
|
32
|
-
if _SELECT_STAR_RE.search(stmt_str):
|
|
33
|
-
unsafe.append("SELECT *")
|
|
34
|
-
|
|
35
|
-
subquery_depth = _max_subquery_depth(source)
|
|
36
|
-
|
|
37
|
-
return SQLMetrics(
|
|
38
|
-
statement_count=len(statements),
|
|
39
|
-
query_types=query_types,
|
|
40
|
-
join_count=join_count,
|
|
41
|
-
subquery_depth=subquery_depth,
|
|
42
|
-
unsafe_patterns=list(dict.fromkeys(unsafe)),
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _max_subquery_depth(source: str) -> int:
|
|
47
|
-
depth = 0
|
|
48
|
-
max_depth = 0
|
|
49
|
-
for ch in source:
|
|
50
|
-
if ch == "(":
|
|
51
|
-
depth += 1
|
|
52
|
-
max_depth = max(max_depth, depth)
|
|
53
|
-
elif ch == ")":
|
|
54
|
-
depth = max(0, depth - 1)
|
|
55
|
-
return max_depth
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
from conftest import VALID_JS
|
|
2
|
-
from code_analyser.core.javascript_ import analyse_javascript
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_function_count():
|
|
6
|
-
m = analyse_javascript(VALID_JS)
|
|
7
|
-
assert m.function_count == 1 # greet
|
|
8
|
-
assert m.arrow_function_count == 2 # double, asyncLoad
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_async_count():
|
|
12
|
-
m = analyse_javascript(VALID_JS)
|
|
13
|
-
assert m.async_function_count == 1 # asyncLoad
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_console_log():
|
|
17
|
-
m = analyse_javascript(VALID_JS)
|
|
18
|
-
assert m.console_log_count == 1
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_import_count():
|
|
22
|
-
m = analyse_javascript(VALID_JS)
|
|
23
|
-
assert m.import_count >= 1
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_todo_count():
|
|
27
|
-
js = "// TODO: fix this\nfunction foo(){}"
|
|
28
|
-
m = analyse_javascript(js)
|
|
29
|
-
assert m.todo_count == 1
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_invalid_js():
|
|
33
|
-
m = analyse_javascript("function foo( {")
|
|
34
|
-
assert m.syntax_valid is False
|
|
35
|
-
assert m.parse_error_count == 1
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_comment_coverage():
|
|
39
|
-
m = analyse_javascript(VALID_JS)
|
|
40
|
-
assert m.comment_coverage >= 0.0
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_jsx_mode():
|
|
44
|
-
jsx = "const el = <div className='foo'>Hello</div>;"
|
|
45
|
-
m = analyse_javascript(jsx, jsx=True)
|
|
46
|
-
assert isinstance(m.syntax_valid, bool)
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
from code_analyser.models import (
|
|
2
|
-
LintViolation, PythonMetrics, NotebookMetrics,
|
|
3
|
-
W3CError, W3CCSSError, ExternalResource,
|
|
4
|
-
HTMLMetrics, CSSMetrics, JSMetrics, TSMetrics, SQLMetrics,
|
|
5
|
-
CrossFileSignals, FileLLMSignals, TopLevelLLMSignals,
|
|
6
|
-
FileAnalysis, CodeAnalysis,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_python_metrics_defaults():
|
|
11
|
-
m = PythonMetrics(
|
|
12
|
-
syntax_valid=True, lint_error_count=0, lint_warning_count=0,
|
|
13
|
-
lint_violations=[], cyclomatic_complexity=1.0, max_nesting_depth=0,
|
|
14
|
-
loc=10, comment_lines=2, blank_lines=1, function_count=1,
|
|
15
|
-
class_count=0, docstring_coverage=1.0, naming_convention="snake_case",
|
|
16
|
-
imports=["os"], todo_count=0, print_count=0,
|
|
17
|
-
type_annotation_coverage=1.0, has_main_guard=False,
|
|
18
|
-
bare_except_count=0, comprehension_count=0,
|
|
19
|
-
)
|
|
20
|
-
assert m.syntax_valid is True
|
|
21
|
-
assert m.naming_convention == "snake_case"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_notebook_metrics():
|
|
25
|
-
from code_analyser.models import PythonMetrics
|
|
26
|
-
pm = PythonMetrics(
|
|
27
|
-
syntax_valid=True, lint_error_count=0, lint_warning_count=0,
|
|
28
|
-
lint_violations=[], cyclomatic_complexity=1.0, max_nesting_depth=0,
|
|
29
|
-
loc=5, comment_lines=0, blank_lines=0, function_count=0,
|
|
30
|
-
class_count=0, docstring_coverage=0.0, naming_convention="unknown",
|
|
31
|
-
imports=[], todo_count=0, print_count=1, type_annotation_coverage=0.0,
|
|
32
|
-
has_main_guard=False, bare_except_count=0, comprehension_count=0,
|
|
33
|
-
)
|
|
34
|
-
n = NotebookMetrics(
|
|
35
|
-
code_cell_count=2, markdown_cell_count=1, has_outputs=False,
|
|
36
|
-
output_cell_count=0, execution_order_valid=True, magic_command_count=1,
|
|
37
|
-
python_metrics=pm,
|
|
38
|
-
)
|
|
39
|
-
assert n.code_cell_count == 2
|
|
40
|
-
assert n.python_metrics is not None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_html_metrics():
|
|
44
|
-
m = HTMLMetrics(
|
|
45
|
-
syntax_valid=True, parse_error_count=0, validator="local", w3c_errors=[],
|
|
46
|
-
has_doctype=True, semantic_elements_used=["header", "main"],
|
|
47
|
-
semantic_element_count=2, div_count=0, span_count=0,
|
|
48
|
-
div_to_semantic_ratio=None, inline_script_count=0,
|
|
49
|
-
inline_style_count=0, inline_event_handler_count=0, comment_count=0,
|
|
50
|
-
external_scripts=[], external_stylesheets=[], cdn_count=0,
|
|
51
|
-
frameworks_detected=[], img_alt_coverage=1.0, form_label_coverage=1.0,
|
|
52
|
-
has_lang_attr=True, has_title=True, heading_hierarchy_valid=True,
|
|
53
|
-
aria_attribute_count=0, ambiguous_link_count=0,
|
|
54
|
-
)
|
|
55
|
-
assert m.validator == "local"
|
|
56
|
-
assert m.div_to_semantic_ratio is None
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_css_metrics():
|
|
60
|
-
m = CSSMetrics(
|
|
61
|
-
syntax_valid=True, parse_error_count=0, validator="local",
|
|
62
|
-
w3c_errors=[], w3c_warnings=[], rule_count=3, selector_count=3,
|
|
63
|
-
important_count=0, duplicate_selector_count=0, media_query_count=1,
|
|
64
|
-
custom_property_count=1, comment_count=0, float_count=0,
|
|
65
|
-
flexbox_count=1, grid_count=1, dominant_layout="mixed",
|
|
66
|
-
float_used_for_layout=False,
|
|
67
|
-
)
|
|
68
|
-
assert m.dominant_layout == "mixed"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_js_metrics():
|
|
72
|
-
m = JSMetrics(
|
|
73
|
-
syntax_valid=True, parse_error_count=0, function_count=1,
|
|
74
|
-
arrow_function_count=1, async_function_count=1, console_log_count=1,
|
|
75
|
-
import_count=1, comment_coverage=1.0, todo_count=0,
|
|
76
|
-
)
|
|
77
|
-
assert m.function_count == 1
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_ts_metrics_extends_js():
|
|
81
|
-
m = TSMetrics(
|
|
82
|
-
syntax_valid=True, parse_error_count=0, function_count=2,
|
|
83
|
-
arrow_function_count=1, async_function_count=0, console_log_count=0,
|
|
84
|
-
import_count=1, comment_coverage=0.5, todo_count=0,
|
|
85
|
-
type_annotation_coverage=0.8, interface_count=1, type_alias_count=1,
|
|
86
|
-
)
|
|
87
|
-
assert m.interface_count == 1
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def test_sql_metrics():
|
|
91
|
-
m = SQLMetrics(
|
|
92
|
-
statement_count=3, query_types={"SELECT": 2, "INSERT": 1},
|
|
93
|
-
join_count=1, subquery_depth=0, unsafe_patterns=[],
|
|
94
|
-
)
|
|
95
|
-
assert m.query_types["SELECT"] == 2
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_code_analysis_structure():
|
|
99
|
-
from code_analyser.models import PythonMetrics, FileAnalysis, CrossFileSignals, CodeAnalysis
|
|
100
|
-
pm = PythonMetrics(
|
|
101
|
-
syntax_valid=True, lint_error_count=0, lint_warning_count=0,
|
|
102
|
-
lint_violations=[], cyclomatic_complexity=1.0, max_nesting_depth=0,
|
|
103
|
-
loc=10, comment_lines=2, blank_lines=1, function_count=1,
|
|
104
|
-
class_count=0, docstring_coverage=1.0, naming_convention="snake_case",
|
|
105
|
-
imports=[], todo_count=0, print_count=0, type_annotation_coverage=1.0,
|
|
106
|
-
has_main_guard=False, bare_except_count=0, comprehension_count=0,
|
|
107
|
-
)
|
|
108
|
-
fa = FileAnalysis(filename="app.py", language="python", metrics=pm, llm_signals=None)
|
|
109
|
-
cf = CrossFileSignals(
|
|
110
|
-
file_count=1, languages_detected=["python"], import_graph={},
|
|
111
|
-
unrecognised_files=[], has_package_json=False, frameworks_detected=[],
|
|
112
|
-
)
|
|
113
|
-
ca = CodeAnalysis(input="app.py", file_count=1, languages_detected=["python"],
|
|
114
|
-
files=[fa], cross_file=cf, llm_signals=None)
|
|
115
|
-
assert ca.file_count == 1
|
|
116
|
-
assert ca.files[0].language == "python"
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from conftest import VALID_SQL, UNSAFE_SQL
|
|
2
|
-
from code_analyser.core.sql_ import analyse_sql
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_statement_count():
|
|
6
|
-
m = analyse_sql(VALID_SQL)
|
|
7
|
-
assert m.statement_count == 5
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_query_types():
|
|
11
|
-
m = analyse_sql(VALID_SQL)
|
|
12
|
-
assert m.query_types.get("SELECT", 0) >= 1
|
|
13
|
-
assert m.query_types.get("INSERT", 0) == 1
|
|
14
|
-
assert m.query_types.get("UPDATE", 0) == 1
|
|
15
|
-
assert m.query_types.get("DELETE", 0) == 1
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_join_count():
|
|
19
|
-
m = analyse_sql(VALID_SQL)
|
|
20
|
-
assert m.join_count == 1
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_no_unsafe_patterns():
|
|
24
|
-
m = analyse_sql(VALID_SQL)
|
|
25
|
-
assert m.unsafe_patterns == []
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_unsafe_patterns():
|
|
29
|
-
m = analyse_sql(UNSAFE_SQL)
|
|
30
|
-
assert len(m.unsafe_patterns) > 0
|
|
31
|
-
patterns_str = " ".join(m.unsafe_patterns)
|
|
32
|
-
assert "UPDATE" in patterns_str or "DELETE" in patterns_str or "SELECT *" in patterns_str
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_subquery_depth():
|
|
36
|
-
sql = "SELECT * FROM (SELECT id FROM (SELECT id FROM users) t1) t2;"
|
|
37
|
-
m = analyse_sql(sql)
|
|
38
|
-
assert m.subquery_depth >= 2
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from conftest import VALID_TS
|
|
2
|
-
from code_analyser.core.typescript_ import analyse_typescript
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_function_count():
|
|
6
|
-
m = analyse_typescript(VALID_TS)
|
|
7
|
-
assert m.function_count >= 1 # greet
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_interface_count():
|
|
11
|
-
m = analyse_typescript(VALID_TS)
|
|
12
|
-
assert m.interface_count == 1 # User
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_type_alias_count():
|
|
16
|
-
m = analyse_typescript(VALID_TS)
|
|
17
|
-
assert m.type_alias_count == 1 # ID
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_type_annotation_coverage_positive():
|
|
21
|
-
m = analyse_typescript(VALID_TS)
|
|
22
|
-
assert m.type_annotation_coverage > 0.0
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_import_count():
|
|
26
|
-
m = analyse_typescript(VALID_TS)
|
|
27
|
-
assert m.import_count >= 1
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_arrow_function_count():
|
|
31
|
-
m = analyse_typescript(VALID_TS)
|
|
32
|
-
assert m.arrow_function_count >= 1 # add
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_syntax_valid_flag():
|
|
36
|
-
m = analyse_typescript(VALID_TS)
|
|
37
|
-
assert isinstance(m.syntax_valid, bool)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|