Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- Parentheses and quote characters inside string literals no longer trigger false unbalanced-linter warnings.
- Single-quote balance is now checked alongside double-quote balance (previously only double quotes were validated).
- Augmented assignments no longer produce invalid-variable-name linter errors.

### Added
Expand Down
16 changes: 12 additions & 4 deletions maithili_dsl/transpiler/linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import re

from .transpile import _tokenize_preserving_strings

LINT_CONFIG = {
"enforce_snake_case": False,
"max_line_length": 80,
Expand Down Expand Up @@ -44,12 +46,18 @@ def lint_maithili_code(code, config=LINT_CONFIG):
if "=" in stripped and stripped.startswith("="):
errors.append(f"पंक्ति {i}: '=' चिह्नक पहिले कोनो मान अपेक्षित अछि")

# Check for unbalanced parentheses or quotes
if stripped.count("(") != stripped.count(")"):
# Check for unbalanced parentheses or quotes.
# Tokenize the line and count only in code regions so that
# parens and quotes inside string literals are not counted.
tokens = _tokenize_preserving_strings(line)
code_parts = [text for is_str, text in tokens if not is_str]
code_line = "".join(code_parts)
if code_line.count("(") != code_line.count(")"):
errors.append(f"पंक्ति {i}: गोल ब्रैकेट असंतुलित अछि")
if stripped.count("\"") % 2 != 0:
if code_line.count("\"") % 2 != 0:
errors.append(f"पंक्ति {i}: उद्धरण चिह्न जोड़ा में नहि अछि")
if code_line.count("'") % 2 != 0:
errors.append(f"पंक्ति {i}: उद्धरण चिह्न जोड़ा में नहि अछि")

# Check for indentation (must start with 4-space or tab if inside block)
if line and not line.startswith(" ") and not stripped.endswith(":") and i > 1:
prev = lines[i-2].strip()
Expand Down
54 changes: 54 additions & 0 deletions tests/test_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,57 @@ class CustomError(Exception):

out = translate_exception_to_maithili(CustomError("demo"))
assert "CustomError" in out

# ---------------------------------------------------------------------------
# Regression: parens and quotes inside string literals (issue #30)
# ---------------------------------------------------------------------------


def test_parens_inside_double_quoted_string_not_flagged():
code = 'छपाउ("(क")\n'
errors = lint_maithili_code(code)
assert not any(
"गोल ब्रैकेट" in e
for e in errors
), f"parens inside double-quoted string falsely flagged: {errors}"


def test_parens_inside_single_quoted_string_not_flagged():
code = "छपाउ('(क')\n"
errors = lint_maithili_code(code)
assert not any(
"गोल ब्रैकेट" in e
for e in errors
), f"parens inside single-quoted string falsely flagged: {errors}"


def test_quote_inside_string_not_flagged():
code = 'छपाउ("it\'s ok")\n'
assert "it's ok" in code
errors = lint_maithili_code(code)
assert not any("उद्धरण" in e for e in errors), (
f"apostrophe in double-quoted string falsely flagged: {errors}"
)


def test_unbalanced_single_quote_flagged():
code = "छपाउ('hi)\n"
errors = lint_maithili_code(code)
assert any("उद्धरण" in e for e in errors), (
f"unbalanced single quote not flagged: {errors}"
)


def test_genuinely_unbalanced_parens_still_flagged():
errors = lint_maithili_code("छपाउ(x\n")
assert any(
"गोल ब्रैकेट" in e
for e in errors
), f"genuinely unbalanced parens not flagged: {errors}"


def test_genuinely_unbalanced_quotes_still_flagged():
errors = lint_maithili_code('छपाउ("hi)\n')
assert any("उद्धरण" in e for e in errors), (
f"genuinely unbalanced quotes not flagged: {errors}"
)
40 changes: 40 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,43 @@ def test_dangerous_module_not_in_whitelist(dangerous):
assert dangerous not in _ALLOWED_MODULES, (
f"{dangerous} must never be in the import whitelist"
)

# ---------------------------------------------------------------------------
# Security regression: linter tokenizer does not widen the trust boundary
# ---------------------------------------------------------------------------


@pytest.mark.security
def test_linter_tokenizer_does_not_expand_trust_boundary():
"""Code-like content inside string literals remains data.

The linter now uses _tokenize_preserving_strings for
paren/quote balance. This test verifies that using the
tokenizer in the lint path does not accidentally weaken
any runtime security boundaries — the transpiler, import
validator, and sandbox execution path are unchanged.
"""
from maithili_dsl.transpiler.linter import lint_maithili_code

code_inside_string = '\u091b\u092a\u093e\u0909("import os; exec()")\n'
errors = lint_maithili_code(code_inside_string)
assert not any(
"\u0917\u094b\u0932 \u092c\u094d\u0930\u0948\u0915\u0947\u091f" in e
for e in errors
)


@pytest.mark.security
def test_actual_dangerous_code_outside_strings_still_blocked():
"""Raw Python import outside strings is still caught by the import validator.

The linter change uses the tokenizer for paren/quote balance checks.
This test verifies that the security model is unchanged: dangerous
code outside string literals remains blocked by _validate_imports.
"""
from maithili_dsl.transpiler.transpile import transpile_maithili_code

code = '\u091b\u092a\u093e\u0909("safe")\nimport os\n'
transpiled = transpile_maithili_code(code)
errors = _validate_imports(transpiled, translated_modules=set())
assert errors, f"raw 'import os' must be blocked by import validator: {errors}"