code-analyser 1.0.2__tar.gz → 1.1.0__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.
Files changed (55) hide show
  1. {code_analyser-1.0.2 → code_analyser-1.1.0}/PKG-INFO +1 -1
  2. {code_analyser-1.0.2 → code_analyser-1.1.0}/pyproject.toml +5 -2
  3. code_analyser-1.1.0/src/code_analyser/__init__.py +10 -0
  4. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/api.py +12 -2
  5. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/cli.py +6 -0
  6. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/javascript_.py +7 -0
  7. code_analyser-1.1.0/src/code_analyser/core/sql_.py +91 -0
  8. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/typescript_.py +19 -0
  9. code_analyser-1.1.0/src/code_analyser/manifest.py +22 -0
  10. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/models.py +7 -0
  11. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/api/test_api.py +3 -0
  12. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/conftest.py +12 -0
  13. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/integration/test_full_pipeline.py +1 -0
  14. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/integration/test_pipeline.py +17 -8
  15. code_analyser-1.1.0/tests/test_invariants.py +57 -0
  16. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_css_.py +0 -6
  17. code_analyser-1.1.0/tests/unit/test_javascript_.py +59 -0
  18. code_analyser-1.1.0/tests/unit/test_models.py +144 -0
  19. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_python_.py +12 -1
  20. code_analyser-1.1.0/tests/unit/test_sql_.py +73 -0
  21. code_analyser-1.1.0/tests/unit/test_typescript_.py +59 -0
  22. {code_analyser-1.0.2 → code_analyser-1.1.0}/uv.lock +1 -1
  23. code_analyser-1.0.2/src/code_analyser/__init__.py +0 -4
  24. code_analyser-1.0.2/src/code_analyser/core/sql_.py +0 -55
  25. code_analyser-1.0.2/tests/unit/test_javascript_.py +0 -46
  26. code_analyser-1.0.2/tests/unit/test_models.py +0 -116
  27. code_analyser-1.0.2/tests/unit/test_sql_.py +0 -38
  28. code_analyser-1.0.2/tests/unit/test_typescript_.py +0 -37
  29. {code_analyser-1.0.2 → code_analyser-1.1.0}/.dockerignore +0 -0
  30. {code_analyser-1.0.2 → code_analyser-1.1.0}/.env.example +0 -0
  31. {code_analyser-1.0.2 → code_analyser-1.1.0}/.gitignore +0 -0
  32. {code_analyser-1.0.2 → code_analyser-1.1.0}/LICENSE +0 -0
  33. {code_analyser-1.0.2 → code_analyser-1.1.0}/README.md +0 -0
  34. {code_analyser-1.0.2 → code_analyser-1.1.0}/docs/superpowers/plans/2026-05-06-code-analyser-rewrite.md +0 -0
  35. {code_analyser-1.0.2 → code_analyser-1.1.0}/docs/superpowers/specs/2026-05-05-code-analyser-design.md +0 -0
  36. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/__init__.py +0 -0
  37. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/css_.py +0 -0
  38. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/html_.py +0 -0
  39. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/notebook_.py +0 -0
  40. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/core/python_.py +0 -0
  41. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/detect.py +0 -0
  42. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/llm.py +0 -0
  43. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/pipeline.py +0 -0
  44. {code_analyser-1.0.2 → code_analyser-1.1.0}/src/code_analyser/settings.py +0 -0
  45. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/__init__.py +0 -0
  46. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/api/__init__.py +0 -0
  47. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/cli/__init__.py +0 -0
  48. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/cli/test_cli.py +0 -0
  49. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/integration/__init__.py +0 -0
  50. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/__init__.py +0 -0
  51. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_detect.py +0 -0
  52. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_html_.py +0 -0
  53. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_llm.py +0 -0
  54. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_notebook_.py +0 -0
  55. {code_analyser-1.0.2 → code_analyser-1.1.0}/tests/unit/test_scaffold.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analyser
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: Source code analyser — part of the analyser family
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-analyser"
7
- version = "1.0.2"
7
+ version = "1.1.0"
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 = ["--cov=src/code_analyser", "--cov-report=term-missing", "--strict-markers"]
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"
@@ -0,0 +1,10 @@
1
+ from importlib.metadata import version as _v
2
+
3
+ from .manifest import MANIFEST
4
+ from .models import CodeAnalysis
5
+ from .pipeline import CodeAnalyser
6
+
7
+ __version__ = _v("code-analyser")
8
+ del _v
9
+
10
+ __all__ = ["CodeAnalyser", "CodeAnalysis", "MANIFEST"]
@@ -6,19 +6,29 @@ from pathlib import Path
6
6
  from importlib.metadata import version
