{"id":40122497,"url":"https://github.com/ia-eknorr/ignition-lint","last_synced_at":"2026-02-25T08:25:27.787Z","repository":{"id":221649364,"uuid":"754892629","full_name":"ia-eknorr/ignition-lint","owner":"ia-eknorr","description":"Github action to lint Ignition projects","archived":false,"fork":false,"pushed_at":"2026-02-24T19:04:54.000Z","size":4184,"stargazers_count":18,"open_issues_count":18,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-24T23:41:11.632Z","etag":null,"topics":["automation","ignition","inductive","inductive-automation","lint","linter"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ia-eknorr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-02-09T00:42:21.000Z","updated_at":"2026-02-17T22:13:01.000Z","dependencies_parsed_at":"2024-02-22T03:25:57.383Z","dependency_job_id":"ad6fdb94-5a6c-4ceb-bb3d-64a45cfa68ef","html_url":"https://github.com/ia-eknorr/ignition-lint","commit_stats":null,"previous_names":["ia-eknorr/ignition-lint"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/ia-eknorr/ignition-lint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ia-eknorr%2Fignition-lint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ia-eknorr%2Fignition-lint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ia-eknorr%2Fignition-lint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ia-eknorr%2Fignition-lint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ia-eknorr","download_url":"https://codeload.github.com/ia-eknorr/ignition-lint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ia-eknorr%2Fignition-lint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29815021,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T05:36:42.804Z","status":"ssl_error","status_checked_at":"2026-02-25T05:36:31.934Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","ignition","inductive","inductive-automation","lint","linter"],"created_at":"2026-01-19T13:00:30.073Z","updated_at":"2026-02-25T08:25:27.779Z","avatar_url":"https://github.com/ia-eknorr.png","language":"Python","funding_links":[],"categories":["Ignition Lint - Linter for Ignition screens and scripts"],"sub_categories":[],"readme":"# Ignition Lint Documentation\n\n## Overview\n\nIgnition Lint is a Python framework designed to analyze and lint Ignition Perspective view.json files. It provides a structured way to parse view files, build an object model representation, and apply customizable linting rules to ensure code quality and consistency across your Ignition projects.\n\n## Getting Started with Poetry\n\n### Prerequisites\n- Python 3.9 or higher\n- Poetry \u003e= 2.0 (install from [python-poetry.org](https://python-poetry.org/docs/#installation))\n\n### Installation Methods\n\n#### Option 1: Install from PyPI (Recommended for Users)\n```bash\n# Install the package\npip install ignition-lint\n\n# Verify installation\nignition-lint --help\n```\n\n#### Option 2: Development Setup with Poetry\n1. **Clone the repository:**\n   ```bash\n   git clone https://github.com/design-group/ignition-lint.git\n   cd ignition-lint\n   ```\n\n2. **Install dependencies with Poetry:**\n   ```bash\n   poetry install\n   ```\n\n3. **Activate the virtual environment:**\n   ```bash\n   poetry shell\n   ```\n\n4. **Verify installation:**\n   ```bash\n   poetry run python -m ignition_lint --help\n   ```\n\n### Development Setup\n\nFor development work, install with development dependencies:\n\n```bash\n# Install all dependencies including dev tools\npoetry install --with dev\n\n# Run tests\ncd tests\npoetry run python test_runner.py --run-all\n\n# Run linting\npoetry run pylint ignition_lint/\n\n# Format code\npoetry run yapf -ir ignition_lint/\n```\n\n### Running Without Activating Shell\n\nYou can run commands directly through Poetry without activating the shell:\n\n```bash\n# Run linting on a view file\npoetry run python -m ignition_lint path/to/view.json\n\n# Run with custom configuration\npoetry run python -m ignition_lint --config my_rules.json --files \"views/**/view.json\"\n\n# Using the CLI entry point\npoetry run ignition-lint path/to/view.json\n```\n\n### Building and Distribution\n\n```bash\n# Build the package\npoetry build\n\n# Install locally for testing\npoetry install\n\n# Export requirements.txt for CI/CD or Docker\npoetry export --output requirements.txt --without-hashes\n```\n\n## Key Features\n\n- **Object Model Representation**: Converts flattened JSON structures into a hierarchical object model\n- **Extensible Rule System**: Easy-to-extend framework for creating custom linting rules\n- **Built-in Rules**: Includes rules for script validation (via Pylint) and binding checks\n- **Batch Processing**: Efficiently processes multiple scripts and files in a single run\n- **Pre-commit Integration**: Can be integrated into your Git workflow\n\n## Architecture\n\n### Core Components\n\n```\nignition_lint/\n├── common/          # Utilities for JSON processing\n├── model/           # Object model definitions\n├── rules/           # Linting rule implementations\n├── linter.py        # Main linting engine\n└── main.py          # CLI entry point\n```\n\n### Object Model\n\nThe framework decompiles Ignition Perspective views into a structured object model with the following node types:\n\n#### Base Classes\n- **ViewNode**: Abstract base class for all nodes in the view tree\n- **Visitor**: Interface for implementing the visitor pattern\n\n#### Component Nodes\n- **Component**: Represents UI components with properties and metadata\n- **Property**: Individual component properties\n\n#### Binding Nodes\n- **Binding**: Base class for all binding types\n- **ExpressionBinding**: Expression-based bindings\n- **PropertyBinding**: Property-to-property bindings\n- **TagBinding**: Tag-based bindings\n\n#### Script Nodes\n- **Script**: Base class for all script types\n- **MessageHandlerScript**: Scripts that handle messages\n- **CustomMethodScript**: Custom component methods\n- **TransformScript**: Script transforms in bindings\n- **EventHandlerScript**: Event handler scripts\n\n#### Event Nodes\n- **EventHandler**: Base class for event handlers\n\n## How It Works\n\n### 1. JSON Flattening\n\nThe framework first flattens the hierarchical view.json structure into path-value pairs:\n\n```python\n# Original JSON\n{\n  \"root\": {\n    \"children\": [{\n      \"meta\": {\"name\": \"Button\"},\n      \"props\": {\"text\": \"Click Me\"}\n    }]\n  }\n}\n\n# Flattened representation\n{\n  \"root.children[0].meta.name\": \"Button\",\n  \"root.children[0].props.text\": \"Click Me\"\n}\n```\n\n### 2. Model Building\n\nThe `ViewModelBuilder` class parses the flattened JSON and constructs the object model:\n\n```python\nfrom ignition_lint.common.flatten_json import flatten_file\nfrom ignition_lint.model import ViewModelBuilder\n\n# Flatten the JSON file\nflattened_json = flatten_file(\"path/to/view.json\")\n\n# Build the object model\nbuilder = ViewModelBuilder()\nmodel = builder.build_model(flattened_json)\n\n# Access different node types\ncomponents = model['components']\nbindings = model['bindings']\nscripts = model['scripts']\n```\n\n### 3. Rule Application\n\nRules are applied using the visitor pattern, allowing each rule to process relevant nodes:\n\n```python\nfrom ignition_lint.linter import LintEngine\nfrom ignition_lint.rules import PylintScriptRule, PollingIntervalRule\n\n# Create linter with rules\nlinter = LintEngine()\nlinter.register_rule(PylintScriptRule())\nlinter.register_rule(PollingIntervalRule(minimum_interval=10000))\n\n# Run linting\nerrors = linter.lint(flattened_json)\n```\n\n## Understanding the Visitor Pattern\n\n### What is the Visitor Pattern?\n\nThe Visitor pattern is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. In Ignition Lint, it allows you to define new operations (linting rules) without changing the node classes.\n\n### How It Works in Ignition Lint\n\n1. **Node Classes**: Each node type (Component, Binding, Script, etc.) has an `accept()` method that takes a visitor\n2. **Visitor Interface**: The `Visitor` base class defines visit methods for each node type\n3. **Double Dispatch**: When a node accepts a visitor, it calls the appropriate visit method on that visitor\n\nHere's the flow:\n\n```python\n# 1. The linter calls accept on a node\nnode.accept(rule)\n\n# 2. The node's accept method calls back to the visitor\ndef accept(self, visitor):\n    return visitor.visit_component(self)  # for a Component node\n\n# 3. The visitor's method processes the node\ndef visit_component(self, node):\n    # Your rule logic here\n    pass\n```\n\n### Why Use the Visitor Pattern?\n\n- **Separation of Concerns**: Node structure is separate from operations\n- **Easy Extension**: Add new rules without modifying node classes\n- **Type Safety**: Each node type has its own visit method\n- **Flexible Processing**: Rules can choose which nodes to process\n\n## Creating Custom Rules - Deep Dive\n\n### What You Have Access To\n\nWhen writing a custom rule, you have access to extensive information about each node:\n\n#### Component Nodes\n\n```python\nclass MyComponentRule(LintingRule):\n    def visit_component(self, node):\n        # Available properties:\n        node.path      # Full path in the view: \"root.children[0].components.Label\"\n        node.name      # Component name: \"Label_1\"\n        node.type      # Component type: \"ia.display.label\"\n        node.properties # Dict of all component properties\n\n        # Example: Check component positioning\n        x_position = node.properties.get('position.x', 0)\n        y_position = node.properties.get('position.y', 0)\n\n        if x_position \u003c 0 or y_position \u003c 0:\n            self.errors.append(\n                f\"{node.path}: Component '{node.name}' has negative position\"\n            )\n```\n\n#### Binding Nodes\n\n```python\nclass MyBindingRule(LintingRule):\n    def visit_expression_binding(self, node):\n        # Available for all bindings:\n        node.path         # Path to the bound property\n        node.binding_type # Type of binding: \"expr\", \"property\", \"tag\"\n        node.config       # Full binding configuration dict\n\n        # Specific to expression bindings:\n        node.expression   # The expression string\n\n        # Example: Check for hardcoded values in expressions\n        if '\"localhost\"' in node.expression or \"'localhost'\" in node.expression:\n            self.errors.append(\n                f\"{node.path}: Expression contains hardcoded localhost\"\n            )\n\n    def visit_tag_binding(self, node):\n        # Specific to tag bindings:\n        node.tag_path    # The tag path string\n\n        # Example: Ensure tags follow naming convention\n        if not node.tag_path.startswith(\"[default]\"):\n            self.errors.append(\n                f\"{node.path}: Tag binding should use [default] provider\"\n            )\n```\n\n#### Script Nodes\n\n```python\nclass MyScriptRule(LintingRule):\n    def visit_custom_method(self, node):\n        # Available properties:\n        node.path         # Path to the method\n        node.name         # Method name: \"refreshData\"\n        node.script         # Raw script code\n        node.params       # List of parameter names\n\n        # Special method:\n        formatted_script = node.get_formatted_script()\n        # Returns properly formatted Python with function definition\n\n        # Example: Check for print statements\n        if 'print(' in node.script:\n            self.errors.append(\n                f\"{node.path}: Method '{node.name}' contains print statement\"\n            )\n\n    def visit_message_handler(self, node):\n        # Additional properties:\n        node.message_type # The message type this handles\n        node.scope        # Dict with scope settings:\n                            # {'page': False, 'session': True, 'view': False}\n\n        # Example: Warn about session-scoped handlers\n        if node.scope.get('session', False):\n            self.errors.append(\n                f\"{node.path}: Message handler '{node.message_type}' \"\n                f\"uses session scope - ensure this is intentional\"\n            )\n```\n\n### Advanced Rule Patterns\n\n#### Pattern 1: Cross-Node Validation\n\n```python\nclass CrossReferenceRule(LintingRule):\n    def __init__(self):\n        super().__init__(node_types=[Component, PropertyBinding])\n        self.component_paths = set()\n        self.binding_targets = []\n\n    def visit_component(self, node):\n        # Collect all component paths\n        self.component_paths.add(node.path)\n\n    def visit_property_binding(self, node):\n        # Store binding for later validation\n        self.binding_targets.append((node.path, node.target_path))\n\n    def process_collected_scripts(self):\n        # This method is called after all nodes are visited\n        for binding_path, target_path in self.binding_targets:\n            if target_path not in self.component_paths:\n                self.errors.append(\n                    f\"{binding_path}: Binding targets non-existent component\"\n                )\n```\n\n#### Pattern 2: Context-Aware Rules\n\n```python\nclass ContextAwareRule(LintingRule):\n    def __init__(self):\n        super().__init__(node_types=[Component, Script])\n        self.current_component = None\n        self.component_stack = []\n\n    def visit_component(self, node):\n        # Track component context\n        self.component_stack.append(node)\n        self.current_component = node\n\n    def visit_script(self, node):\n        # Use component context\n        if self.current_component and self.current_component.type == \"ia.display.table\":\n            if \"selectedRow\" in node.script and \"rowData\" not in node.script:\n                self.errors.append(\n                    f\"{node.path}: Table script uses selectedRow without rowData check\"\n                )\n```\n\n#### Pattern 3: Statistical Analysis\n\n```python\nclass ComplexityAnalysisRule(LintingRule):\n    def __init__(self, max_complexity_score=100):\n        super().__init__(node_types=[Component])\n        self.max_complexity = max_complexity_score\n        self.complexity_scores = {}\n\n    def visit_component(self, node):\n        score = 0\n\n        # Calculate complexity based on various factors\n        score += len(node.properties) * 2  # Property count\n\n        # Check for deeply nested properties\n        for prop_name in node.properties:\n            score += prop_name.count('.') * 3  # Nesting depth\n\n        # Store score\n        self.complexity_scores[node.path] = score\n\n        if score \u003e self.max_complexity:\n            self.errors.append(\n                f\"{node.path}: Component complexity score {score} \"\n                f\"exceeds maximum {self.max_complexity}\"\n            )\n```\n\n### Accessing Raw JSON Data\n\nSometimes you need access to the original flattened JSON data:\n\n```python\nclass RawDataRule(LintingRule):\n    def __init__(self):\n        super().__init__()\n        self.flattened_json = None\n\n    def lint(self, flattened_json):\n        # Store the flattened JSON for use in visit methods\n        self.flattened_json = flattened_json\n        return super().lint(flattened_json)\n\n    def visit_component(self, node):\n        # Access any part of the flattened JSON\n        style_classes = self.flattened_json.get(\n            f\"{node.path}.props.style.classes\",\n            \"\"\n        )\n\n        if style_classes and \"/\" in style_classes:\n            self.errors.append(\n                f\"{node.path}: Style classes contain invalid '/' character\"\n            )\n```\n\n### Rule Lifecycle Methods\n\n```python\nclass LifecycleAwareRule(LintingRule):\n    def __init__(self):\n        super().__init__()\n        self.setup_complete = False\n\n    def before_visit(self):\n        \"\"\"Called before visiting any nodes.\"\"\"\n        self.setup_complete = True\n        self.errors = []  # Reset errors\n\n    def visit_component(self, node):\n        \"\"\"Process each component.\"\"\"\n        # Your logic here\n        pass\n\n    def process_collected_scripts(self):\n        \"\"\"Called after all nodes are visited.\"\"\"\n        # Batch processing, cross-validation, etc.\n        pass\n\n    def after_visit(self):\n        \"\"\"Called after all processing is complete.\"\"\"\n        # Cleanup, summary generation, etc.\n        pass\n```\n\n## Node Properties Reference\n\n### Component\n- `path`: Full path to the component\n- `name`: Component instance name\n- `type`: Component type (e.g., \"ia.display.label\")\n- `properties`: Dictionary of all component properties\n- `children`: List of child components (if container)\n\n### ExpressionBinding\n- `path`: Path to the bound property\n- `expression`: The expression string\n- `binding_type`: Always \"expr\"\n- `config`: Full binding configuration\n\n### PropertyBinding\n- `path`: Path to the bound property\n- `target_path`: Path to the source property\n- `binding_type`: Always \"property\"\n- `config`: Full binding configuration\n\n### TagBinding\n- `path`: Path to the bound property\n- `tag_path`: The tag path string\n- `binding_type`: Always \"tag\"\n- `config`: Full binding configuration\n\n### MessageHandlerScript\n- `path`: Path to the handler\n- `script`: Script string\n- `message_type`: Type of message handled\n- `scope`: Scope configuration dict\n- `get_formatted_script()`: Returns formatted Python code\n\n### CustomMethodScript\n- `path`: Path to the method\n- `name`: Method name\n- `script`: Script string\n- `params`: List of parameter names\n- `get_formatted_script()`: Returns formatted Python code\n\n### TransformScript\n- `path`: Path to the transform\n- `script`: Script string\n- `binding_path`: Path to parent binding\n- `get_formatted_script()`: Returns formatted Python code\n\n### EventHandlerScript\n- `path`: Path to the handler\n- `event_type`: Event type (e.g., \"onClick\")\n- `script`: Script string\n- `scope`: Scope setting (\"L\", \"P\", \"S\")\n- `get_formatted_script()`: Returns formatted Python code\n\n## Available Rules\n\nThe following rules are currently implemented and available for use:\n\n| Rule | Type | Description | Configuration Options | Default Enabled |\n|------|------|-------------|----------------------|-----------------|\n| `NamePatternRule` | Warning | Validates naming conventions for components and other elements | `convention`, `target_node_types`, `custom_pattern`, `node_type_specific_rules` | ✅ |\n| `PollingIntervalRule` | Error | Ensures polling intervals meet minimum thresholds to prevent performance issues | `minimum_interval` (default: 10000ms) | ✅ |\n| `PylintScriptRule` | Error | Runs Pylint analysis on all scripts to detect syntax errors, undefined variables, and code quality issues | None (uses default Pylint configuration) | ✅ |\n| `UnusedCustomPropertiesRule` | Warning | Detects custom properties and view parameters that are defined but never referenced | None | ✅ |\n| `BadComponentReferenceRule` | Error | Identifies brittle component object traversal patterns (getSibling, getParent, etc.) | `forbidden_patterns`, `case_sensitive` | ✅ |\n\n### Rule Details\n\n#### NamePatternRule\nValidates naming conventions across different node types with flexible configuration options.\n\n**Supported Conventions:**\n- `PascalCase` (default)\n- `camelCase`\n- `snake_case`\n- `kebab-case`\n- `SCREAMING_SNAKE_CASE`\n- `Title Case`\n- `lower case`\n\n**Configuration Example:**\n```json\n{\n  \"NamePatternRule\": {\n    \"enabled\": true,\n    \"kwargs\": {\n      \"convention\": \"PascalCase\",\n      \"target_node_types\": [\"component\"],\n      \"node_type_specific_rules\": {\n        \"custom_method\": {\n          \"convention\": \"camelCase\"\n        }\n      }\n    }\n  }\n}\n```\n\n#### PollingIntervalRule\nPrevents performance issues by enforcing minimum polling intervals in `now()` expressions.\n\n**What it checks:**\n- Expression bindings containing `now()` calls\n- Property and tag bindings with polling configurations\n- Validates interval values are above minimum threshold\n\n#### PylintScriptRule\nComprehensive Python code analysis using Pylint for all script types:\n- Custom method scripts\n- Event handler scripts\n- Message handler scripts\n- Transform scripts\n\n**Detected Issues:**\n- Syntax errors\n- Undefined variables\n- Unused imports\n- Code style violations\n- Logical errors\n\n#### UnusedCustomPropertiesRule\nIdentifies unused custom properties and view parameters to reduce view complexity.\n\n**Detection Coverage:**\n- View-level custom properties (`custom.*`)\n- View parameters (`params.*`)\n- Component-level custom properties (`*.custom.*`)\n- References in expressions, bindings, and scripts\n\n#### BadComponentReferenceRule\nPrevents brittle view dependencies by detecting object traversal patterns.\n\n**Forbidden Patterns:**\n- `.getSibling()`, `.getParent()`, `.getChild()`, `.getChildren()`\n- `self.parent`, `self.children` property access\n- Any direct component tree navigation\n\n**Recommended Alternatives:**\n- Use `view.custom` properties for data sharing\n- Implement message handling for component communication\n- Design views with explicit data flow patterns\n\n## Usage Methods\n\nThis package can be utilized in several ways to fit different development workflows:\n\n### 1. Command Line Interface (CLI)\n\n#### Using the Installed Package\n```bash\n# After pip install ignition-lint\nignition-lint path/to/view.json\n\n# Lint multiple files with glob pattern\nignition-lint --files \"**/view.json\"\n\n# Use custom configuration\nignition-lint --config my_rules.json --files \"views/**/view.json\"\n\n# Show help\nignition-lint --help\n```\n\n#### Using Poetry (Development)\n```bash\n# Using the CLI entry point\npoetry run ignition-lint path/to/view.json\n\n# Using the module directly\npoetry run python -m ignition_lint path/to/view.json\n\n# If you've activated the Poetry shell\npoetry shell\nignition-lint path/to/view.json\n```\n\n### 2. Pre-commit Hook Integration\n\nAdd to your `.pre-commit-config.yaml`:\n\n```yaml\nrepos:\n  - repo: https://github.com/design-group/ignition-lint\n    rev: v0.1.0\n    hooks:\n      - id: ignition-lint\n        args: [\n          \"--config\", \"rule_config.json\",\n          \"--files\", \"**/*.json\"\n        ]\n        files: view\\.json$\n```\n\nInstall and run:\n```bash\n# Install pre-commit hooks\npre-commit install\n\n# Run on all files\npre-commit run --all-files\n\n# Run on staged files only\npre-commit run\n```\n\n### 3. GitHub Actions Workflow\n\nCreate `.github/workflows/ignition-lint.yml`:\n\n```yaml\nname: Ignition Lint\n\non:\n  push:\n    branches: [ main, develop ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.11\"\n\n      - name: Install ignition-lint\n        run: pip install ignition-lint\n\n      - name: Run ignition-lint\n        run: |\n          # Lint all view.json files in the repository\n          find . -name \"view.json\" -type f | while read file; do\n            echo \"Linting $file\"\n            ignition-lint \"$file\"\n          done\n```\n\n### 4. Development Mode with Poetry\n\nFor contributors and package developers:\n\n```bash\n# Clone and set up development environment\ngit clone https://github.com/design-group/ignition-lint.git\ncd ignition-lint\n\n# Install with Poetry\npoetry install\n\n# Test the package locally\npoetry run ignition-lint tests/cases/PreferredStyle/view.json\n\n# Run the full test suite\ncd tests\npoetry run python test_runner.py --run-all\n\n# Test GitHub Actions workflows locally\n./test-actions.sh\n\n# Format and lint code before committing\npoetry run yapf -ir src/ tests/\npoetry run pylint src/ignition_lint/\n```\n\n## Configuration System\n\n### Rule Configuration\n\nRules are configured via JSON files (default: `rule_config.json`):\n\n```json\n{\n  \"NamePatternRule\": {\n    \"enabled\": true,\n    \"kwargs\": {\n      \"convention\": \"PascalCase\",\n      \"target_node_types\": [\"component\"]\n    }\n  },\n  \"PollingIntervalRule\": {\n    \"enabled\": true,\n    \"kwargs\": {\n      \"minimum_interval\": 10000\n    }\n  }\n}\n```\n\n### Severity Levels\n\nSeverity levels are determined by rule developers based on what each rule checks. Users cannot configure severity levels.\n\n- **Warnings**: Style and preference issues that don't prevent functionality\n- **Errors**: Critical issues that can cause functional problems or break systems\n\n#### Built-in Rule Severities\n\n| Rule | Severity | Reason |\n|------|----------|---------|\n| `NamePatternRule` | Warning | Naming conventions are style preferences |\n| `PollingIntervalRule` | Error | Performance issues can cause system problems |\n| `PylintScriptRule` | Error | Syntax errors, undefined variables break functionality |\n\n#### Output Examples\n\n**Warnings (exit code 0):**\n```\n⚠️  Found 3 warnings in view.json:\n  📋 NamePatternRule (warning):\n    • component: Name doesn't follow PascalCase convention\n\n✅ No errors found (warnings only)\n```\n\n**Errors (exit code 1):**\n```\n❌ Found 2 errors in view.json:\n  📋 PollingIntervalRule (error):\n    • binding: Polling interval 5000ms below minimum 10000ms\n\n📈 Summary:\n  ❌ Total issues: 2\n```\n\n### Developer Guidelines for Rule Severity\n\nWhen creating custom rules, set the severity based on the impact:\n\n```python\nclass MyCustomRule(LintingRule):\n    # Use \"warning\" for style/preference issues\n    severity = \"warning\"\n\n    # Use \"error\" for functional/performance issues\n    # severity = \"error\"\n```\n\n## Best Practices\n\n1. **Rule Granularity**: Keep rules focused on a single concern\n2. **Performance**: Use batch processing for operations like script analysis\n3. **Error Messages**: Provide clear, actionable error messages with paths\n4. **Configuration**: Make rules configurable for different project requirements\n5. **Testing**: Test rules with various edge cases and malformed inputs\n6. **Node Type Selection**: Only register for node types you actually need to process\n\n## Future Enhancements\n\nThe framework is designed to be extended with:\n- Additional node types (e.g., style classes, custom properties)\n- More sophisticated analysis rules\n- Integration with CI/CD pipelines\n- Performance metrics and reporting\n- Auto-fix capabilities for certain rule violations\n\n## Contributing\n\nWhen adding new features:\n1. Follow the existing object model patterns\n2. Implement the visitor pattern for new node types\n3. Provide configuration options for new rules\n4. Document rule behavior and configuration\n5. Add appropriate error handling\n\n### Development Workflow\n\n```bash\n# Fork and clone the repository\ngit clone https://github.com/yourusername/ignition-lint.git\ncd ignition-lint\n\n# Install development dependencies\npoetry install --with dev\n\n# Create a feature branch\ngit checkout -b feature/my-new-feature\n\n# Make your changes and test\npoetry run pytest\npoetry run pylint ignition_lint/\n\n# Commit and push\ngit commit -m \"Add new feature\"\ngit push origin feature/my-new-feature\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fia-eknorr%2Fignition-lint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fia-eknorr%2Fignition-lint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fia-eknorr%2Fignition-lint/lists"}