Tim Starling has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/394905 )

Change subject: Add a tool to modify and standardize file headers
......................................................................

Add a tool to modify and standardize file headers

See the core commit Ie0cea0ef5 for the details of the resulting changes.

Change-Id: I4689786bb87ac8fe86564e148d0997cb578dbb47
---
A bin/fixFileHeader.php
M composer.json
A src/FileHeaderFixer.php
3 files changed, 386 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/tools/namespaceizer 
refs/changes/05/394905/1

diff --git a/bin/fixFileHeader.php b/bin/fixFileHeader.php
new file mode 100644
index 0000000..0334ac9
--- /dev/null
+++ b/bin/fixFileHeader.php
@@ -0,0 +1,29 @@
+<?php
+
+use MediaWiki\Tool\Namespaceizer\FileHeaderFixer;
+
+if ( PHP_SAPI !== 'cli' ) {
+       exit( 1 );
+}
+
+require __DIR__ .'/../vendor/autoload.php';
+
+function fixFileHeader( $argv ) {
+       if ( count( $argv ) < 2 ) {
+               echo "Usage: {$argv[0]} <file> ...\n";
+               exit( 1 );
+       }
+       $files = array_slice( $argv, 1 );
+
+       foreach ( $files as $fileName ) {
+               $input = file_get_contents( $fileName );
+               $result = FileHeaderFixer::fix( $input, function ( $line, $msg 
) use ( $fileName ) {
+                       echo "$fileName:$line: $msg\n";
+               } );
+               if ( $result !== false && $input !== $result ) {
+                       file_put_contents( $fileName, $result );
+               }
+       }
+}
+
+fixFileHeader( $argv );
diff --git a/composer.json b/composer.json
index 1a2d50d..6fb2e15 100644
--- a/composer.json
+++ b/composer.json
@@ -5,5 +5,10 @@
                "psr-4": {
                        "MediaWiki\\Tool\\Namespaceizer\\": "src/"
                }
+       },
+       "minimum-stability": "alpha",
+       "prefer-stable": true,
+       "require": {
+               "nikic/php-parser": "~4.0"
        }
 }
