PHP_CodeSniffer
[ class tree: PHP_CodeSniffer ] [ index: PHP_CodeSniffer ] [ all elements ]

Source for file Fixer.php

Documentation is available at Fixer.php

  1. <?php
  2. /**
  3.  * A helper class for fixing errors.
  4.  *
  5.  * Provides helper functions that act upon a token array and modify the file
  6.  * content.
  7.  *
  8.  * @author    Greg Sherwood <gsherwood@squiz.net>
  9.  * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
  10.  * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  11.  */
  12.  
  13. namespace PHP_CodeSniffer;
  14.  
  15. use PHP_CodeSniffer\Files\File;
  16. use PHP_CodeSniffer\Util\Common;
  17.  
  18. class Fixer
  19. {
  20.  
  21.     /**
  22.      * Is the fixer enabled and fixing a file?
  23.      *
  24.      * Sniffs should check this value to ensure they are not
  25.      * doing extra processing to prepare for a fix when fixing is
  26.      * not required.
  27.      *
  28.      * @var boolean 
  29.      */
  30.     public $enabled = false;
  31.  
  32.     /**
  33.      * The number of times we have looped over a file.
  34.      *
  35.      * @var integer 
  36.      */
  37.     public $loops = 0;
  38.  
  39.     /**
  40.      * The file being fixed.
  41.      *
  42.      * @var \PHP_CodeSniffer\Files\File 
  43.      */
  44.     private $currentFile = null;
  45.  
  46.     /**
  47.      * The list of tokens that make up the file contents.
  48.      *
  49.      * This is a simplified list which just contains the token content and nothing
  50.      * else. This is the array that is updated as fixes are made, not the file's
  51.      * token array. Imploding this array will give you the file content back.
  52.      *
  53.      * @var array<int, string>
  54.      */
  55.     private $tokens = array();
  56.  
  57.     /**
  58.      * A list of tokens that have already been fixed.
  59.      *
  60.      * We don't allow the same token to be fixed more than once each time
  61.      * through a file as this can easily cause conflicts between sniffs.
  62.      *
  63.      * @var int[] 
  64.      */
  65.     private $fixedTokens = array();
  66.  
  67.     /**
  68.      * The last value of each fixed token.
  69.      *
  70.      * If a token is being "fixed" back to its last value, the fix is
  71.      * probably conflicting with another.
  72.      *
  73.      * @var array<int, string>
  74.      */
  75.     private $oldTokenValues = array();
  76.  
  77.     /**
  78.      * A list of tokens that have been fixed during a changeset.
  79.      *
  80.      * All changes in changeset must be able to be applied, or else
  81.      * the entire changeset is rejected.
  82.      *
  83.      * @var array 
  84.      */
  85.     private $changeset = array();
  86.  
  87.     /**
  88.      * Is there an open changeset.
  89.      *
  90.      * @var boolean 
  91.      */
  92.     private $inChangeset = false;
  93.  
  94.     /**
  95.      * Is the current fixing loop in conflict?
  96.      *
  97.      * @var boolean 
  98.      */
  99.     private $inConflict = false;
  100.  
  101.     /**
  102.      * The number of fixes that have been performed.
  103.      *
  104.      * @var integer 
  105.      */
  106.     private $numFixes = 0;
  107.  
  108.  
  109.     /**
  110.      * Starts fixing a new file.
  111.      *
  112.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being fixed.
  113.      *
  114.      * @return void 
  115.      */
  116.     public function startFile(File $phpcsFile)
  117.     {
  118.         $this->currentFile $phpcsFile;
  119.         $this->numFixes    = 0;
  120.         $this->fixedTokens = array();
  121.  
  122.         $tokens       $phpcsFile->getTokens();
  123.         $this->tokens = array();
  124.         foreach ($tokens as $index => $token{
  125.             if (isset($token['orig_content']=== true{
  126.                 $this->tokens[$index$token['orig_content'];
  127.             else {
  128.                 $this->tokens[$index$token['content'];
  129.             }
  130.         }
  131.  
  132.     }//end startFile()
  133.  
  134.  
  135.     /**
  136.      * Attempt to fix the file by processing it until no fixes are made.
  137.      *
  138.      * @return boolean 
  139.      */
  140.     public function fixFile()
  141.     {
  142.         $fixable $this->currentFile->getFixableCount();
  143.         if ($fixable === 0{
  144.             // Nothing to fix.
  145.             return false;
  146.         }
  147.  
  148.         $stdin = false;
  149.         if (empty($this->currentFile->config->files=== true{
  150.             $stdin = true;
  151.         }
  152.  
  153.         $this->enabled = true;
  154.  
  155.         $this->loops = 0;
  156.         while ($this->loops < 50{
  157.             ob_start();
  158.  
  159.             // Only needed once file content has changed.
  160.             $contents $this->getContents();
  161.  
  162.             if (PHP_CODESNIFFER_VERBOSITY > 2{
  163.                 @ob_end_clean();
  164.                 echo '---START FILE CONTENT---'.PHP_EOL;
  165.                 $lines explode($this->currentFile->eolChar$contents);
  166.                 $max   strlen(count($lines));
  167.                 foreach ($lines as $lineNum => $line{
  168.                     $lineNum++;
  169.                     echo str_pad($lineNum$max' 'STR_PAD_LEFT).'|'.$line.PHP_EOL;
  170.                 }
  171.  
  172.                 echo '--- END FILE CONTENT ---'.PHP_EOL;
  173.                 ob_start();
  174.             }
  175.  
  176.             $this->inConflict = false;
  177.             $this->currentFile->ruleset->populateTokenListeners();
  178.             $this->currentFile->setContent($contents);
  179.             $this->currentFile->process();
  180.             ob_end_clean();
  181.  
  182.             $this->loops++;
  183.  
  184.             if (PHP_CODESNIFFER_CBF === true && PHP_CODESNIFFER_VERBOSITY > 0{
  185.                 echo "\r".str_repeat(' '80)."\r";
  186.                 echo "\t=> Fixing file: $this->numFixes/$fixable violations remaining [made $this->loops pass";
  187.                 if ($this->loops > 1{
  188.                     echo 'es';
  189.                 }
  190.  
  191.                 echo ']... ';
  192.             }
  193.  
  194.             if ($this->numFixes === 0 && $this->inConflict === false{
  195.                 // Nothing left to do.
  196.                 break;
  197.             else if (PHP_CODESNIFFER_VERBOSITY > 1{
  198.                 echo "\t* fixed $this->numFixes violations, starting loop ".($this->loops + 1).' *'.PHP_EOL;
  199.             }
  200.         }//end while
  201.  
  202.         $this->enabled = false;
  203.  
  204.         if ($this->numFixes > 0{
  205.             if (PHP_CODESNIFFER_VERBOSITY > 1{
  206.                 @ob_end_clean();
  207.                 echo "\t*** Reached maximum number of loops with $this->numFixes violations left unfixed ***".PHP_EOL;
  208.                 ob_start();
  209.             }
  210.  
  211.             return false;
  212.         }
  213.  
  214.         return true;
  215.  
  216.     }//end fixFile()
  217.  
  218.  
  219.     /**
  220.      * Generates a text diff of the original file and the new content.
  221.      *
  222.      * @param string  $filePath Optional file path to diff the file against.
  223.      *                           If not specified, the original version of the
  224.      *                           file will be used.
  225.      * @param boolean $colors   Print colored output or not.
  226.      *
  227.      * @return string 
  228.      */
  229.     public function generateDiff($filePath=null$colors=true)
  230.     {
  231.         if ($filePath === null{
  232.             $filePath $this->currentFile->getFilename();
  233.         }
  234.  
  235.         $cwd      getcwd().DIRECTORY_SEPARATOR;
  236.         $filename str_replace($cwd''$filePath);
  237.         $contents $this->getContents();
  238.  
  239.         $tempName  tempnam(sys_get_temp_dir()'phpcs-fixer');
  240.         $fixedFile fopen($tempName'w');
  241.         fwrite($fixedFile$contents);
  242.  
  243.         // We must use something like shell_exec() because whitespace at the end
  244.         // of lines is critical to diff files.
  245.         $cmd  = "diff -u -L\"$filename\" -LPHP_CodeSniffer \"$filename\" \"$tempName\"";
  246.         $diff shell_exec($cmd);
  247.  
  248.         fclose($fixedFile);
  249.         if (is_file($tempName=== true{
  250.             unlink($tempName);
  251.         }
  252.  
  253.         if ($colors === false{
  254.             return $diff;
  255.         }
  256.  
  257.         $diffLines explode(PHP_EOL$diff);
  258.         if (count($diffLines=== 1{
  259.             // Seems to be required for cygwin.
  260.             $diffLines explode("\n"$diff);
  261.         }
  262.  
  263.         $diff = array();
  264.         foreach ($diffLines as $line{
  265.             if (isset($line[0]=== true{
  266.                 switch ($line[0]{
  267.                 case '-':
  268.                     $diff[= "\033[31m$line\033[0m";
  269.                     break;
  270.                 case '+':
  271.                     $diff[= "\033[32m$line\033[0m";
  272.                     break;
  273.                 default:
  274.                     $diff[$line;
  275.                 }
  276.             }
  277.         }
  278.  
  279.         $diff implode(PHP_EOL$diff);
  280.  
  281.         return $diff;
  282.  
  283.     }//end generateDiff()
  284.  
  285.  
  286.     /**
  287.      * Get a count of fixes that have been performed on the file.
  288.      *
  289.      * This value is reset every time a new file is started, or an existing
  290.      * file is restarted.
  291.      *
  292.      * @return int 
  293.      */
  294.     public function getFixCount()
  295.     {
  296.         return $this->numFixes;
  297.  
  298.     }//end getFixCount()
  299.  
  300.  
  301.     /**
  302.      * Get the current content of the file, as a string.
  303.      *
  304.      * @return string 
  305.      */
  306.     public function getContents()
  307.     {
  308.         $contents implode($this->tokens);
  309.         return $contents;
  310.  
  311.     }//end getContents()
  312.  
  313.  
  314.     /**
  315.      * Get the current fixed content of a token.
  316.      *
  317.      * This function takes changesets into account so should be used
  318.      * instead of directly accessing the token array.
  319.      *
  320.      * @param int $stackPtr The position of the token in the token stack.
  321.      *
  322.      * @return string 
  323.      */
  324.     public function getTokenContent($stackPtr)
  325.     {
  326.         if ($this->inChangeset === true
  327.             && isset($this->changeset[$stackPtr]=== true
  328.         {
  329.             return $this->changeset[$stackPtr];
  330.         else {
  331.             return $this->tokens[$stackPtr];
  332.         }
  333.  
  334.     }//end getTokenContent()
  335.  
  336.  
  337.     /**
  338.      * Start recording actions for a changeset.
  339.      *
  340.      * @return void 
  341.      */
  342.     public function beginChangeset()
  343.     {
  344.         if ($this->inConflict === true{
  345.             return false;
  346.         }
  347.  
  348.         if (PHP_CODESNIFFER_VERBOSITY > 1{
  349.             $bt    debug_backtrace();
  350.             $sniff $bt[1]['class'];
  351.             $line  $bt[0]['line'];
  352.  
  353.             @ob_end_clean();
  354.             echo "\t=> Changeset started by $sniff (line $line)".PHP_EOL;
  355.             ob_start();
  356.         }
  357.  
  358.         $this->changeset   = array();
  359.         $this->inChangeset = true;
  360.  
  361.     }//end beginChangeset()
  362.  
  363.  
  364.     /**
  365.      * Stop recording actions for a changeset, and apply logged changes.
  366.      *
  367.      * @return boolean 
  368.      */
  369.     public function endChangeset()
  370.     {
  371.         if ($this->inConflict === true{
  372.             return false;
  373.         }
  374.  
  375.         $this->inChangeset = false;
  376.  
  377.         $success = true;
  378.         $applied = array();
  379.         foreach ($this->changeset as $stackPtr => $content{
  380.             $success $this->replaceToken($stackPtr$content);
  381.             if ($success === false{
  382.                 break;
  383.             else {
  384.                 $applied[$stackPtr;
  385.             }
  386.         }
  387.  
  388.         if ($success === false{
  389.             // Rolling back all changes.
  390.             foreach ($applied as $stackPtr{
  391.                 $this->revertToken($stackPtr);
  392.             }
  393.  
  394.             if (PHP_CODESNIFFER_VERBOSITY > 1{
  395.                 @ob_end_clean();
  396.                 echo "\t=> Changeset failed to apply".PHP_EOL;
  397.                 ob_start();
  398.             }
  399.         else if (PHP_CODESNIFFER_VERBOSITY > 1{
  400.             $fixes count($this->changeset);
  401.             @ob_end_clean();
  402.             echo "\t=> Changeset ended: $fixes changes applied".PHP_EOL;
  403.             ob_start();
  404.         }
  405.  
  406.         $this->changeset = array();
  407.  
  408.     }//end endChangeset()
  409.  
  410.  
  411.     /**
  412.      * Stop recording actions for a changeset, and discard logged changes.
  413.      *
  414.      * @return void 
  415.      */
  416.     public function rollbackChangeset()
  417.     {
  418.         $this->inChangeset = false;
  419.         $this->inConflict  = false;
  420.  
  421.         if (empty($this->changeset=== false{
  422.             if (PHP_CODESNIFFER_VERBOSITY > 1{
  423.                 $bt debug_backtrace();
  424.                 if ($bt[1]['class'=== 'PHP_CodeSniffer_Fixer'{
  425.                     $sniff $bt[2]['class'];
  426.                     $line  $bt[1]['line'];
  427.                 else {
  428.                     $sniff $bt[1]['class'];
  429.                     $line  $bt[0]['line'];
  430.                 }
  431.  
  432.                 $numChanges count($this->changeset);
  433.  
  434.                 @ob_end_clean();
  435.                 echo "\t\tR: $sniff (line $line) rolled back the changeset ($numChanges changes)".PHP_EOL;
  436.                 echo "\t=> Changeset rolled back".PHP_EOL;
  437.                 ob_start();
  438.             }
  439.  
  440.             $this->changeset = array();
  441.         }//end if
  442.  
  443.     }//end rollbackChangeset()
  444.  
  445.  
  446.     /**
  447.      * Replace the entire contents of a token.
  448.      *
  449.      * @param int    $stackPtr The position of the token in the token stack.
  450.      * @param string $content  The new content of the token.
  451.      *
  452.      * @return bool If the change was accepted.
  453.      */
  454.     public function replaceToken($stackPtr$content)
  455.     {
  456.         if ($this->inConflict === true{
  457.             return false;
  458.         }
  459.  
  460.         if ($this->inChangeset === false
  461.             && isset($this->fixedTokens[$stackPtr]=== true
  462.         {
  463.             $indent "\t";
  464.             if (empty($this->changeset=== false{
  465.                 $indent .= "\t";
  466.             }
  467.  
  468.             if (PHP_CODESNIFFER_VERBOSITY > 1{
  469.                 @ob_end_clean();
  470.                 echo "$indent* token $stackPtr has already been modified, skipping *".PHP_EOL;
  471.                 ob_start();
  472.             }
  473.  
  474.             return false;
  475.         }
  476.  
  477.         if (PHP_CODESNIFFER_VERBOSITY > 1{
  478.             $bt debug_backtrace();
  479.             if ($bt[1]['class'=== 'PHP_CodeSniffer_Fixer'{
  480.                 $sniff $bt[2]['class'];
  481.                 $line  $bt[1]['line'];
  482.             else {
  483.                 $sniff $bt[1]['class'];
  484.                 $line  $bt[0]['line'];
  485.             }
  486.  
  487.             $tokens     $this->currentFile->getTokens();
  488.             $type       $tokens[$stackPtr]['type'];
  489.             $oldContent = Common::prepareForOutput($this->tokens[$stackPtr]);
  490.             $newContent = Common::prepareForOutput($content);
  491.             if (trim($this->tokens[$stackPtr]=== '' && isset($this->tokens[($stackPtr + 1)]=== true{
  492.                 // Add some context for whitespace only changes.
  493.                 $append      = Common::prepareForOutput($this->tokens[($stackPtr + 1)]);
  494.                 $oldContent .= $append;
  495.                 $newContent .= $append;
  496.             }
  497.         }//end if
  498.  
  499.         if ($this->inChangeset === true{
  500.             $this->changeset[$stackPtr$content;
  501.  
  502.             if (PHP_CODESNIFFER_VERBOSITY > 1{
  503.                 @ob_end_clean();
  504.                 echo "\t\tQ: $sniff (line $line) replaced token $stackPtr ($type) \"$oldContent\" => \"$newContent\"".PHP_EOL;
  505.                 ob_start();
  506.             }
  507.  
  508.             return true;
  509.         }
  510.  
  511.         if (isset($this->oldTokenValues[$stackPtr]=== false{
  512.             $this->oldTokenValues[$stackPtr= array(
  513.                                                 'curr' => $content,
  514.                                                 'prev' => $this->tokens[$stackPtr],
  515.                                                 'loop' => $this->loops,
  516.                                                );
  517.         else {
  518.             if ($this->oldTokenValues[$stackPtr]['prev'=== $content
  519.                 && $this->oldTokenValues[$stackPtr]['loop'=== ($this->loops - 1)
  520.             {
  521.                 if (PHP_CODESNIFFER_VERBOSITY > 1{
  522.                     $indent "\t";
  523.                     if (empty($this->changeset=== false{
  524.                         $indent .= "\t";
  525.                     }
  526.  
  527.                     $loop $this->oldTokenValues[$stackPtr]['loop'];
  528.  
  529.                     @ob_end_clean();
  530.                     echo "$indent**** $sniff (line $line) has possible conflict with another sniff on loop $loop; caused by the following change ****".PHP_EOL;
  531.                     echo "$indent**** replaced token $stackPtr ($type) \"$oldContent\" => \"$newContent\" ****".PHP_EOL;
  532.                 }
  533.  
  534.                 if ($this->oldTokenValues[$stackPtr]['loop'>= ($this->loops - 1)) {
  535.                     $this->inConflict = true;
  536.                     if (PHP_CODESNIFFER_VERBOSITY > 1{
  537.                         echo "$indent**** ignoring all changes until next loop ****".PHP_EOL;
  538.                     }
  539.                 }
  540.  
  541.                 if (PHP_CODESNIFFER_VERBOSITY > 1{
  542.                     ob_start();
  543.                 }
  544.  
  545.                 return false;
  546.             }//end if
  547.  
  548.             $this->oldTokenValues[$stackPtr]['prev'$this->oldTokenValues[$stackPtr]['curr'];
  549.             $this->oldTokenValues[$stackPtr]['curr'$content;
  550.             $this->oldTokenValues[$stackPtr]['loop'$this->loops;
  551.         }//end if
  552.  
  553.         $this->fixedTokens[$stackPtr$this->tokens[$stackPtr];
  554.         $this->tokens[$stackPtr]      $content;
  555.         $this->numFixes++;
  556.  
  557.         if (PHP_CODESNIFFER_VERBOSITY > 1{
  558.             $indent "\t";
  559.             if (empty($this->changeset=== false{
  560.                 $indent .= "\tA: ";
  561.             }
  562.  
  563.             @ob_end_clean();
  564.             echo "$indent$sniff (line $line) replaced token $stackPtr ($type) \"$oldContent\" => \"$newContent\"".PHP_EOL;
  565.             ob_start();
  566.         }
  567.  
  568.         return true;
  569.  
  570.     }//end replaceToken()
  571.  
  572.  
  573.     /**
  574.      * Reverts the previous fix made to a token.
  575.      *
  576.      * @param int $stackPtr The position of the token in the token stack.
  577.      *
  578.      * @return bool If a change was reverted.
  579.      */
  580.     public function revertToken($stackPtr)
  581.     {
  582.         if (isset($this->fixedTokens[$stackPtr]=== false{
  583.             return false;
  584.         }
  585.  
  586.         if (PHP_CODESNIFFER_VERBOSITY > 1{
  587.             $bt debug_backtrace();
  588.             if ($bt[1]['class'=== 'PHP_CodeSniffer_Fixer'{
  589.                 $sniff $bt[2]['class'];
  590.                 $line  $bt[1]['line'];
  591.             else {
  592.                 $sniff $bt[1]['class'];
  593.                 $line  $bt[0]['line'];
  594.             }
  595.  
  596.             $tokens     $this->currentFile->getTokens();
  597.             $type       $tokens[$stackPtr]['type'];
  598.             $oldContent = Common::prepareForOutput($this->tokens[$stackPtr]);
  599.             $newContent = Common::prepareForOutput($this->fixedTokens[$stackPtr]);
  600.             if (trim($this->tokens[$stackPtr]=== '' && isset($tokens[($stackPtr + 1)]=== true{
  601.                 // Add some context for whitespace only changes.
  602.                 $append      = Common::prepareForOutput($this->tokens[($stackPtr + 1)]);
  603.                 $oldContent .= $append;
  604.                 $newContent .= $append;
  605.             }
  606.         }//end if
  607.  
  608.         $this->tokens[$stackPtr$this->fixedTokens[$stackPtr];
  609.         unset($this->fixedTokens[$stackPtr]);
  610.         $this->numFixes--;
  611.  
  612.         if (PHP_CODESNIFFER_VERBOSITY > 1{
  613.             $indent "\t";
  614.             if (empty($this->changeset=== false{
  615.                 $indent .= "\tR: ";
  616.             }
  617.  
  618.             @ob_end_clean();
  619.             echo "$indent$sniff (line $line) reverted token $stackPtr ($type) \"$oldContent\" => \"$newContent\"".PHP_EOL;
  620.             ob_start();
  621.         }
  622.  
  623.         return true;
  624.  
  625.     }//end revertToken()
  626.  
  627.  
  628.     /**
  629.      * Replace the content of a token with a part of its current content.
  630.      *
  631.      * @param int $stackPtr The position of the token in the token stack.
  632.      * @param int $start    The first character to keep.
  633.      * @param int $length   The number of chacters to keep. If NULL, the content of
  634.      *                       the token from $start to the end of the content is kept.
  635.      *
  636.      * @return bool If the change was accepted.
  637.      */
  638.     public function substrToken($stackPtr$start$length=null)
  639.     {
  640.         $current $this->getTokenContent($stackPtr);
  641.  
  642.         if ($length === null{
  643.             $newContent substr($current$start);
  644.         else {
  645.             $newContent substr($current$start$length);
  646.         }
  647.  
  648.         return $this->replaceToken($stackPtr$newContent);
  649.  
  650.     }//end substrToken()
  651.  
  652.  
  653.     /**
  654.      * Adds a newline to end of a token's content.
  655.      *
  656.      * @param int $stackPtr The position of the token in the token stack.
  657.      *
  658.      * @return bool If the change was accepted.
  659.      */
  660.     public function addNewline($stackPtr)
  661.     {
  662.         $current $this->getTokenContent($stackPtr);
  663.         return $this->replaceToken($stackPtr$current.$this->currentFile->eolChar);
  664.  
  665.     }//end addNewline()
  666.  
  667.  
  668.     /**
  669.      * Adds a newline to the start of a token's content.
  670.      *
  671.      * @param int $stackPtr The position of the token in the token stack.
  672.      *
  673.      * @return bool If the change was accepted.
  674.      */
  675.     public function addNewlineBefore($stackPtr)
  676.     {
  677.         $current $this->getTokenContent($stackPtr);
  678.         return $this->replaceToken($stackPtr$this->currentFile->eolChar.$current);
  679.  
  680.     }//end addNewlineBefore()
  681.  
  682.  
  683.     /**
  684.      * Adds content to the end of a token's current content.
  685.      *
  686.      * @param int    $stackPtr The position of the token in the token stack.
  687.      * @param string $content  The content to add.
  688.      *
  689.      * @return bool If the change was accepted.
  690.      */
  691.     public function addContent($stackPtr$content)
  692.     {
  693.         $current $this->getTokenContent($stackPtr);
  694.         return $this->replaceToken($stackPtr$current.$content);
  695.  
  696.     }//end addContent()
  697.  
  698.  
  699.     /**
  700.      * Adds content to the start of a token's current content.
  701.      *
  702.      * @param int    $stackPtr The position of the token in the token stack.
  703.      * @param string $content  The content to add.
  704.      *
  705.      * @return bool If the change was accepted.
  706.      */
  707.     public function addContentBefore($stackPtr$content)
  708.     {
  709.         $current $this->getTokenContent($stackPtr);
  710.         return $this->replaceToken($stackPtr$content.$current);
  711.  
  712.     }//end addContentBefore()
  713.  
  714.  
  715. }//end class

Documentation generated on Mon, 11 Mar 2019 14:49:41 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.