diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c517dad..39c1fd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,35 +15,17 @@ jobs: strategy: matrix: include: - - mw: 'REL1_35' - php: 7.4 - experimental: false - - mw: 'REL1_37' - php: 7.4 - experimental: false - - mw: 'REL1_37' - php: 8.0 - experimental: false - - mw: 'REL1_37' - php: 8.1 - experimental: false - - mw: 'REL1_38' - php: 7.4 - experimental: false - - mw: 'REL1_38' + - mw: 'REL1_39' php: 8.0 experimental: false - - mw: 'REL1_38' + - mw: 'REL1_40' php: 8.1 - experimental: false - - mw: 'master' - php: 7.4 experimental: true - mw: 'master' - php: 8.0 + php: 8.1 experimental: true - mw: 'master' - php: 8.1 + php: 8.2 experimental: true runs-on: ubuntu-latest @@ -62,7 +44,7 @@ jobs: - name: Cache MediaWiki id: cache-mediawiki - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | mediawiki @@ -71,12 +53,12 @@ jobs: key: mw_${{ matrix.mw }}-php${{ matrix.php }} - name: Cache Composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.composer/cache key: composer-php${{ matrix.php }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: path: EarlyCopy @@ -85,12 +67,160 @@ jobs: working-directory: ~ run: bash EarlyCopy/.github/workflows/installWiki.sh ${{ matrix.mw }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: path: mediawiki/vendor/mediawiki/scss - name: Composer update - run: composer update + run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - name: Run PHPUnit run: php tests/phpunit/phpunit.php -c vendor/mediawiki/scss + + code-style: + name: "Code style: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" + + strategy: + matrix: + include: + - mw: 'REL1_39' + php: '8.0' + + runs-on: ubuntu-latest + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl, php-ast + tools: composer + + - uses: actions/checkout@v3 + + - name: Composer install + run: composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + + - run: vendor/bin/phpcs -p -s + + PHPStan: + name: "PHPStan: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" + + strategy: + matrix: + include: + - mw: 'REL1_39' + php: '8.0' + + runs-on: ubuntu-latest + + defaults: + run: + working-directory: mediawiki + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl + tools: composer + + - name: Cache MediaWiki + id: cache-mediawiki + uses: actions/cache@v3 + with: + path: | + mediawiki + !mediawiki/extensions/ + !mediawiki/vendor/ + key: mw_${{ matrix.mw }}-php${{ matrix.php }} + + - name: Cache Composer cache + uses: actions/cache@v3 + with: + path: ~/.composer/cache + key: composer-php${{ matrix.php }} + + - uses: actions/checkout@v3 + with: + path: EarlyCopy + + - name: Install MediaWiki + if: steps.cache-mediawiki.outputs.cache-hit != 'true' + working-directory: ~ + run: bash EarlyCopy/.github/workflows/installWiki.sh ${{ matrix.mw }} + + - uses: actions/checkout@v3 + with: + path: mediawiki/vendor/mediawiki/scss + + - name: Composer update + run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + + - name: Composer install + run: cd vendor/mediawiki/scss && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + + - name: PHPStan + run: cd vendor/mediawiki/scss && php vendor/bin/phpstan analyse + + Psalm: + name: "Psalm: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" + + strategy: + matrix: + include: + - mw: 'REL1_39' + php: '8.0' + + runs-on: ubuntu-latest + + defaults: + run: + working-directory: mediawiki + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl + tools: composer + + - name: Cache MediaWiki + id: cache-mediawiki + uses: actions/cache@v3 + with: + path: | + mediawiki + !mediawiki/extensions/ + !mediawiki/vendor/ + key: mw_${{ matrix.mw }}-php${{ matrix.php }} + + - name: Cache Composer cache + uses: actions/cache@v3 + with: + path: ~/.composer/cache + key: composer-php${{ matrix.php }} + + - uses: actions/checkout@v3 + with: + path: EarlyCopy + + - name: Install MediaWiki + if: steps.cache-mediawiki.outputs.cache-hit != 'true' + working-directory: ~ + run: bash EarlyCopy/.github/workflows/installWiki.sh ${{ matrix.mw }} + + - uses: actions/checkout@v3 + with: + path: mediawiki/vendor/mediawiki/scss + + - name: Composer update + run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + + - name: Composer install + run: cd vendor/mediawiki/scss && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + + - name: Psalm + run: cd vendor/mediawiki/scss && php vendor/bin/psalm --config=psalm.xml --shepherd --stats diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..25d2cbe --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: ci test cs phpunit phpcs stan psalm + +ci: test cs +test: phpunit +cs: phpcs stan psalm + +phpunit: +ifdef filter + php ../../../tests/phpunit/phpunit.php -c phpunit.xml.dist --filter $(filter) +else + php ../../../tests/phpunit/phpunit.php -c phpunit.xml.dist +endif + +phpcs: + vendor/bin/phpcs -p -s --standard=$(shell pwd)/phpcs.xml + +stan: + vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=2G + +stan-baseline: + vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=2G --generate-baseline + +psalm: + vendor/bin/psalm --config=psalm.xml --no-diff + +psalm-baseline: + vendor/bin/psalm --config=psalm.xml --set-baseline=psalm-baseline.xml diff --git a/README.md b/README.md index 96a69d7..d5773db 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ The MediaWiki SCSS library provides a ResourceLoader module capable of compiling ## Requirements -- [PHP] 7.4.3 or later -- [MediaWiki] 1.35 or later +- [PHP] 8.0 or later +- [MediaWiki] 1.39 or later - [Composer] ## Use @@ -123,6 +123,15 @@ version 3][license] (or any later version). ## Release notes +### Version 4.0.0 + +Under development. + +* Raised minimum MediaWiki version from 1.35 to 1.39 +* Raised minimum PHP version from 7.4.3 to 8.0 +* Raised minimum `scssphp` version from 1.10.2 to 1.11.0 +* Modernized coding standards + ### Version 3.0.1 Released on 2022-07-25 diff --git a/composer.json b/composer.json index 089fd74..02db317 100644 --- a/composer.json +++ b/composer.json @@ -36,8 +36,15 @@ "rss": "https://github.com/ProfessionalWiki/SCSS/releases.atom" }, "require": { - "php": ">=7.4.3", - "scssphp/scssphp": "^1.10.2" + "php": ">=8.0", + "scssphp/scssphp": "^1.11.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.8", + "vimeo/psalm": "^5.12.0", + "phpstan/phpstan": "^1.10.15", + "mediawiki/mediawiki-codesniffer": "^v41.0.0", + "slevomat/coding-standard": "^v8.11.1" }, "autoload": { "psr-4": { @@ -47,7 +54,12 @@ }, "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "4.x-dev" + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true } } } diff --git a/docs/doxygen.php b/docs/doxygen.php deleted file mode 100644 index 2145b19..0000000 --- a/docs/doxygen.php +++ /dev/null @@ -1,15 +0,0 @@ - + + src/ + tests/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..47e1a93 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,21 @@ +parameters: + ignoreErrors: + - + message: "#^Call to function is_array\\(\\) with MediaWiki\\\\ResourceLoader\\\\FilePath\\|string will always evaluate to false\\.$#" + count: 1 + path: src/ResourceLoaderSCSSModule.php + + - + message: "#^Elseif branch is unreachable because previous condition is always true\\.$#" + count: 1 + path: src/ResourceLoaderSCSSModule.php + + - + message: "#^Method SCSS\\\\ResourceLoaderSCSSModule\\:\\:collateStyleFilesByPosition\\(\\) should return array\\\\> but returns array\\\\>\\.$#" + count: 1 + path: src/ResourceLoaderSCSSModule.php + + - + message: "#^Offset 'position' on \\*NEVER\\* on left side of \\?\\? always exists and is always null\\.$#" + count: 1 + path: src/ResourceLoaderSCSSModule.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e4cf04f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 9 + paths: + - src + scanDirectories: + - ../../../includes + - ../.. diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..167d6da --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,38 @@ + + + + + is_array( $value ) + + + + + + $collatedFiles[$optionValue] + $collatedFiles[$optionValue] + $collatedFiles[$optionValue] + + + $collatedFiles[$optionValue] + + + $cacheResult + $collatedFiles[$optionValue][] + $optionValue + cacheTriggers]]> + paths]]> + variables]]> + + + $collatedFiles + string[][] + + + $triggerFile !== null + is_int( $key ) + + + ResourceLoaderSCSSModule + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..489af63 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ResourceLoaderSCSSModule.php b/src/ResourceLoaderSCSSModule.php index e5d7118..7372362 100644 --- a/src/ResourceLoaderSCSSModule.php +++ b/src/ResourceLoaderSCSSModule.php @@ -3,7 +3,7 @@ * File containing the ResourceLoaderSCSSModule class * * @copyright 2018 - 2019, Stephan Gambke - * @license GNU General Public License, version 3 (or any later version) + * @license GPL-3.0-or-later * * This file is part of the MediaWiki extension SCSS. * The SCSS extension is free software: you can redistribute it and/or modify @@ -28,11 +28,10 @@ use BagOStuff; use CSSJanus; use Exception; -use ScssPhp\ScssPhp\Compiler; +use MediaWiki\ResourceLoader\Context; +use MediaWiki\ResourceLoader\FileModule; use ObjectCache; -use ResourceLoaderContext; -use ResourceLoaderFileModule; - +use ScssPhp\ScssPhp\Compiler; /** * ResourceLoader module based on local JavaScript/SCSS files. @@ -49,22 +48,36 @@ * * @ingroup SCSS */ -class ResourceLoaderSCSSModule extends ResourceLoaderFileModule { +class ResourceLoaderSCSSModule extends FileModule { - private $styleModulePositions = [ + /** + * @var string[] + */ + private array $styleModulePositions = [ 'beforeFunctions', 'functions', 'afterFunctions', 'beforeVariables', 'variables', 'afterVariables', 'beforeMain', 'main', 'afterMain', ]; - private $cache = null; - private $cacheKey = null; + private ?BagOStuff $cache = null; + private ?string $cacheKey = null; + + /** + * @var array + */ + protected array $variables = []; + + /** + * @var string[] + */ + protected array $paths = []; - protected $variables = []; - protected $paths = []; - protected $cacheTriggers = []; + /** + * @var string[] + */ + protected array $cacheTriggers = []; - protected $styleText = null; + protected ?string $styleText = null; /** * ResourceLoaderSCSSModule constructor. @@ -73,8 +86,7 @@ class ResourceLoaderSCSSModule extends ResourceLoaderFileModule { * @param string|null $localBasePath * @param string|null $remoteBasePath */ - public function __construct( $options = [], $localBasePath = null, $remoteBasePath = null ) { - + public function __construct( array $options = [], ?string $localBasePath = null, ?string $remoteBasePath = null ) { parent::__construct( $options, $localBasePath, $remoteBasePath ); $this->applyOptions( $options ); @@ -83,70 +95,47 @@ public function __construct( $options = [], $localBasePath = null, $remoteBasePa /** * @param mixed[] $options */ - protected function applyOptions( $options ) { - - $mapConfigToLocalVar = [ - 'variables' => 'variables', - 'paths' => 'paths', - 'cacheTriggers' => 'cacheTriggers', - ]; - - foreach ( $mapConfigToLocalVar as $config => $local ) { - if ( isset( $options[ $config ] ) ) { - $this->$local = $options[ $config ]; - } - } + protected function applyOptions( array $options ): void { + $this->variables = $options['variables'] ?? []; + $this->paths = $options['paths'] ?? []; + $this->cacheTriggers = $options['cacheTriggers'] ?? []; } /** - * Get the compiled Bootstrap styles + * Get the compiled SCSS styles * - * @param ResourceLoaderContext $context - * - * @return array + * @return string[] */ - public function getStyles( ResourceLoaderContext $context ) { - + public function getStyles( Context $context ): array { if ( $this->styleText === null ) { - $this->retrieveStylesFromCache( $context ); + } - if ( $this->styleText === null ) { - $this->compileStyles( $context ); - } + if ( $this->styleText === null ) { + $this->compileStyles( $context ); } - return [ 'all' => $this->styleText ]; + return [ 'all' => $this->styleText ?? '' ]; } - /** - * @param ResourceLoaderContext $context - */ - protected function retrieveStylesFromCache( ResourceLoaderContext $context ) { - + protected function retrieveStylesFromCache( Context $context ): void { // Try for cache hit $cacheKey = $this->getCacheKey( $context ); $cacheResult = $this->getCache()->get( $cacheKey ); if ( is_array( $cacheResult ) ) { - - if ( $this->isCacheOutdated( $cacheResult[ 'storetime' ] ) ) { + if ( $this->isCacheOutdated( (int)$cacheResult[ 'storetime' ] ) ) { wfDebug( "SCSS: Cache miss for {$this->getName()}: Cache outdated.\n", 'private' ); } else { - $this->styleText = $cacheResult[ 'styles' ]; + $this->styleText = (string)$cacheResult[ 'styles' ]; wfDebug( "SCSS: Cache hit for {$this->getName()}: Got styles from cache.\n", 'private' ); } - } else { wfDebug( "SCSS: Cache miss for {$this->getName()}: Styles not found in cache.\n", 'private' ); } } - /** - * @return BagOStuff|null - */ - protected function getCache() { - + protected function getCache(): BagOStuff { if ( $this->cache === null ) { $this->cache = ObjectCache::getInstance( $this->getCacheType() ); } @@ -154,8 +143,8 @@ protected function getCache() { return $this->cache; } - private function getCacheType() { - return array_key_exists( 'egScssCacheType', $GLOBALS ) ? $GLOBALS[ 'egScssCacheType' ] : -1; + private function getCacheType(): int { + return array_key_exists( 'egScssCacheType', $GLOBALS ) ? (int)$GLOBALS[ 'egScssCacheType' ] : -1; } /** @@ -163,19 +152,12 @@ private function getCacheType() { * * @param BagOStuff $cache */ - public function setCache( BagOStuff $cache ) { + public function setCache( BagOStuff $cache ): void { $this->cache = $cache; } - /** - * @param ResourceLoaderContext $context - * - * @return string - */ - protected function getCacheKey( ResourceLoaderContext $context ) { - + protected function getCacheKey( Context $context ): string { if ( $this->cacheKey === null ) { - $styles = serialize( $this->styles ); $vars = $this->variables; @@ -196,15 +178,8 @@ protected function getCacheKey( ResourceLoaderContext $context ) { return $this->cacheKey; } - /** - * @param int $cacheStoreTime - * - * @return bool - */ - protected function isCacheOutdated( $cacheStoreTime ) { - + protected function isCacheOutdated( int $cacheStoreTime ): bool { foreach ( $this->cacheTriggers as $triggerFile ) { - if ( $triggerFile !== null && $cacheStoreTime < filemtime( $triggerFile ) ) { return true; } @@ -214,21 +189,14 @@ protected function isCacheOutdated( $cacheStoreTime ) { return false; } - /** - * @param ResourceLoaderContext $context - */ - protected function compileStyles( ResourceLoaderContext $context ) { - + protected function compileStyles( Context $context ): void { $scss = new Compiler(); $scss->setImportPaths( $this->getLocalPath( '' ) ); // Allows inlining of arbitrary files regardless of extension, .css in particular $scss->addImportPath( - - // addImportPath is declared as requiring a string param, but actually also understand callables - /** @scrutinizer ignore-type */ - function ( $path ) { - if ( file_exists( $path ) ) { + static function ( string|callable $path ) { + if ( is_string( $path ) && file_exists( $path ) ) { return $path; } return null; @@ -237,7 +205,6 @@ function ( $path ) { ); try { - $imports = $this->getStyleFilesList(); foreach ( $imports as $key => $import ) { @@ -245,9 +212,9 @@ function ( $path ) { $imports[ $key ] = '@import "' . $path . '";'; } - $scss->setVariables( $this->variables ); + $scss->addVariables( $this->variables ); - $style = $scss->compile( implode( $imports ) ); + $style = $scss->compileString( implode( $imports ) )->getCss(); if ( $this->getFlip( $context ) ) { $style = CSSJanus::transform( $style, true, false ); @@ -256,45 +223,36 @@ function ( $path ) { $this->styleText = $style; $this->updateCache( $context ); - } catch ( Exception $e ) { $this->purgeCache( $context ); wfDebug( $e->getMessage() ); $this->styleText = '/* SCSS compile error: ' . $e->getMessage() . '*/'; } - } - /** - * @param ResourceLoaderContext $context - */ - protected function updateCache( ResourceLoaderContext $context ) { - + protected function updateCache( Context $context ): void { $this->getCache()->set( $this->getCacheKey( $context ), [ 'styles' => $this->styleText, 'storetime' => time() ] ); } - /** - * @param ResourceLoaderContext $context - */ - protected function purgeCache( ResourceLoaderContext $context ) { + protected function purgeCache( Context $context ): void { $this->getCache()->delete( $this->getCacheKey( $context ) ); } /** * @see ResourceLoaderFileModule::supportsURLLoading */ - public function supportsURLLoading() { + public function supportsURLLoading(): bool { return false; } /** - * @return array + * @return string[] */ - protected function getStyleFilesList() { + protected function getStyleFilesList(): array { $styles = $this->collateStyleFilesByPosition(); $imports = []; @@ -310,7 +268,7 @@ protected function getStyleFilesList() { /** * @return string[][] */ - private function collateStyleFilesByPosition() { + private function collateStyleFilesByPosition(): array { $collatedFiles = []; foreach ( $this->styles as $key => $value ) { if ( is_int( $key ) ) { diff --git a/tests/phpunit/ResourceLoaderSCSSModuleTest.php b/tests/phpunit/ResourceLoaderSCSSModuleTest.php index b9a2f0c..c5f7b41 100644 --- a/tests/phpunit/ResourceLoaderSCSSModuleTest.php +++ b/tests/phpunit/ResourceLoaderSCSSModuleTest.php @@ -3,7 +3,7 @@ * File holding the ResourceLoaderSCSSModuleTest class * * @copyright (C) 2018 - 2019, Stephan Gambke - * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License, version 3 (or later) + * @license GPL-3.0-or-later * * This file is part of the MediaWiki extension Bootstrap. * The Bootstrap extension is free software: you can redistribute it and/or @@ -23,14 +23,14 @@ * @ingroup Bootstrap */ - namespace SCSS\Tests; -use SCSS\ResourceLoaderSCSSModule; - use HashBagOStuff; +use PHPUnit\Framework\TestCase; +use SCSS\ResourceLoaderSCSSModule; /** + * @covers \SCSS\ResourceLoaderSCSSModule * @uses \SCSS\ResourceLoaderSCSSModule * * @ingroup Test @@ -41,18 +41,16 @@ * * @since 1.0 */ -class ResourceLoaderSCSSModuleTest extends \PHPUnit_Framework_TestCase { - - public function testCanConstruct() { +class ResourceLoaderSCSSModuleTest extends TestCase { + public function testCanConstruct(): void { $this->assertInstanceOf( '\SCSS\ResourceLoaderSCSSModule', new ResourceLoaderSCSSModule() ); } - public function testGetStyles() { - + public function testGetStyles(): void { $resourceLoaderContext = $this->getMockBuilder( '\ResourceLoaderContext' ) ->disableOriginalConstructor() ->getMock(); @@ -63,8 +61,7 @@ public function testGetStyles() { $this->assertArrayHasKey( 'all', $instance->getStyles( $resourceLoaderContext ) ); } - public function testGetStylesFromPresetCache() { - + public function testGetStylesFromPresetCache(): void { $resourceLoaderContext = $this->getMockBuilder( '\ResourceLoaderContext' ) ->disableOriginalConstructor() ->getMock(); @@ -99,26 +96,24 @@ public function testGetStylesFromPresetCache() { $this->assertEquals( 'foo', $styles['all'] ); } - // FIXME: Re-activate. Needs faulty SCSS file as fixture. - //public function testGetStylesTryCatchExceptionIsThrownByScssParser() { - // - // $resourceLoaderContext = $this->getMockBuilder( '\ResourceLoaderContext' ) - // ->disableOriginalConstructor() - // ->getMock(); - // - // $options = [ - // 'styles' => [ 'Foo"' ] - // ]; - // - // $instance = new ResourceLoaderSCSSModule( $options ); - // $instance->setCache( new HashBagOStuff ); - // - // $result = $instance->getStyles( $resourceLoaderContext ); - // - // $this->assertContains( 'SCSS compile error', $result['all'] ); - //} - - public function testSupportsURLLoading() { + public function testGetStylesTryCatchExceptionIsThrownByScssParser(): void { + + $resourceLoaderContext = $this->getMockBuilder( '\ResourceLoaderContext' ) + ->disableOriginalConstructor() + ->getMock(); + + $options = [ + 'styles' => [ 'Foo"' ] + ]; + + $instance = new ResourceLoaderSCSSModule( $options ); + $instance->setCache( new HashBagOStuff ); + + $result = $instance->getStyles( $resourceLoaderContext ); + $this->assertStringContainsString( 'SCSS compile error', $result['all'] ); + } + + public function testSupportsURLLoading(): void { $instance = new ResourceLoaderSCSSModule(); $this->assertFalse( $instance->supportsURLLoading() ); }