├── tests ├── views │ ├── invalid.blade.php │ ├── invalid-expression.blade.php │ ├── valid.blade.php │ └── invalid-phpstan.blade.php └── BladeLinterCommandTest.php ├── config └── config.php ├── src ├── Backend.php ├── BladeLinterServiceProvider.php ├── ErrorRecord.php ├── Backend │ ├── Evaluate.php │ ├── PhpParser.php │ ├── ExtAst.php │ └── Cli.php └── BladeLinterCommand.php ├── .phpcs.xml.dist ├── phpunit.xml.dist ├── LICENSE.md ├── bin └── blade-linter ├── README.md └── composer.json /tests/views/invalid.blade.php: -------------------------------------------------------------------------------- 1 | @if() 2 | @section() 3 | @push() 4 | @yield() 5 | -------------------------------------------------------------------------------- /tests/views/invalid-expression.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layout') 2 | 3 | @section("body") 4 | @php(isset($a = 1)) 5 | @endsection 6 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public function analyze(\SplFileInfo $file, string $code): array; 14 | 15 | public static function name(): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/BladeLinterServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 13 | $this->commands([BladeLinterCommand::class]); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ErrorRecord.php: -------------------------------------------------------------------------------- 1 | message} in {$this->path} on line {$this->line}"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | config 4 | src 5 | tests 6 | 7 | */.direnv/* 8 | */tests/views/* 9 | */vendor/* 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Backend/Evaluate.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function analyze(\SplFileInfo $file, string $code): array 17 | { 18 | $code = 'function() { ?>' . $code . 'getMessage(), 26 | $file->getPathname(), 27 | $e->getLine() 28 | ) 29 | ]; 30 | } 31 | 32 | return []; 33 | } 34 | 35 | public static function name(): string 36 | { 37 | return 'eval'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | src/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | tests 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Backend/PhpParser.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function analyze(\SplFileInfo $file, string $code): array 19 | { 20 | $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 21 | try { 22 | $parser->parse($code); 23 | } catch (Error $e) { 24 | return [ 25 | new ErrorRecord( 26 | 'Parse error: ' . $e->getRawMessage(), 27 | $file->getPathname(), 28 | $e->getStartLine() 29 | ) 30 | ]; 31 | } 32 | 33 | return []; 34 | } 35 | 36 | public static function name(): string 37 | { 38 | return 'php-parser'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Backend/ExtAst.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function analyze(\SplFileInfo $file, string $code): array 23 | { 24 | try { 25 | parse_code($code, $this->astVersion); 26 | } catch (\ParseError $e) { 27 | return [ 28 | new ErrorRecord( 29 | 'Parse error: ' . $e->getMessage(), 30 | $file->getPathname(), 31 | $e->getLine() 32 | ) 33 | ]; 34 | } 35 | 36 | return []; 37 | } 38 | 39 | public static function name(): string 40 | { 41 | return 'ext-ast'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © Benjamin Delespierre, John Boehr, and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /bin/blade-linter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | useAppPath($basePath); 17 | $app['config'] = fn() => new Config([ 18 | // use current directory as view path 19 | 'view.paths' => [ $basePath ], 20 | 21 | // send compiled views to /tmp 22 | 'view.compiled' => sys_get_temp_dir(), 23 | ]); 24 | 25 | // register services 26 | (new FilesystemServiceProvider($app))->register(); 27 | (new ViewServiceProvider($app))->register(); 28 | 29 | // set the container so the Config::get calls resolve 30 | Facade::setFacadeApplication($app); 31 | 32 | // prepare the command 33 | $command = new BladeLinterCommand(); 34 | $command->setLaravel($app); 35 | $command->setName('lint'); 36 | 37 | // create the Symfony console application 38 | $application = new Application(); 39 | $application->add($command); 40 | $application->run(); 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Blade Linter 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/jbboehr/laravel-blade-linter.svg?style=flat-square)](https://packagist.org/packages/jbboehr/laravel-blade-linter) 4 | [![ci](https://github.com/jbboehr/laravel-blade-linter/actions/workflows/ci.yml/badge.svg)](https://github.com/jbboehr/laravel-blade-linter/actions/workflows/ci.yml) 5 | [![Test Coverage](https://api.codeclimate.com/v1/badges/91da0bd0a4a06c57fc94/test_coverage)](https://codeclimate.com/github/jbboehr/laravel-blade-linter/test_coverage) 6 | [![Maintainability](https://api.codeclimate.com/v1/badges/91da0bd0a4a06c57fc94/maintainability)](https://codeclimate.com/github/jbboehr/laravel-blade-linter/maintainability) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md) 8 | 9 | Performs syntax-checks of your Blade templates. Just that. 10 | 11 | ## Installation 12 | 13 | You can install the package via composer: 14 | 15 | ```bash 16 | composer require --dev bdelespierre/laravel-blade-linter 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```bash 22 | php artisan blade:lint 23 | ``` 24 | 25 | Or if you want to lint specific templates or directories: 26 | 27 | ```bash 28 | php artisan blade:lint resources/views/ 29 | ``` 30 | 31 | ### Testing 32 | 33 | ``` bash 34 | composer test 35 | ``` 36 | 37 | ## Credits 38 | 39 | - [All Contributors](./graphs/contributors) 40 | 41 | ## License 42 | 43 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 44 | -------------------------------------------------------------------------------- /src/Backend/Cli.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function analyze(\SplFileInfo $file, string $code): array 20 | { 21 | $output = ''; 22 | $message = ''; 23 | $result = $this->lint($code, $output, $message); 24 | 25 | if (!$result) { 26 | $line = null; 27 | if (false !== preg_match(self::REGEX, trim($message), $matches)) { 28 | $line = isset($matches[1]) ? (int) $matches[1] : null; 29 | $message = preg_replace(self::REGEX, '', $message) ?? ''; 30 | } 31 | 32 | return [ 33 | new ErrorRecord( 34 | trim($message), 35 | $file->getPathname(), 36 | $line 37 | ), 38 | ]; 39 | } 40 | 41 | return []; 42 | } 43 | 44 | public static function name(): string 45 | { 46 | return 'cli'; 47 | } 48 | 49 | private function lint(string $code, string &$stdout = "", string &$stderr = ""): bool 50 | { 51 | $descriptors = [ 52 | 0 => ["pipe", "r"], // read from stdin 53 | 1 => ["pipe", "w"], // write to stdout 54 | 2 => ["pipe", "w"], // write to stderr 55 | ]; 56 | 57 | // open linter process (php -l) 58 | $process = proc_open('php -d display_errors=stderr -l', $descriptors, $pipes); 59 | 60 | if (!is_resource($process)) { 61 | throw new \RuntimeException("unable to open process 'php -l'"); 62 | } 63 | 64 | fwrite($pipes[0], $code); 65 | fclose($pipes[0]); 66 | 67 | $stdout = stream_get_contents($pipes[1]) ?: ''; 68 | fclose($pipes[1]); 69 | 70 | $stderr = stream_get_contents($pipes[2]) ?: ''; 71 | fclose($pipes[2]); 72 | 73 | // it is important that you close any pipes before calling 74 | // proc_close in order to avoid a deadlock 75 | $retval = proc_close($process); 76 | 77 | // zero actually means "no error" 78 | return $retval === 0; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bdelespierre/laravel-blade-linter", 3 | "description": "A simple Blade template syntax checker for Laravel", 4 | "keywords": [ 5 | "laravel-blade-linter", 6 | "laravel", 7 | "blade", 8 | "linter" 9 | ], 10 | "homepage": "https://github.com/bdelespierre/laravel-blade-linter", 11 | "license": "MIT", 12 | "type": "library", 13 | "authors": [ 14 | { 15 | "name": "Benjamin Delespierre", 16 | "email": "benjamin.delespierre@gmail.com" 17 | }, 18 | { 19 | "name": "John Boehr", 20 | "email": "jbboehr@gmail.com" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.0", 25 | "illuminate/support": "^9.0 || ^10.0" 26 | }, 27 | "require-dev": { 28 | "laravel/framework": "^9.0 || ^10.0", 29 | "nikic/php-parser": "^4.15", 30 | "nunomaduro/larastan": "^2.0", 31 | "orchestra/testbench": "^7.22.0", 32 | "phpstan/phpstan": "^1.9", 33 | "phpstan/phpstan-phpunit": "^1.3", 34 | "phpstan/phpstan-strict-rules": "^1.4", 35 | "phpunit/phpunit": "^9.3", 36 | "squizlabs/php_codesniffer": "^3.7" 37 | }, 38 | "suggest": { 39 | "ext-ast": "*", 40 | "nikic/php-parser": "*" 41 | }, 42 | "bin": [ 43 | "bin/blade-linter" 44 | ], 45 | "autoload": { 46 | "psr-4": { 47 | "Bdelespierre\\LaravelBladeLinter\\": "src", 48 | "Bdelespierre\\LaravelBladeLinter\\FakeApp\\": "." 49 | } 50 | }, 51 | "autoload-dev": { 52 | "Tests\\": "tests/" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "drupol/composer-plugin-nixify": true 58 | } 59 | }, 60 | "extra": { 61 | "laravel": { 62 | "providers": [ 63 | "Bdelespierre\\LaravelBladeLinter\\BladeLinterServiceProvider" 64 | ], 65 | "aliases": { 66 | "LaravelBladeLinter": "Bdelespierre\\LaravelBladeLinter\\BladeLinterFacade" 67 | } 68 | }, 69 | "branch-alias": { 70 | "dev-master": "1.0.x-dev", 71 | "dev-develop": "1.0.x-dev" 72 | } 73 | }, 74 | "scripts": { 75 | "test": [ 76 | "vendor/bin/phpunit --color=always" 77 | ], 78 | "test:coverage": [ 79 | "@putenv XDEBUG_MODE=coverage", 80 | "vendor/bin/phpunit --color=always --coverage-clover=\"build/coverage/clover.xml\"" 81 | ], 82 | "test:coverage-html": [ 83 | "@putenv XDEBUG_MODE=coverage", 84 | "vendor/bin/phpunit --color=always --coverage-html=\"build/coverage\"" 85 | ], 86 | "build": [ 87 | "rm -rfv vendor/ *.phar && composer install --no-dev && phar-composer build ./" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/BladeLinterCommandTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected function getPackageProviders($app): array 18 | { 19 | return [ 20 | BladeLinterServiceProvider::class 21 | ]; 22 | } 23 | 24 | /** 25 | * @param Application $app 26 | */ 27 | protected function getEnvironmentSetUp($app): void 28 | { 29 | $app->make('config')->set('view.paths', [ 30 | __DIR__ . '/views', 31 | ]); 32 | } 33 | 34 | /** 35 | * @dataProvider backendProvider 36 | */ 37 | public function testValidBladeFilePass(string $backend): void 38 | { 39 | $path = __DIR__ . '/views/valid.blade.php'; 40 | $exit = Artisan::call('blade:lint', ['-v' => true, '--backend' => $backend, 'path' => $path]); 41 | $output = trim(Artisan::output()); 42 | 43 | $this->assertEquals( 44 | 0, 45 | $exit, 46 | "Validating a valid template should exit with an 'OK' status" 47 | ); 48 | 49 | $this->assertStringContainsString( 50 | "No syntax errors detected in {$path}", 51 | $output, 52 | "Validating a valid template should display the validation message" 53 | ); 54 | 55 | if ($backend !== 'auto') { 56 | $this->assertStringContainsString($backend, $output); 57 | } 58 | } 59 | 60 | /** 61 | * @dataProvider backendProvider 62 | */ 63 | public function testInvalidBladeFilePass(string $backend): void 64 | { 65 | $path = __DIR__ . '/views/invalid.blade.php'; 66 | $exit = Artisan::call('blade:lint', ['-v' => true, '--backend' => $backend, 'path' => $path]); 67 | 68 | $this->assertEquals( 69 | 1, 70 | $exit, 71 | "Validating an invalid template should exit with a 'NOK' status" 72 | ); 73 | 74 | $this->assertMatchesRegularExpression( 75 | "~Parse error: ?syntax error, unexpected .* in {$path} on line 1~i", 76 | trim(Artisan::output()), 77 | "Syntax error should be displayed" 78 | ); 79 | } 80 | 81 | /** 82 | * @dataProvider backendProvider 83 | */ 84 | public function testInvalidExpressionBladeFilePass(string $backend): void 85 | { 86 | if ($backend !== 'cli') { 87 | $this->markTestSkipped('This currently is an E_COMPILE_ERROR in PHP and not a parse error'); 88 | } 89 | 90 | $path = __DIR__ . '/views/invalid-expression.blade.php'; 91 | $exit = Artisan::call('blade:lint', ['-v' => true, '--backend' => $backend, 'path' => $path]); 92 | 93 | $this->assertEquals( 94 | 1, 95 | $exit, 96 | "Validating an invalid template should exit with a 'NOK' status" 97 | ); 98 | 99 | $this->assertMatchesRegularExpression( 100 | "~Fatal error: Cannot use isset\(\) on the result of an expression .* in {$path} on line \d+~i", 101 | trim(Artisan::output()), 102 | "Syntax error should be displayed" 103 | ); 104 | } 105 | 106 | /** 107 | * @dataProvider backendProvider 108 | */ 109 | public function testWithoutPath(string $backend): void 110 | { 111 | if ($backend === 'eval') { 112 | $this->markTestSkipped('This currently is an E_COMPILE_ERROR in PHP and not a parse error'); 113 | } 114 | 115 | $exit = Artisan::call('blade:lint', ['-v' => true, '--backend' => $backend]); 116 | 117 | $this->assertEquals( 118 | 1, 119 | $exit, 120 | "Validating an invalid template should exit with a 'NOK' status" 121 | ); 122 | 123 | $output = Artisan::output(); 124 | 125 | $this->assertMatchesRegularExpression( 126 | "~No syntax errors detected in .*/tests/views/invalid-phpstan\\.blade\\.php\n~i", 127 | $output, 128 | ); 129 | 130 | $this->assertMatchesRegularExpression( 131 | "~Parse error: ?syntax error, unexpected .* in .*/tests/views/invalid\\.blade\\.php on line 1\n~i", 132 | $output, 133 | ); 134 | 135 | $this->assertMatchesRegularExpression( 136 | "~No syntax errors detected in .*/tests/views/valid\\.blade\\.php\n~i", 137 | $output, 138 | ); 139 | } 140 | 141 | /** 142 | * @dataProvider backendProvider 143 | */ 144 | public function testWithMultiplePaths(string $backend): void 145 | { 146 | $path = [ 147 | __DIR__ . '/views/valid.blade.php', 148 | __DIR__ . '/views/invalid.blade.php', 149 | ]; 150 | 151 | $exit = Artisan::call('blade:lint', ['-v' => true, '--backend' => $backend, 'path' => $path]); 152 | 153 | $this->assertEquals( 154 | 1, 155 | $exit, 156 | "Validating an invalid template should exit with a 'NOK' status" 157 | ); 158 | 159 | $output = trim(Artisan::output()); 160 | 161 | $this->assertStringContainsString( 162 | "No syntax errors detected in {$path[0]}", 163 | $output, 164 | "Validating a valid template should display the validation message" 165 | ); 166 | 167 | $this->assertMatchesRegularExpression( 168 | "~Parse error: ?syntax error, unexpected .* in {$path[1]} on line 1~i", 169 | $output, 170 | "Syntax error should be displayed" 171 | ); 172 | } 173 | 174 | /** 175 | * @return list> 176 | */ 177 | public function backendProvider(): array 178 | { 179 | return [ 180 | ['auto'], 181 | ['cli'], 182 | ['eval'], 183 | ['ext-ast'], 184 | ['php-parser'], 185 | ]; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/BladeLinterCommand.php: -------------------------------------------------------------------------------- 1 | prepareBackends(); 26 | $codeclimate = $this->getCodeClimateOutput(); 27 | $allErrors = []; 28 | $nScanned = 0; 29 | 30 | if ($this->getOutput()->isVerbose()) { 31 | $this->info('blade-lint: Using backends: ' . join(', ', array_map(fn (Backend $backend) => $backend->name(), $backends))); 32 | } 33 | 34 | foreach ($this->getBladeFiles() as $file) { 35 | $errors = $this->checkFile($file, ...$backends); 36 | if (count($errors) > 0) { 37 | $status = self::FAILURE; 38 | foreach ($errors as $error) { 39 | $this->error($error->toString()); 40 | } 41 | } elseif ($this->getOutput()->isVerbose()) { 42 | $this->line("No syntax errors detected in {$file->getPathname()}"); 43 | } 44 | 45 | $allErrors = array_merge($allErrors, $errors); 46 | $nScanned++; 47 | } 48 | 49 | if ($codeclimate !== null) { 50 | fwrite($codeclimate, json_encode( 51 | array_map(function (ErrorRecord $error) { 52 | return [ 53 | 'type' => 'issue', 54 | 'check_name' => 'Laravel Blade Lint', 55 | 'description' => $error->message, 56 | 'fingerprint' => md5(join("|", [$error->message, $error->path, $error->line])), 57 | 'categories' => ['Bug Risk'], 58 | 'location' => [ 59 | 'path' => $error->path, 60 | 'lines' => [ 61 | 'begin' => $error->line, 62 | ], 63 | ], 64 | 'severity' => 'blocker' 65 | ]; 66 | }, $allErrors), 67 | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR 68 | )); 69 | } 70 | 71 | $this->info('blade-lint: scanned: ' . $nScanned . ' files'); 72 | 73 | return $status ?? self::SUCCESS; 74 | } 75 | 76 | /** 77 | * @return \Generator<\SplFileInfo> 78 | */ 79 | protected function getBladeFiles(): \Generator 80 | { 81 | $paths = Arr::wrap($this->argument('path') ?: Config::get('view.paths')); 82 | 83 | foreach ($paths as $path) { 84 | if (is_file($path)) { 85 | yield new \SplFileInfo($path); 86 | continue; 87 | } 88 | 89 | $it = new \RecursiveDirectoryIterator($path); 90 | $it = new \RecursiveIteratorIterator($it); 91 | /** @var \RegexIterator> $it */ 92 | $it = new \RegexIterator($it, '/\.blade\.php$/', \RegexIterator::MATCH); 93 | 94 | yield from $it; 95 | } 96 | } 97 | 98 | /** 99 | * @return list 100 | */ 101 | private function checkFile(\SplFileInfo $file, Backend ...$backends): array 102 | { 103 | $code = file_get_contents($file->getPathname()); 104 | 105 | if ($code === false) { 106 | throw new \RuntimeException('Failed to open file ' . $file->getPathname()); 107 | } 108 | 109 | // compile the file and send it to the linter process 110 | $compiled = Blade::compileString($code); 111 | 112 | $errors = []; 113 | 114 | foreach ($backends as $backend) { 115 | $errors = array_merge( 116 | $errors, 117 | $backend->analyze($file, $compiled) 118 | ); 119 | } 120 | 121 | return $errors; 122 | } 123 | 124 | /** 125 | * @return Backend[] 126 | */ 127 | private function prepareBackends(): array 128 | { 129 | return array_map(function ($backendSpec) { 130 | switch ($backendSpec) { 131 | default: // case 'auto': 132 | $fast = $this->option('fast'); 133 | if ($fast && extension_loaded('ast')) { 134 | goto ext_ast; 135 | } elseif ($fast && class_exists(ParserFactory::class)) { 136 | goto php_parser; 137 | } 138 | goto cli; 139 | 140 | case 'cli': 141 | cli: 142 | return new Backend\Cli(); 143 | 144 | case 'eval': 145 | return new Backend\Evaluate(); 146 | 147 | case 'ext-ast': 148 | ext_ast: 149 | return new Backend\ExtAst(); 150 | 151 | case 'php-parser': 152 | php_parser: 153 | return new Backend\PhpParser(); 154 | } 155 | /** @phpstan-ignore-next-line */ 156 | }, (array) $this->option('backend')); 157 | } 158 | 159 | /** 160 | * @return ?resource 161 | */ 162 | private function getCodeClimateOutput(): mixed 163 | { 164 | $codeclimate = $this->option('codeclimate') ?: 'stderr'; 165 | 166 | /** @phpstan-ignore-next-line */ 167 | if ($codeclimate === true || is_array($codeclimate)) { 168 | $codeclimate = 'stderr'; 169 | } 170 | 171 | return match ($codeclimate) { 172 | 'false' => null, 173 | 'stderr' => STDERR, 174 | 'stdout' => STDOUT, 175 | default => fopen($codeclimate, 'w') ?: null, 176 | }; 177 | } 178 | } 179 | --------------------------------------------------------------------------------