diff --git a/src/FileHeaderFixer.php b/src/FileHeaderFixer.php
new file mode 100644
index 0000000..a7affaa
--- /dev/null
+++ b/src/FileHeaderFixer.php
@@ -0,0 +1,352 @@
+<?php
+
+namespace MediaWiki\Tool\Namespaceizer;
+
+use PhpParser\Lexer;
+use PhpParser\NodeTraverser;
+use PhpParser\NodeVisitor;
+use PhpParser\Parser;
+use PhpParser\PrettyPrinter;
+use PhpParser\Comment;
+
+class FileHeaderFixer {
+       private $text;
+
+       public static function fix( $text, $errorCallback ) {
+               $fixer = new self( $text, $errorCallback );
+               try {
+                       return $fixer->execute();
+               } catch ( Error $error ) {
+                       return false;
+               }
+       }
+
+       private function __construct( $text, $errorCallback ) {
+               $this->text = $text;
+               $this->errorCallback = $errorCallback;
+       }
+
+       private function execute() {
+               $lexer = new Lexer\Emulative( [
+                       'usedAttributes' => [
+                               'comments',
+                               'startLine', 'endLine',
+                               'startTokenPos', 'endTokenPos',
+                       ],
+               ] );
+               $parser = new Parser\Php7( $lexer );
+               $oldAst = $parser->parse( $this->text );
+
+               $traverser = new NodeTraverser;
+               $traverser->addVisitor( new NodeVisitor\CloningVisitor );
+               $newAst = $traverser->traverse( $oldAst );
+
+               if ( count( $newAst ) < 1 ) {
+                       $this->fatal( 0, "empty file" );
+               }
+
+               $nonClass = $this->findNonClassCode( $newAst );
+               if ( $nonClass ) {
+                       $this->fatal( $nonClass->getLine(), "non-class code 
found" );
+               }
+
+               $firstNode = $newAst[0];
+               $comments = $firstNode->getAttribute( 'comments' );
+               if ( !count( $comments ) ) {
+                       $this->error( $firstNode->getLine(), "First node has no 
comments" );
+                       return $this->text;
+               }
+
+               $commentText = $comments[0]->getText();
+               $parts = $this->splitComment( $firstNode->getLine(), 
$commentText );
+
+               if ( isset( $parts['start'] ) ) {
+                       $parts['start'] = $this->removeCreationDate( 
$parts['start'] );
+               }
+
+               $ingroup = false;
+               if ( isset( $parts['end'] ) ) {
+                       $ingroup = $this->getAnnotation( $parts['end'], 
'ingroup' );
+                       $parts['end'] = $this->removeAnnotation( $parts['end'], 
'ingroup' );
+               }
+               if ( isset( $parts['license'] ) ) {
+                       $newLicenseComment = "/*\n" . $parts['license'] . " 
*/\n";
+               } else {
+                       $newLicenseComment = null;
+               }
+
+               if ( isset( $parts['license'] ) && ( isset( $parts['start'] ) 
|| isset( $parts['end'] ) ) ) {
+                       $newFileComment = "/**\n";
+                       if ( isset( $parts['start'] ) ) {
+                               $newFileComment .= $parts['start'];
+                       }
+                       if ( isset( $parts['end'] ) ) {
+                               $newFileComment .= $parts['end'];
+                       }
+                       $newFileComment .= " */";
+
+                       $newComments = [];
+                       if ( !$this->isPracticallyEmpty( $newFileComment ) ) {
+                               $newComments[] = new Comment( $newFileComment );
+                       }
+                       $newComments[] = new Comment( $newLicenseComment );
+                       array_splice( $comments, 0, 1, $newComments );
+               } elseif ( isset( $parts['license'] ) ) {
+                       $comments[0] = new Comment( $newLicenseComment );
+               }
+
+               $firstNode->setAttribute( 'comments', $comments );
+
+               if ( $ingroup !== false ) {
+                       $this->forEachClass( $newAst, function ( $node ) use ( 
$ingroup ) {
+                               $this->addAnnotation( $node, 'ingroup', 
$ingroup );
+                       } );
+               }
+
+               $printer = new PrettyPrinter\Standard();
+               return $printer->printFormatPreserving( $newAst, $oldAst, 
$lexer->getTokens() );
+       }
+
+       /**
+        * Split a string into an array of lines, assuming that the final line 
is
+        * terminated by an LF character
+        */
+       private function explodeLines( $text ) {
+               $lines = explode( "\n", $text );
+               if ( end( $lines ) === '' ) {
+                       array_pop( $lines );
+               }
+               return $lines;
+       }
+
+       /**
+        * Reassemble a string from an array of lines, adding a trailing line 
break
+        */
+       private function implodeLines( $lines ) {
+               return $lines ? implode( "\n", $lines ) . "\n" : '';
+       }
+
+       private function splitComment( $startLine, $text ) {
+               $lines = $this->explodeLines( $text );
+               $n = count( $lines );
+               if ( $lines[0] !== '/**' ) {
+                       $this->fatal( $startLine, "File starts with non-doc 
comment" );
+               }
+               if ( $lines[$n - 1] !== ' */' ) {
+                       $this->fatal( $startLine + $n - 1, "Unexpected comment 
end" );
+               }
+
+               $licenseStart = $licenseEnd = null;
+               for ( $i = 1; $i < $n - 1; $i++ ) {
+                       if ( strpos( $lines[$i], 'This program is free 
software' ) !== false ) {
+                               // Include copyright notices immediately before 
the license in the license section
+                               if ( $i >= 3 && $lines[$i - 1] === ' *'
+                                       && $this->isCopyright( $lines[$i - 2] )
+                               ) {
+                                       $i -= 2;
+                               }
+                               while ( $i >= 2 && $this->isCopyright( 
$lines[$i - 1] ) ) {
+                                       $i--;
+                               }
+                               $licenseStart = $i;
+                               break;
+                       }
+               }
+               if ( $licenseStart !== null ) {
+                       for ( ; $i < $n - 1; $i++ ) {
+                               if ( strpos( $lines[$i], '51 Franklin Street' ) 
!== false ) {
+                                       if ( strpos( $lines[$i + 1], 
'www.gnu.org' ) !== false ) {
+                                               $i++;
+                                       }
+                                       $licenseEnd = $i;
+                                       if ( $lines[$i + 1] === ' *' ) {
+                                               $i++;
+                                       }
+                                       $footerStart = $i + 1;
+                                       break;
+                               }
+                       }
+               }
+
+               $parts = [];
+               if ( $licenseStart === null ) {
+                       $parts['start'] = $this->implodeLines(
+                               array_slice( $lines, 1, $n - 2 ) );
+               } else {
+                       if ( $licenseStart > 1 ) {
+                               $parts['start'] = $this->implodeLines(
+                                       array_slice( $lines, 1, $licenseStart - 
1 ) );
+                       }
+                       if ( $licenseEnd === null ) {
+                               $parts['license'] = $this->implodeLines(
+                                       array_slice( $lines, $licenseStart, $n 
- $licenseStart - 1 ) );
+                       } else {
+                               $parts['license'] = $this->implodeLines(
+                                       array_slice( $lines, $licenseStart, 
$licenseEnd - $licenseStart + 1 ) );
+                               $parts['end'] = $this->implodeLines(
+                                       array_slice( $lines, $footerStart, $n - 
$footerStart - 1 ) );
+                       }
+               }
+
+               return $parts;
+       }
+
+       private function isCopyright( $line ) {
+               return !!preg_match( '!' .
+                       'copyright|' . // Regular
+                       'https://www.mediawiki.org/\s*$|' . // Brionic
+                       'You may copy this code freely' . // Dairikite
+                       '!i', $line );
+       }
+
+       private function getAnnotation( $text, $name ) {
+               foreach ( explode( "\n", $text ) as $line ) {
+                       if ( preg_match( "/@" . preg_quote( $name, '/' ) . 
'\s+(.*)$/', $line, $m ) ) {
+                               return $m[1];
+                       }
+               }
+               return false;
+       }
+
+       private function removeAnnotation( $text, $name ) {
+               $lines = $this->explodeLines( $text );
+               $removeStart = null;
+               $removeLength = 1;
+               foreach ( $lines as $i => $line ) {
+                       $pos = strpos( $line, "@$name" );
+                       if ( $pos !== false ) {
+                               $removeStart = $i;
+                               if ( $i > 0 && $i < count( $lines ) - 2
+                                       && $line[$i - 1] === ' *'
+                                       && $line[$i + 1] === ' *'
+                               ) {
+                                       $removeLength = 2;
+                               }
+                               break;
+                       }
+               }
+
+               if ( $removeStart !== null ) {
+                       array_splice( $lines, $removeStart, $removeLength );
+               }
+               return $this->implodeLines( $lines );
+       }
+
+       private function addAnnotation( $node, $name, $value ) {
+               $comments = $node->getComments();
+               if ( $comments ) {
+                       $lastIndex = count( $comments ) - 1;
+                       $lastComment = (string)$comments[$lastIndex];
+
+                       if ( strpos( $lastComment, "@$name $value" ) !== false 
) {
+                               // Don't add duplicate
+                               return;
+                       }
+
+                       if ( strpos( $lastComment, '@file' ) !== false
+                               || strpos( $lastComment, 'This program is free 
software' ) !== false
+                       ) {
+                               $comments[] = new Comment( "/**\n * @$name 
$value\n */" );
+                       } else {
+                               $lastComment = str_replace( "\n */", "\n * 
@$name $value\n */", $lastComment );
+                               array_splice( $comments, $lastIndex, 1, [
+                                       new Comment( $lastComment )
+                               ] );
+                       }
+               } else {
+                       $comments = [
+                               new Comment( "/**\n * @$name $value\n */" )
+                       ];
+               }
+               $node->setAttribute( 'comments', $comments );
+       }
+
+       /**
+        * Determine if a full comment string has nothing useful in it
+        */
+       private function isPracticallyEmpty( $text ) {
+               foreach ( $this->explodeLines( $text ) as $line ) {
+                       if ( $line !== ''
+                               && $line !== ' *'
+                               && $line !== ' * @file'
+                               && $line !== '/**'
+                               && $line !== ' */'
+                       ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       private function removeCreationDate( $text ) {
+               $lines = $this->explodeLines( $text );
+               $n = count( $lines );
+               foreach ( $lines as $i => $line ) {
+                       if ( preg_match( '/^ \* Created on .*20\d\d$/', $line ) 
) {
+                               // Comments of this kind usually have an excess 
of empty lines
+                               // preceding them. Remove those lines.
+                               $startBlock = $i;
+                               while ( $startBlock > 0 && $lines[$startBlock - 
1] === ' *' ) {
+                                       $startBlock--;
+                               }
+
+                               array_splice( $lines, $startBlock, $i - 
$startBlock + 1, [] );
+                               return $this->implodeLines( $lines );
+                       }
+               }
+               return $text;
+       }
+
+       private function error( $line, $msg ) {
+               if ( $this->errorCallback ) {
+                       call_user_func( $this->errorCallback, $line, $msg );
+               }
+       }
+
+       private function fatal( $line, $msg ) {
+               $this->error( $line, $msg );
+               throw new Error( $msg );
+       }
+
+       private function info( $msg ) {
+               fwrite( STDERR, "$msg\n" );
+       }
+
+       private function findNonClassCode( $ast ) {
+               foreach ( $ast as $node ) {
+                       $type = $node->getType();
+                       if ( in_array( $type, [
+                               'Stmt_Class',
+                               'Stmt_Interface',
+                               'Stmt_Trait',
+                               'Stmt_Namespace',
+                               'Stmt_Use'] )
+                       ) {
+                               continue;
+                       }
+                       if ( $type === 'Stmt_Expression' ) {
+                               if ( $node->expr->getType() === 'Expr_FuncCall'
+                                       && !empty( $node->args )
+                                       && $node->args[0]->getType() === 
'Scalar_String'
+                                       && $node->args[0]->value === 
'class_alias'
+                               ) {
+                                       continue;
+                               }
+                       }
+                       return $node;
+               }
+               return false;
+       }
+
+       private function forEachClass( $nodeList, $callback ) {
+               foreach ( $nodeList as $node ) {
+                       if ( in_array( $node->getType(), [
+                               'Stmt_Class',
+                               'Stmt_Interface',
+                               'Stmt_Trait' ] )
+                       ) {
+                               $callback( $node );
+                       }
+               }
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/394905
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I4689786bb87ac8fe86564e148d0997cb578dbb47
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/tools/namespaceizer
Gerrit-Branch: master
Gerrit-Owner: Tim Starling <tstarl...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to