diff --git a/src/frameforge/core.py b/src/frameforge/core.py index 1dda06c..a334652 100644 --- a/src/frameforge/core.py +++ b/src/frameforge/core.py @@ -6,7 +6,7 @@ from pathlib import Path from urllib.parse import urlsplit from importlib.resources import files -from .semantic import evaluate, validate_rules +from .semantic import evaluate, mask_comments, validate_rules from .plugin_checks import run as run_plugin_checks, validate_profile_checks from .remediation import advise as advise_warnings from .custom_standards import summarize as summarize_custom_standards @@ -125,9 +125,11 @@ def professional_preview(root, manifest): content = safe.read_text(encoding='utf-8') except (OSError, UnicodeError): continue - pattern = (r'//[^\n]*|/\*[\s\S]*?\*/' if path.suffix in ('.cs','.asmdef') - else r'--\[\[[\s\S]*?\]\]|--[^\n]*') - content = re.sub(pattern, lambda m: re.sub(r'[^\n]', ' ', m.group()), content) + if path.suffix == '.cs': + content = mask_comments(content, 'csharp') + elif path.suffix == '.lua': + content = mask_comments(content, 'lua') + # .asmdef is JSON: do not apply C# comment rules to JSON string values. evidence.append((rel, content)) if len(evidence) >= 150: break diff --git a/src/frameforge/semantic.py b/src/frameforge/semantic.py index e6153cd..f7561c4 100644 --- a/src/frameforge/semantic.py +++ b/src/frameforge/semantic.py @@ -3,6 +3,28 @@ from pathlib import Path +def mask_comments(source, language): + """Mask real C# or Lua comments while preserving quoted string contents and line numbers.""" + if language == "csharp": + pattern = re.compile( + r'(?P(?P"{3,})[\s\S]*?(?P=raw_delimiter)|@"(?:""|[^"])*"|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\')' + r'|(?P//[^\n]*|/\*[\s\S]*?\*/)' + ) + elif language == "lua": + pattern = re.compile( + r'(?P\[(?P=*)\[[\s\S]*?\](?P=eq)\])' + r'|(?P"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\')' + r'|(?P--\[(?P=*)\[[\s\S]*?\](?P=comment_eq)\]|--[^\n]*)' + ) + else: + raise ValueError("unsupported comment masking language") + return pattern.sub( + lambda match: re.sub(r'[^\n]', ' ', match.group()) + if match.group("comment") else match.group(), + source, + ) + + def evaluate(root, rule): """Observe declared literals across bounded source paths; never claim control-flow compliance.""" root = Path(root).resolve() @@ -16,23 +38,7 @@ def evaluate(root, rule): except (UnicodeError, OSError): continue # Comment filtering is intentionally conservative; not a language parser. - language = rule["language"] - if language == "csharp": - pattern = re.compile( - r'(?P(?P"{3,})[\s\S]*?(?P=raw_delimiter)|@"(?:""|[^"])*"|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\')' - r'|(?P//[^\n]*|/\*[\s\S]*?\*/)' - ) - else: - pattern = re.compile( - r'(?P\[(?P=*)\[[\s\S]*?\](?P=eq)\])' - r'|(?P"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\')' - r'|(?P--\[(?P=*)\[[\s\S]*?\](?P=comment_eq)\]|--[^\n]*)' - ) - source = pattern.sub( - lambda match: re.sub(r'[^\n]', ' ', match.group()) - if match.group("comment") else match.group(), - source, - ) + source = mask_comments(source, rule["language"]) sources.append((relative, source)) if not sources: diff --git a/tests/test_professional_guidance.py b/tests/test_professional_guidance.py index 0e20677..d09e542 100644 --- a/tests/test_professional_guidance.py +++ b/tests/test_professional_guidance.py @@ -48,6 +48,35 @@ def test_unmatched_signal_is_not_a_gap(self): r=inspect(root)['professional_guidance'] self.assertTrue(all(item['status']=='not_evaluated' for item in r)) + def test_csharp_url_string_does_not_hide_professional_guidance_signal(self): + with tempfile.TemporaryDirectory() as d: + root=Path(d) + (root/'ProjectSettings').mkdir() + (root/'ProjectSettings/ProjectVersion.txt').write_text('Unity') + source=root/'Assets/Controller.cs' + source.parent.mkdir() + source.write_text('var url = "https://example.com"; MonoBehaviour; // MonoBehaviour\n') + (root/'.frameforge').mkdir() + (root/'.frameforge/profile.json').write_text(json.dumps( + {'schema_version':1,'selected_plugins':[{'id':'unity','version':'1.0.0'}]})) + result=inspect(root)['professional_guidance'] + self.assertEqual(result[0]['status'],'review_recommended') + self.assertEqual(result[0]['evidence'][0]['path'],'Assets/Controller.cs') + + def test_lua_comment_marker_in_string_does_not_hide_skynet_guidance_signal(self): + with tempfile.TemporaryDirectory() as d: + root=Path(d) + (root/'Dockerfile').write_text('FROM cloudwu/skynet') + source=root/'backend/actor.lua' + source.parent.mkdir() + source.write_text('local marker = "-- text"; skynet.start(function() end) -- skynet.start(false)\n') + (root/'.frameforge').mkdir() + (root/'.frameforge/profile.json').write_text(json.dumps( + {'schema_version':1,'selected_plugins':[{'id':'skynet','version':'1.0.0'}]})) + result=inspect(root)['professional_guidance'] + self.assertEqual(result[0]['status'],'review_recommended') + self.assertEqual(result[0]['evidence'][0]['path'],'backend/actor.lua') + def test_comments_not_treated_as_signals(self): with tempfile.TemporaryDirectory() as d: root=Path(d)