Skip to content

Commit

Permalink
Added FixerBlame tool and one test
Browse files Browse the repository at this point in the history
  • Loading branch information
connorhu committed Mar 5, 2024
1 parent 8794475 commit 0ae17bc
Show file tree
Hide file tree
Showing 9 changed files with 501 additions and 3 deletions.
11 changes: 11 additions & 0 deletions src/ExperimentalLineNumberTool/CodeChange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace PhpCsFixer\ExperimentalLineNumberTool;

final class CodeChange
{
public string $content;
public int $change;
public ?int $newLineNumber = null;
public ?int $oldLineNumber = null;
}
299 changes: 299 additions & 0 deletions src/ExperimentalLineNumberTool/FixerBlame.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
<?php

namespace PhpCsFixer\ExperimentalLineNumberTool;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Tokenizer\Tokens;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder;

final class FixerBlame
{
private Differ $differ;

/**
* @var array<array{
* fixerName: string,
* source: string
* }>
*/
private array $changeStack = [];

public function __construct()
{
$this->differ = new Differ(new StrictUnifiedDiffOutputBuilder([
'collapseRanges' => true,
'commonLineThreshold' => 1,
'contextLines' => 1,
'fromFile' => 'Original',
'toFile' => 'New',
]));
}

public function originalCode($code): void
{
if ($code instanceof Tokens) {
$code = $code->generateCode();
}

$this->changeStack[] = [
'fixerName' => '__initial__',
'source' => $code,
];
}

public function snapshotCode(string $fixerName, string $source): void
{
$this->changeStack[] = [
'fixerName' => $fixerName,
'source' => $source,
];
}

public function snapshotTokens(AbstractFixer $fixer, Tokens $tokens): void
{
$this->changeStack[] = [
'fixerName' => $fixer->getName(),
'source' => $tokens->generateCode(),
];
}

/**
* @param string $oldCode
* @param string $newCode
* @return array<CodeChange>
*/
private function diff(string $oldCode, string $newCode): array
{
$diffResults = $this->differ->diffToArray($oldCode, $newCode);

$linePointerInOldContent = 1;
$linePointerInNewContent = 1;

$buffer = [];
foreach ($diffResults as $diffResult) {
if ($diffResult[1] === Differ::ADDED) {
$diff = new CodeChange();
$diff->content = $diffResult[0];
$diff->change = Differ::ADDED;
$diff->newLineNumber = $linePointerInNewContent++;

$buffer[] = $diff;

continue;
}

if ($diffResult[1] === Differ::REMOVED) {
$diff = new CodeChange();
$diff->content = $diffResult[0];
$diff->change = Differ::REMOVED;
$diff->oldLineNumber = $linePointerInOldContent++;

$buffer[] = $diff;

continue;
}

$diff = new CodeChange();
$diff->content = $diffResult[0];
$diff->change = Differ::OLD;
$diff->newLineNumber = $linePointerInNewContent++;
$diff->oldLineNumber = $linePointerInOldContent++;

$buffer[] = $diff;
}

return $buffer;
}

/**
* @param array<CodeChange> $diffs
*
* @return array<PatchInfo>
*/
private function findPatches(array $diffs): array
{
/** @var array<PatchInfo> $patches */
$patches = [];
$patchInfo = null;
$state = 'file_start';

foreach ($diffs as $key => $diffResult) {
if ($state === 'file_start') {
if ($diffResult->change === Differ::OLD) {
$state = 'between_patch';
continue;
}

if ($diffResult->change === Differ::ADDED || $diffResult->change === Differ::REMOVED) {
$patchInfo = new PatchInfo();
$patchInfo->startKey = $key;
$patchInfo->countChange($diffResult->change);

$state = 'in_patch';
continue;
}
}

if ($state === 'between_patch' && ($diffResult->change === Differ::ADDED || $diffResult->change === Differ::REMOVED)) {
$patchInfo = new PatchInfo();
$patchInfo->startKey = $key;
$patchInfo->countChange($diffResult->change);

$state = 'in_patch';
continue;
}

if ($state === 'in_patch' && $diffResult->change === Differ::OLD) {
$state = 'between_patch';

$patchInfo->endKey = $key;
$patches[] = $patchInfo;
$patchInfo = null;

continue;
}

if ($state === 'in_patch') {
$patchInfo->countChange($diffResult->change);
}
}

if ($state === 'in_patch') {
$patchInfo->endKey = count($diffs)-1;
$patches[] = $patchInfo;
$patchInfo = null;
}

return $patches;
}

/**
* @return array<FixerChange>
*/
public function calculateChanges(): array
{
$changes = [];

foreach ($this->changeStack as $changeIndex => $change) {
if ($changeIndex === 0) {
continue;
}

$oldChangeContent = $this->changeStack[$changeIndex-1]['source'];
$newChangeContent = $change['source'];

$fixerName = $change['fixerName'];

$diffResults = $this->diff($oldChangeContent, $newChangeContent);
$patches = $this->findPatches($diffResults);

foreach ($patches as $patchInfo) {
$patchContent = $patchInfo->getPatchContent($diffResults);

$numberOfChanges = count($patchContent);

// simple remove
if ($numberOfChanges === 1 && $patchContent[0]->change === Differ::REMOVED) {
$changes[] = [
'fixerName' => $fixerName,
'start' => $patchContent[0]->oldLineNumber,
'changedSum' => $patchInfo->getChangeSum(),
'changedAt' => 0,
];

continue;
}

// line changed
if ($numberOfChanges === 2 && $patchContent[0]->change === Differ::REMOVED && $patchContent[1]->change === Differ::ADDED) {
$addedLine = $patchContent[1]->content;
$removedLine = $patchContent[0]->content;

$changedAt = null;

for ($i = 0; $i < min(strlen($addedLine), strlen($removedLine)); $i++) {
if ($addedLine[$i] !== $removedLine[$i]) {
$changedAt = $i+1;
break;
}
}

$changes[] = [
'fixerName' => $fixerName,
'start' => $patchContent[0]->oldLineNumber,
'changedSum' => $patchInfo->getChangeSum(),
'changedAt' => $changedAt ?? strlen($removedLine)+1,
];

continue;
}

$onlyRemove = 0x1;
$onlyAdd = 0x1;

foreach ($patchContent as $patchRow) {
if ($patchRow->change === Differ::ADDED) {
$onlyAdd &= 0x1;
} else {
$onlyAdd &= 0;
}

if ($patchRow->change === Differ::REMOVED) {
$onlyRemove &= 0x1;
} else {
$onlyRemove &= 0;
}
}

if ($onlyAdd === 1 xor $onlyRemove === 1) {
if ($onlyAdd === 1) {
$lineNumber = $patchContent[0]->newLineNumber;
} else {
$lineNumber = $patchContent[0]->oldLineNumber;
}

$changes[] = [
'fixerName' => $fixerName,
'start' => $lineNumber,
'changedSum' => $patchInfo->getChangeSum(),
'changedAt' => 0,
];

continue;
} else {
if ($patchContent[0]->change === Differ::ADDED) {
throw new \RuntimeException('added lines first?');
}

$changes[] = [
'fixerName' => $fixerName,
'start' => $patchContent[0]->oldLineNumber,
'changedSum' => $patchInfo->getChangeSum(),
'changedAt' => 0,
];

continue;
}

throw new \RuntimeException('unhandled case');
}
}

$changeSet = [];
foreach ($changes as $index => $change) {
$lineChanges = 0;
for ($i = $index-1; $i >= 0; --$i) {
if ($changes[$i]['start'] >= $change['start']) {
continue;
}

$lineChanges -= $changes[$i]['changedSum'];
}

$changeSet[] = new FixerChange($change['fixerName'], $change['start'] + $lineChanges, $change['changedAt']);
}

return $changeSet;
}
}
23 changes: 23 additions & 0 deletions src/ExperimentalLineNumberTool/FixerChange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace PhpCsFixer\ExperimentalLineNumberTool;

