quantum-framework 0.18.0__py3-none-any.whl
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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum CLI - Lint Command
|
|
3
|
+
|
|
4
|
+
Lint and validate .q files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from quantum.cli.utils import get_console, find_project_root, find_q_files
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Severity(Enum):
|
|
20
|
+
"""Lint issue severity levels."""
|
|
21
|
+
ERROR = 'error'
|
|
22
|
+
WARNING = 'warning'
|
|
23
|
+
INFO = 'info'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LintIssue:
|
|
28
|
+
"""A linting issue found in a file."""
|
|
29
|
+
file: Path
|
|
30
|
+
line: int
|
|
31
|
+
column: int
|
|
32
|
+
severity: Severity
|
|
33
|
+
code: str
|
|
34
|
+
message: str
|
|
35
|
+
suggestion: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class QuantumLinter:
|
|
39
|
+
"""Linter for Quantum .q files."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, debug: bool = False):
|
|
42
|
+
self.debug = debug
|
|
43
|
+
self.issues: List[LintIssue] = []
|
|
44
|
+
|
|
45
|
+
def lint_file(self, file_path: Path) -> List[LintIssue]:
|
|
46
|
+
"""Lint a single .q file."""
|
|
47
|
+
issues = []
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
content = file_path.read_text(encoding='utf-8')
|
|
51
|
+
lines = content.splitlines()
|
|
52
|
+
|
|
53
|
+
# Run lint checks
|
|
54
|
+
issues.extend(self._check_xml_structure(file_path, content, lines))
|
|
55
|
+
issues.extend(self._check_quantum_conventions(file_path, content, lines))
|
|
56
|
+
issues.extend(self._check_databinding(file_path, content, lines))
|
|
57
|
+
issues.extend(self._check_component_structure(file_path, content, lines))
|
|
58
|
+
issues.extend(self._check_accessibility(file_path, content, lines))
|
|
59
|
+
|
|
60
|
+
# Try parsing with Quantum parser
|
|
61
|
+
issues.extend(self._check_parse_errors(file_path, content))
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
issues.append(LintIssue(
|
|
65
|
+
file=file_path,
|
|
66
|
+
line=0,
|
|
67
|
+
column=0,
|
|
68
|
+
severity=Severity.ERROR,
|
|
69
|
+
code='E001',
|
|
70
|
+
message=f"Failed to read file: {e}"
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
return issues
|
|
74
|
+
|
|
75
|
+
def lint_directory(self, directory: Path, recursive: bool = True) -> List[LintIssue]:
|
|
76
|
+
"""Lint all .q files in a directory."""
|
|
77
|
+
q_files = find_q_files(directory, recursive=recursive)
|
|
78
|
+
all_issues = []
|
|
79
|
+
|
|
80
|
+
for q_file in q_files:
|
|
81
|
+
issues = self.lint_file(q_file)
|
|
82
|
+
all_issues.extend(issues)
|
|
83
|
+
|
|
84
|
+
return all_issues
|
|
85
|
+
|
|
86
|
+
def _check_xml_structure(self, file_path: Path, content: str, lines: List[str]) -> List[LintIssue]:
|
|
87
|
+
"""Check XML structure and syntax."""
|
|
88
|
+
issues = []
|
|
89
|
+
import re
|
|
90
|
+
|
|
91
|
+
# Check for unclosed tags
|
|
92
|
+
tag_pattern = re.compile(r'<(\/?)(q:)?(\w+)([^>]*?)(/?)>')
|
|
93
|
+
open_tags = []
|
|
94
|
+
|
|
95
|
+
for i, line in enumerate(lines, 1):
|
|
96
|
+
for match in tag_pattern.finditer(line):
|
|
97
|
+
is_closing = match.group(1) == '/'
|
|
98
|
+
is_self_closing = match.group(5) == '/'
|
|
99
|
+
tag_name = match.group(3)
|
|
100
|
+
|
|
101
|
+
if is_closing:
|
|
102
|
+
if open_tags and open_tags[-1] == tag_name:
|
|
103
|
+
open_tags.pop()
|
|
104
|
+
else:
|
|
105
|
+
issues.append(LintIssue(
|
|
106
|
+
file=file_path,
|
|
107
|
+
line=i,
|
|
108
|
+
column=match.start(),
|
|
109
|
+
severity=Severity.ERROR,
|
|
110
|
+
code='E010',
|
|
111
|
+
message=f"Unexpected closing tag </{tag_name}>"
|
|
112
|
+
))
|
|
113
|
+
elif not is_self_closing:
|
|
114
|
+
# Skip void elements
|
|
115
|
+
void_elements = ['br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'param', 'source', 'track', 'wbr']
|
|
116
|
+
if tag_name.lower() not in void_elements:
|
|
117
|
+
open_tags.append(tag_name)
|
|
118
|
+
|
|
119
|
+
# Check for missing DOCTYPE/xml declaration (warning only)
|
|
120
|
+
if lines and not lines[0].strip().startswith('<?xml') and not lines[0].strip().startswith('<!DOCTYPE') and not lines[0].strip().startswith('<!--'):
|
|
121
|
+
# Not an error, just info
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
return issues
|
|
125
|
+
|
|
126
|
+
def _check_quantum_conventions(self, file_path: Path, content: str, lines: List[str]) -> List[LintIssue]:
|
|
127
|
+
"""Check Quantum-specific conventions."""
|
|
128
|
+
issues = []
|
|
129
|
+
import re
|
|
130
|
+
|
|
131
|
+
# Check for component name matching filename
|
|
132
|
+
component_pattern = re.compile(r'<q:component\s+name="([^"]+)"')
|
|
133
|
+
for i, line in enumerate(lines, 1):
|
|
134
|
+
match = component_pattern.search(line)
|
|
135
|
+
if match:
|
|
136
|
+
component_name = match.group(1)
|
|
137
|
+
expected_name = file_path.stem
|
|
138
|
+
|
|
139
|
+
# Component name should match file name (PascalCase)
|
|
140
|
+
if component_name.lower() != expected_name.lower().replace('-', '').replace('_', ''):
|
|
141
|
+
issues.append(LintIssue(
|
|
142
|
+
file=file_path,
|
|
143
|
+
line=i,
|
|
144
|
+
column=match.start(),
|
|
145
|
+
severity=Severity.WARNING,
|
|
146
|
+
code='W001',
|
|
147
|
+
message=f"Component name '{component_name}' does not match filename '{file_path.name}'",
|
|
148
|
+
suggestion=f"Rename component to '{expected_name}' or rename file"
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
# Check for xmlns declaration
|
|
152
|
+
if 'xmlns:q=' not in content and '<q:' in content:
|
|
153
|
+
issues.append(LintIssue(
|
|
154
|
+
file=file_path,
|
|
155
|
+
line=1,
|
|
156
|
+
column=0,
|
|
157
|
+
severity=Severity.WARNING,
|
|
158
|
+
code='W002',
|
|
159
|
+
message="Missing xmlns:q namespace declaration",
|
|
160
|
+
suggestion='Add xmlns:q="https://quantum.lang/ns" to root element'
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
# Check for deprecated tags
|
|
164
|
+
deprecated_tags = {
|
|
165
|
+
'q:cfif': 'q:if',
|
|
166
|
+
'q:cfset': 'q:set',
|
|
167
|
+
'q:cfloop': 'q:loop',
|
|
168
|
+
'q:cfquery': 'q:query',
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for old_tag, new_tag in deprecated_tags.items():
|
|
172
|
+
for i, line in enumerate(lines, 1):
|
|
173
|
+
if f'<{old_tag}' in line.lower():
|
|
174
|
+
issues.append(LintIssue(
|
|
175
|
+
file=file_path,
|
|
176
|
+
line=i,
|
|
177
|
+
column=line.lower().find(f'<{old_tag}'),
|
|
178
|
+
severity=Severity.WARNING,
|
|
179
|
+
code='W003',
|
|
180
|
+
message=f"Deprecated tag '{old_tag}'",
|
|
181
|
+
suggestion=f"Use '{new_tag}' instead"
|
|
182
|
+
))
|
|
183
|
+
|
|
184
|
+
return issues
|
|
185
|
+
|
|
186
|
+
def _check_databinding(self, file_path: Path, content: str, lines: List[str]) -> List[LintIssue]:
|
|
187
|
+
"""Check databinding syntax."""
|
|
188
|
+
issues = []
|
|
189
|
+
import re
|
|
190
|
+
|
|
191
|
+
# Check for unclosed databinding braces
|
|
192
|
+
for i, line in enumerate(lines, 1):
|
|
193
|
+
# Count { and } not in strings
|
|
194
|
+
open_count = line.count('{')
|
|
195
|
+
close_count = line.count('}')
|
|
196
|
+
|
|
197
|
+
# Simple check - might have false positives for multi-line expressions
|
|
198
|
+
if open_count != close_count:
|
|
199
|
+
# Check if it's a JavaScript object (common false positive)
|
|
200
|
+
if not re.search(r'[=:]\s*\{', line):
|
|
201
|
+
issues.append(LintIssue(
|
|
202
|
+
file=file_path,
|
|
203
|
+
line=i,
|
|
204
|
+
column=0,
|
|
205
|
+
severity=Severity.WARNING,
|
|
206
|
+
code='W010',
|
|
207
|
+
message="Potentially unmatched databinding braces",
|
|
208
|
+
suggestion="Check for balanced { and } in expressions"
|
|
209
|
+
))
|
|
210
|
+
|
|
211
|
+
# Check for double-braces which might be a mistake
|
|
212
|
+
double_brace_pattern = re.compile(r'\{\{([^}]+)\}\}')
|
|
213
|
+
for i, line in enumerate(lines, 1):
|
|
214
|
+
match = double_brace_pattern.search(line)
|
|
215
|
+
if match:
|
|
216
|
+
# Could be intentional (e.g., template syntax) - just info
|
|
217
|
+
issues.append(LintIssue(
|
|
218
|
+
file=file_path,
|
|
219
|
+
line=i,
|
|
220
|
+
column=match.start(),
|
|
221
|
+
severity=Severity.INFO,
|
|
222
|
+
code='I010',
|
|
223
|
+
message="Double braces {{ }} detected - Quantum uses single braces { }",
|
|
224
|
+
suggestion="Use single braces for databinding: {expression}"
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
return issues
|
|
228
|
+
|
|
229
|
+
def _check_component_structure(self, file_path: Path, content: str, lines: List[str]) -> List[LintIssue]:
|
|
230
|
+
"""Check component structure best practices."""
|
|
231
|
+
issues = []
|
|
232
|
+
import re
|
|
233
|
+
|
|
234
|
+
# Check for multiple root components
|
|
235
|
+
component_count = content.count('<q:component')
|
|
236
|
+
if component_count > 1:
|
|
237
|
+
issues.append(LintIssue(
|
|
238
|
+
file=file_path,
|
|
239
|
+
line=1,
|
|
240
|
+
column=0,
|
|
241
|
+
severity=Severity.WARNING,
|
|
242
|
+
code='W020',
|
|
243
|
+
message=f"File contains {component_count} components - consider splitting",
|
|
244
|
+
suggestion="One component per file is recommended"
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
# Check for required params without defaults
|
|
248
|
+
param_pattern = re.compile(r'<q:param\s+name="([^"]+)"[^>]*required="true"[^>]*/>')
|
|
249
|
+
for i, line in enumerate(lines, 1):
|
|
250
|
+
match = param_pattern.search(line)
|
|
251
|
+
if match and 'default=' not in line:
|
|
252
|
+
# This is fine, just informational
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
# Check for unused params (simplified check)
|
|
256
|
+
param_names = []
|
|
257
|
+
param_lines = {}
|
|
258
|
+
for i, line in enumerate(lines, 1):
|
|
259
|
+
match = re.search(r'<q:param\s+name="([^"]+)"', line)
|
|
260
|
+
if match:
|
|
261
|
+
param_names.append(match.group(1))
|
|
262
|
+
param_lines[match.group(1)] = i
|
|
263
|
+
|
|
264
|
+
for param in param_names:
|
|
265
|
+
# Check if param is used in databinding
|
|
266
|
+
usage_pattern = re.compile(r'\{[^}]*\b' + re.escape(param) + r'\b[^}]*\}')
|
|
267
|
+
if not usage_pattern.search(content):
|
|
268
|
+
issues.append(LintIssue(
|
|
269
|
+
file=file_path,
|
|
270
|
+
line=param_lines[param],
|
|
271
|
+
column=0,
|
|
272
|
+
severity=Severity.INFO,
|
|
273
|
+
code='I020',
|
|
274
|
+
message=f"Parameter '{param}' may be unused",
|
|
275
|
+
suggestion="Remove unused parameters or verify usage"
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
return issues
|
|
279
|
+
|
|
280
|
+
def _check_accessibility(self, file_path: Path, content: str, lines: List[str]) -> List[LintIssue]:
|
|
281
|
+
"""Check accessibility best practices."""
|
|
282
|
+
issues = []
|
|
283
|
+
import re
|
|
284
|
+
|
|
285
|
+
# Check for images without alt
|
|
286
|
+
img_pattern = re.compile(r'<img\s+[^>]*>')
|
|
287
|
+
for i, line in enumerate(lines, 1):
|
|
288
|
+
for match in img_pattern.finditer(line):
|
|
289
|
+
img_tag = match.group()
|
|
290
|
+
if 'alt=' not in img_tag:
|
|
291
|
+
issues.append(LintIssue(
|
|
292
|
+
file=file_path,
|
|
293
|
+
line=i,
|
|
294
|
+
column=match.start(),
|
|
295
|
+
severity=Severity.WARNING,
|
|
296
|
+
code='A001',
|
|
297
|
+
message="Image missing alt attribute",
|
|
298
|
+
suggestion='Add alt="description" for accessibility'
|
|
299
|
+
))
|
|
300
|
+
|
|
301
|
+
# Check for buttons without type
|
|
302
|
+
button_pattern = re.compile(r'<button\s+[^>]*>')
|
|
303
|
+
for i, line in enumerate(lines, 1):
|
|
304
|
+
for match in button_pattern.finditer(line):
|
|
305
|
+
button_tag = match.group()
|
|
306
|
+
if 'type=' not in button_tag:
|
|
307
|
+
issues.append(LintIssue(
|
|
308
|
+
file=file_path,
|
|
309
|
+
line=i,
|
|
310
|
+
column=match.start(),
|
|
311
|
+
severity=Severity.INFO,
|
|
312
|
+
code='A002',
|
|
313
|
+
message="Button missing type attribute",
|
|
314
|
+
suggestion='Add type="button" or type="submit"'
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
# Check for form inputs without labels
|
|
318
|
+
input_pattern = re.compile(r'<input\s+[^>]*id="([^"]+)"[^>]*>')
|
|
319
|
+
for i, line in enumerate(lines, 1):
|
|
320
|
+
for match in input_pattern.finditer(line):
|
|
321
|
+
input_id = match.group(1)
|
|
322
|
+
label_pattern = re.compile(rf'<label\s+[^>]*for="{input_id}"')
|
|
323
|
+
if not label_pattern.search(content):
|
|
324
|
+
issues.append(LintIssue(
|
|
325
|
+
file=file_path,
|
|
326
|
+
line=i,
|
|
327
|
+
column=match.start(),
|
|
328
|
+
severity=Severity.INFO,
|
|
329
|
+
code='A003',
|
|
330
|
+
message=f"Input '{input_id}' may be missing a label",
|
|
331
|
+
suggestion=f'Add <label for="{input_id}">Label</label>'
|
|
332
|
+
))
|
|
333
|
+
|
|
334
|
+
return issues
|
|
335
|
+
|
|
336
|
+
def _check_parse_errors(self, file_path: Path, content: str) -> List[LintIssue]:
|
|
337
|
+
"""Check for parser errors."""
|
|
338
|
+
issues = []
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
from quantum.core.parser import QuantumParser, QuantumParseError
|
|
342
|
+
|
|
343
|
+
parser = QuantumParser()
|
|
344
|
+
parser.parse(content)
|
|
345
|
+
|
|
346
|
+
except QuantumParseError as e:
|
|
347
|
+
issues.append(LintIssue(
|
|
348
|
+
file=file_path,
|
|
349
|
+
line=getattr(e, 'line', 0) or 0,
|
|
350
|
+
column=getattr(e, 'column', 0) or 0,
|
|
351
|
+
severity=Severity.ERROR,
|
|
352
|
+
code='E100',
|
|
353
|
+
message=f"Parse error: {e}"
|
|
354
|
+
))
|
|
355
|
+
except Exception as e:
|
|
356
|
+
# Other parsing errors
|
|
357
|
+
issues.append(LintIssue(
|
|
358
|
+
file=file_path,
|
|
359
|
+
line=0,
|
|
360
|
+
column=0,
|
|
361
|
+
severity=Severity.ERROR,
|
|
362
|
+
code='E101',
|
|
363
|
+
message=f"Failed to parse: {e}"
|
|
364
|
+
))
|
|
365
|
+
|
|
366
|
+
return issues
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@click.command('lint')
|
|
370
|
+
@click.argument('path', required=False, type=click.Path())
|
|
371
|
+
@click.option('--fix', is_flag=True, help='Attempt to fix issues automatically')
|
|
372
|
+
@click.option('--format', '-f', type=click.Choice(['text', 'json', 'github']), default='text',
|
|
373
|
+
help='Output format')
|
|
374
|
+
@click.option('--severity', '-s', type=click.Choice(['error', 'warning', 'info']), default='warning',
|
|
375
|
+
help='Minimum severity to report')
|
|
376
|
+
@click.option('--ignore', '-i', type=str, multiple=True, help='Issue codes to ignore')
|
|
377
|
+
@click.option('--quiet', '-q', is_flag=True, help='Quiet mode - only show errors')
|
|
378
|
+
def lint(
|
|
379
|
+
path: Optional[str],
|
|
380
|
+
fix: bool,
|
|
381
|
+
format: str,
|
|
382
|
+
severity: str,
|
|
383
|
+
ignore: tuple,
|
|
384
|
+
quiet: bool
|
|
385
|
+
):
|
|
386
|
+
"""Lint and validate .q files.
|
|
387
|
+
|
|
388
|
+
Checks for syntax errors, best practices, and accessibility issues.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
|
|
392
|
+
quantum lint
|
|
393
|
+
|
|
394
|
+
quantum lint ./components
|
|
395
|
+
|
|
396
|
+
quantum lint --format json
|
|
397
|
+
|
|
398
|
+
quantum lint --severity error
|
|
399
|
+
|
|
400
|
+
quantum lint --ignore W001 --ignore W002
|
|
401
|
+
"""
|
|
402
|
+
console = get_console(quiet=quiet)
|
|
403
|
+
|
|
404
|
+
# Find project root
|
|
405
|
+
project_root = find_project_root()
|
|
406
|
+
if not project_root:
|
|
407
|
+
project_root = Path.cwd()
|
|
408
|
+
|
|
409
|
+
# Determine lint path
|
|
410
|
+
if path:
|
|
411
|
+
lint_path = Path(path)
|
|
412
|
+
if not lint_path.is_absolute():
|
|
413
|
+
lint_path = project_root / path
|
|
414
|
+
else:
|
|
415
|
+
lint_path = project_root
|
|
416
|
+
|
|
417
|
+
if not lint_path.exists():
|
|
418
|
+
console.error(f"Path not found: {lint_path}")
|
|
419
|
+
raise click.Abort()
|
|
420
|
+
|
|
421
|
+
console.header(
|
|
422
|
+
"Quantum Linter",
|
|
423
|
+
f"Checking: {lint_path}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Run linter
|
|
427
|
+
linter = QuantumLinter(debug=False)
|
|
428
|
+
|
|
429
|
+
if lint_path.is_file():
|
|
430
|
+
issues = linter.lint_file(lint_path)
|
|
431
|
+
else:
|
|
432
|
+
issues = linter.lint_directory(lint_path)
|
|
433
|
+
|
|
434
|
+
# Filter by severity
|
|
435
|
+
severity_order = {'error': 0, 'warning': 1, 'info': 2}
|
|
436
|
+
min_severity = severity_order[severity]
|
|
437
|
+
issues = [i for i in issues if severity_order[i.severity.value] <= min_severity]
|
|
438
|
+
|
|
439
|
+
# Filter by ignore codes
|
|
440
|
+
if ignore:
|
|
441
|
+
issues = [i for i in issues if i.code not in ignore]
|
|
442
|
+
|
|
443
|
+
# Sort issues
|
|
444
|
+
issues.sort(key=lambda i: (str(i.file), i.line, severity_order[i.severity.value]))
|
|
445
|
+
|
|
446
|
+
# Output results
|
|
447
|
+
if format == 'json':
|
|
448
|
+
_output_json(issues)
|
|
449
|
+
elif format == 'github':
|
|
450
|
+
_output_github(issues)
|
|
451
|
+
else:
|
|
452
|
+
_output_text(issues, console, project_root)
|
|
453
|
+
|
|
454
|
+
# Summary
|
|
455
|
+
error_count = sum(1 for i in issues if i.severity == Severity.ERROR)
|
|
456
|
+
warning_count = sum(1 for i in issues if i.severity == Severity.WARNING)
|
|
457
|
+
info_count = sum(1 for i in issues if i.severity == Severity.INFO)
|
|
458
|
+
|
|
459
|
+
console.print()
|
|
460
|
+
if error_count > 0:
|
|
461
|
+
console.error(f"Found {error_count} error(s), {warning_count} warning(s), {info_count} info(s)")
|
|
462
|
+
sys.exit(1)
|
|
463
|
+
elif warning_count > 0:
|
|
464
|
+
console.warning(f"Found {warning_count} warning(s), {info_count} info(s)")
|
|
465
|
+
else:
|
|
466
|
+
console.success(f"No issues found! ({info_count} info messages)")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _output_text(issues: List[LintIssue], console, project_root: Path):
|
|
470
|
+
"""Output issues in text format."""
|
|
471
|
+
current_file = None
|
|
472
|
+
|
|
473
|
+
for issue in issues:
|
|
474
|
+
if issue.file != current_file:
|
|
475
|
+
current_file = issue.file
|
|
476
|
+
rel_path = issue.file.relative_to(project_root) if issue.file.is_relative_to(project_root) else issue.file
|
|
477
|
+
console.print(f"\n[bold]{rel_path}[/bold]")
|
|
478
|
+
|
|
479
|
+
severity_colors = {
|
|
480
|
+
Severity.ERROR: 'red',
|
|
481
|
+
Severity.WARNING: 'yellow',
|
|
482
|
+
Severity.INFO: 'cyan',
|
|
483
|
+
}
|
|
484
|
+
color = severity_colors[issue.severity]
|
|
485
|
+
|
|
486
|
+
location = f"{issue.line}:{issue.column}" if issue.column else str(issue.line)
|
|
487
|
+
console.print(f" [{color}]{issue.severity.value.upper()}[/{color}] [{issue.code}] {location}: {issue.message}")
|
|
488
|
+
|
|
489
|
+
if issue.suggestion:
|
|
490
|
+
console.print(f" [dim]Suggestion: {issue.suggestion}[/dim]")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _output_json(issues: List[LintIssue]):
|
|
494
|
+
"""Output issues in JSON format."""
|
|
495
|
+
import json
|
|
496
|
+
|
|
497
|
+
output = [
|
|
498
|
+
{
|
|
499
|
+
'file': str(issue.file),
|
|
500
|
+
'line': issue.line,
|
|
501
|
+
'column': issue.column,
|
|
502
|
+
'severity': issue.severity.value,
|
|
503
|
+
'code': issue.code,
|
|
504
|
+
'message': issue.message,
|
|
505
|
+
'suggestion': issue.suggestion,
|
|
506
|
+
}
|
|
507
|
+
for issue in issues
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
print(json.dumps(output, indent=2))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _output_github(issues: List[LintIssue]):
|
|
514
|
+
"""Output issues in GitHub Actions format."""
|
|
515
|
+
severity_map = {
|
|
516
|
+
Severity.ERROR: 'error',
|
|
517
|
+
Severity.WARNING: 'warning',
|
|
518
|
+
Severity.INFO: 'notice',
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
for issue in issues:
|
|
522
|
+
severity = severity_map[issue.severity]
|
|
523
|
+
print(f"::{severity} file={issue.file},line={issue.line},col={issue.column}::[{issue.code}] {issue.message}")
|