7
7
  from fastapi import FastAPI, File, HTTPException, UploadFile
8
8
 
9
+ from .manifest import MANIFEST
9
10
  from .models import CodeAnalysis
10
11
  from .pipeline import CodeAnalyser
11
12
 
12
13
  _start_time = time.time()
13
14
 
14
- app = FastAPI(title="code-analyser", version="1.0.0")
15
+ app = FastAPI(title="code-analyser", version=version("code-analyser"))
15
16
 
16
17
  _analyser = CodeAnalyser()
17
18
 
18
19
 
19
20
  @app.get("/health")
20
21
  def health() -> dict:
21
- return {"status": "ok", "uptime": round(time.time() - _start_time, 1)}
22
+ return {
23
+ "status": "ok",
24
+ "uptime": round(time.time() - _start_time, 1),
25
+ "version": version("code-analyser"),
26
+ }
27
+
28
+
29
+ @app.get("/manifest")
30
+ def manifest() -> dict:
31
+ return MANIFEST
22
32
 
23
33
 
24
34
  @app.post("/analyse", response_model=CodeAnalysis)
@@ -12,6 +12,12 @@ def main() -> None:
12
12
  _serve(sys.argv[2:])
13
13
  return
14
14
 
15
+ if len(sys.argv) > 1 and sys.argv[1] == "manifest":
16
+ import json
17
+ from .manifest import MANIFEST
18
+ print(json.dumps(MANIFEST, indent=2))
19
+ return
20
+
15
21
  parser = argparse.ArgumentParser(
16
22
  prog="code-analyser",
17
23
  description="Source code signals analyser",
@@ -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))
@@ -0,0 +1,22 @@
1
+ """Capability manifest for the lens family (consumed by auto-analyser)."""
2
+ from __future__ import annotations
3
+
4
+ from importlib.metadata import PackageNotFoundError, version
5
+
6
+
7
+ def _version() -> str:
8
+ try:
9
+ return version("code-analyser")
10
+ except PackageNotFoundError:
11
+ return "0.0.0"
12
+
13
+
14
+ MANIFEST: dict = {
15
+ "name": "code-analyser",
16
+ "version": _version(),
17
+ "role": "analyser",
18
+ "accepts": ["code"],
19
+ "extensions": [".py", ".js", ".ts", ".tsx", ".jsx", ".html", ".css", ".scss", ".sql", ".ipynb"],
20
+ "auto_routable": True,
21
+ "produces": "CodeAnalysis",
22
+ }
@@ -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
 
@@ -14,6 +14,7 @@ def no_network(monkeypatch):
14
14
  monkeypatch.setattr("httpx.post", _raise)
15
15
 
16
16
 
17
+ @pytest.mark.slow
17
18
  def test_full_zip_integration(tmp_path):
18
19
  buf = io.BytesIO()
19
20
  with zipfile.ZipFile(buf, "w") as zf:
@@ -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
- # ruff may or may not be installed; if it is, violations have code/line/message
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
@@ -68,7 +68,7 @@ wheels = [
68
68
 
69
69
  [[package]]
70
70
  name = "code-analyser"
71
- version = "1.0.2"
71
+ version = "1.0.3"
72
72
  source = { editable = "." }
73
73
  dependencies = [
74
74
  { name = "esprima" },
@@ -1,4 +0,0 @@
1
- from .models import CodeAnalysis
2
- from .pipeline import CodeAnalyser
3
-
4
- __all__ = ["CodeAnalyser", "CodeAnalysis"]
@@ -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