final class FixerChange
{
public string $fixerName;

public int $line;

public int $char = 0;

public function __construct(
string $fixerName,
int $line,
int $char = 0
)
{
$this->fixerName = $fixerName;
$this->line = $line;
$this->char = $char;
}
}
40 changes: 40 additions & 0 deletions src/ExperimentalLineNumberTool/PatchInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace PhpCsFixer\ExperimentalLineNumberTool;

final class PatchInfo
{
public int $startKey;
public int $endKey;
public int $linesAdded = 0;
public int $linesRemoved = 0;

public function countChange(int $changeType): void
{
if ($changeType === \SebastianBergmann\Diff\Differ::ADDED) {
$this->linesAdded++;
}

if ($changeType === \SebastianBergmann\Diff\Differ::REMOVED) {
$this->linesRemoved++;
}
}

/**
* @param array<CodeChange> $diffResults
* @return array<CodeChange>
*/
public function getPatchContent(array $diffResults): array
{
if ($this->startKey === $this->endKey) {
return [$diffResults[$this->startKey]];
}

return array_slice($diffResults, $this->startKey, $this->endKey - $this->startKey);
}

public function getChangeSum(): int
{
return $this->linesAdded - $this->linesRemoved;
}
}
Loading

0 comments on commit 0ae17bc

Please sign in to comment.