Ein PowerShell-Analysewerkzeug in PowerShell bauen — Skripte mit dem AST lesen statt mit regulären Ausdrücken

· Aktualisiert am: · · PowerShell, AST, Statische Analyse, Entwicklerwerkzeuge

Angenommen, Sie wollen eine Reihe von PowerShell-Skripten durchsehen und jede Stelle auflisten, die Write-Host aufruft. Nach dem Wort zu suchen ist einfach, aber alle drei der folgenden Zeilen passen.

$source = @'
# Write-Host gibt Text auf dem Bildschirm aus
$message = 'Write-Host'
Write-Host 'Hallo'
'@

Nur Zeile 3 ist das, was Sie suchen. Zeile 1 ist ein Kommentar und Zeile 2 eine Zeichenfolge, die einer Variablen zugewiesen wird, also ruft keine von beiden Write-Host auf.

PowerShell selbst unterscheidet das, während es den Code ausführt. Sie können das Ergebnis dieser Unterscheidung auch selbst herausholen. Dieser Artikel beginnt damit, den Code zu parsen und die dabei entstehenden Objekte zu untersuchen. Zusätzliche Module werden nicht verwendet.1

Das @' ... '@ oben ist ein Here-String in einfachen Anführungszeichen. Es legt diese drei Zeilen als Zeichenfolge in $source ab, die vorerst nicht ausgeführt wird und in der $message nicht erweitert wird.2

1. Anzeigen, was der Parser zurückgibt

Übergeben Sie $source zuerst an ParseInput.

$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput(
    $source, [ref] $tokens, [ref] $parseErrors)

if ($parseErrors.Count -gt 0) {
    throw $parseErrors[0].Message
}

$ast.GetType().Name
ScriptBlockAst

Was in $ast gelandet ist, ist weder eine Zeichenfolge noch das Ergebnis der Skriptausführung: Es ist ein Objekt vom Typ ScriptBlockAst. Es stellt den gesamten von Ihnen gelieferten Code dar. Neben der Rückgabe dieses Objekts gibt ParseInput über die mit [ref] übergebenen Variablen die Token und die Syntaxfehler zurück.1

AST steht für „abstrakter Syntaxbaum“. Der Name lässt an eine besondere Datenstruktur denken, aber aus Sicht von PowerShell ist es zunächst ein Objekt mit Eigenschaften und Methoden. Folgen Sie diesen Eigenschaften, gelangen Sie zu weiteren Objekten, die Zuweisungen und Befehlsaufrufe darstellen.3

In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (5 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle

2. Welche Objekte sind aus diesen drei Zeilen geworden?

In Code wie diesem, der begin, process und end nicht ausschreibt, liegen gewöhnliche Anweisungen in EndBlock.Statements. Stellen wir ihre Typen neben den ursprünglichen Code.4

$statements = $ast.EndBlock.Statements
$statements | ForEach-Object {
    [pscustomobject]@{
        Type = $_.GetType().Name
        Text = $_.Extent.Text
    }
}
Type                   Text
----                   ----
AssignmentStatementAst $message = 'Write-Host'
PipelineAst            Write-Host 'Hallo'

Zwei Einträge. Die Kommentarzeile erscheint nicht in dieser Anweisungsliste. Brauchen Sie die Kommentare selbst, erhalten Sie sie aus den soeben empfangenen $tokens.3

Aus Zeile 2 wurde ein AssignmentStatementAst, das eine Zuweisung darstellt. Zeile 3 ist ein PipelineAst: Auch ohne ein | zu schreiben, wird sie als Pipeline mit einem einzigen Befehl dargestellt. Extent.Text ist der ursprüngliche Code, der zu dem Objekt gehört. Wann immer der Typname allein Sie im Unklaren lässt, sagt Ihnen das, von welchem Teil der Quelle die Rede ist.56

Wie der ursprüngliche Code auf Syntaxobjekte abgebildet wirdDer EndBlock des gesamten Skripts enthält die Zuweisung aus Zeile 2 und die Pipeline aus Zeile 3, und Letztere enthält den Befehlsaufruf.ScriptBlockAstEndBlockZuweisung: Zeile 2Pipeline: Zeile 3CommandAst

Abbildung 1: Der Aufruf in Zeile 3 liegt innerhalb der Pipeline.

Array-Indizes beginnen bei 0, also ist $statements[1] die Pipeline aus Zeile 3. Nehmen Sie daraus das erste Element.

$pipeline = $statements[1]
$call = $pipeline.PipelineElements[0]

$call.GetType().Name
$call.Extent.Text
$call.GetCommandName()
CommandAst
Write-Host 'Hallo'
Write-Host

Da ist das CommandAst. Extent.Text, das den ursprünglichen Code zurückgibt, und GetCommandName(), das den Namen des Aufrufs zurückgibt, werden beide auf demselben Objekt verwendet.7

Die Aufschlüsselung einschließlich der Argumente steht in CommandElements.

$call.CommandElements | ForEach-Object {
    [pscustomobject]@{
        Type = $_.GetType().Name
        Text = $_.Extent.Text
    }
}
Type                        Text
----                        ----
StringConstantExpressionAst Write-Host
StringConstantExpressionAst 'Hallo'

Name und Argument sind beide Knoten, die Zeichenfolgen darstellen. Trotzdem gibt GetCommandName() Write-Host zurück. Es wählt nicht nach der Schreibweise der Zeichenfolge allein, sondern sieht auf das Element, das innerhalb eines Befehlsaufrufs als Name dient. Das 'Write-Host' in Zeile 2 steht auf der rechten Seite einer Zuweisung und gehört zu diesem Aufruf überhaupt nicht.78

Das ist der Unterschied zwischen einer Textsuche und dem Parsen. Selbst beim gleichen Wort lässt sich anhand der Struktur, in der es steht, seine Rolle unterscheiden.

3. Nach CommandAst suchen, statt über Indizes zu laufen

Den Inhalt haben wir bestätigt. In einem echten Skript können Sie jedoch nicht „das erste Element der zweiten Anweisung“ fest verdrahten. Aufrufe stehen in Funktionen, in if-Blöcken und mitten in einer Pipeline.

Die Suchmethode dafür ist FindAll.

$commands = @($ast.FindAll({
    param($node)
    $node -is [System.Management.Automation.Language.CommandAst]
}, $true))

$commands.Count
$commands[0].GetType().Name
$commands[0].Extent.Text
1
CommandAst
Write-Host 'Hallo'

FindAll durchläuft den Syntaxbaum und übergibt jeden Knoten an die Prüfung in { ... }. Prüfen Sie den Typ des übergebenen $node mit -is und geben Sie $true zurück, wenn es ein CommandAst ist. Dieser Knoten bleibt in den Ergebnissen. Das abschließende $true weist an, auch in verschachtelten Funktionen und Skriptblöcken zu suchen.9

Bei function Show-Message { Write-Host 'hello' } etwa steigt es in den Funktionskörper hinab und greift den Aufruf auf. Die Funktion muss dafür nicht ausgeführt werden.

Suche nach Typ bis in eine Funktion hineinDie Suche steigt in den Körper einer im Skript enthaltenen Funktion hinab und nimmt den zu CommandAst passenden Aufruf in die Ergebnisse auf.SkriptFunktionsdefinitionFunktionskörperCommandAstTyp passt: bleibt in den Ergebnissen

Abbildung 2: Sie erhalten die zur Suchbedingung passenden Knoten, ohne die Verschachtelungstiefe selbst zu zählen.

In den drei Zeilen vom Anfang fand es denselben einen Aufruf, den wir über den Index erreicht hatten. Nehmen Sie Name und Position heraus, und Sie haben etwas, das sich als Suchergebnis verwenden lässt.

$commands | ForEach-Object {
    [pscustomobject]@{
        Name   = $_.GetCommandName()
        Line   = $_.Extent.StartLineNumber
        Column = $_.Extent.StartColumnNumber
    }
}
Name       Line Column
----       ---- ------
Write-Host    3      1

Zeilen und Spalten sind 1-basiert. Weil derselbe Knoten sowohl den Namen als auch die Position trägt, müssen Sie die Zeile nicht mit einer separaten Textsuche erneut suchen.6

4. Wie ein Aufruf über eine Variable aussieht

Ändern wir das Analyseziel ein wenig. Nehmen Sie neben dem direkt geschriebenen Namen auch Fälle auf, die eine Zeichenfolge oder eine Variable an den Aufrufoperator & übergeben.

$source = @'
Write-Host 'direct'
& 'Write-Host' 'quoted'
$command = 'Write-Host'
& $command 'variable'
'@

$ast = [System.Management.Automation.Language.Parser]::ParseInput(
    $source, [ref] $tokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0) { throw $parseErrors[0].Message }

$commands = @($ast.FindAll({
    param($node)
    $node -is [System.Management.Automation.Language.CommandAst]
}, $true))

$commands | ForEach-Object {
    [pscustomobject]@{
        Line = $_.Extent.StartLineNumber
        Name = $_.GetCommandName()
        Text = $_.Extent.Text
    }
}
Line Name       Text
---- ----       ----
   1 Write-Host Write-Host 'direct'
   2 Write-Host & 'Write-Host' 'quoted'
   4            & $command 'variable'

Auch Zeile 4 wurde als CommandAst gefunden. Der Rückgabewert von GetCommandName() ist jedoch $null.

Ein Mensch, der diese vier Zeilen liest, kann den Wert von $command aus der Zuweisung direkt darüber erschließen. Diese Methode geht nicht zu Zuweisungen an eine Variable zurück und berechnet deren Wert. Anders als wenn der Name direkt im Code steht, kann diese API allein den Namen nicht herausholen.7

Derselbe Aufrufknoten, unterschiedliche Ergebnisse beim Ermitteln des NamensBei einem Aufruf, dessen Name eine Zeichenfolge ist, lässt sich Write-Host ermitteln; bei einem Aufruf über eine Variable ist der Name null, aber beide tragen die Position des Aufrufs.CommandAstNamenselement ist eine ZeichenfolgeNamenselement ist eine VariableName: Write-HostName: null

Abbildung 3: Auch bei leerem Namen lässt sich noch erkennen, dass in Zeile 4 ein Aufruf steht.

Halten Sie diesen Unterschied in einem Werkzeug, das Dateien durchsieht, in einer Spalte NameKind fest. Zeilen, deren Namenszeichenfolge ermittelt werden konnte, sind Static; Zeilen, bei denen das nicht gelang, sind Unresolved. Die namenlosen Zeilen zu behalten heißt, die Stellen nicht aus dem Blick zu verlieren, die ein Mensch prüfen sollte.

5. Die Positionen von Write-Host aus einer Datei herausziehen

Wenn Sie eine .ps1 statt einer Zeichenfolge lesen, verwenden Sie ParseFile anstelle von ParseInput. An der Art, den AST zu lesen, ändert sich nichts.10

Get-ScriptCommand, das alles Bisherige zusammenfasst, finden Sie im vollständigen Code am Ende dieses Artikels und im Beispielpaket. Speichern Sie den vollständigen Code als Get-ScriptCommand.ps1 und die vier Zeilen innerhalb des Here-Strings aus Abschnitt 4 als demo.ps1 im selben Ordner. Das Beispielpaket enthält beide Dateien.

Führen Sie es in diesem Ordner aus.

. .\Get-ScriptCommand.ps1
$calls = @(Get-ScriptCommand -LiteralPath .\demo.ps1)

$calls |
    Where-Object { $_.NameKind -eq 'Static' -and $_.Name -eq 'Write-Host' } |
    Format-Table Line, Column, NameKind, Name -AutoSize
Line Column NameKind Name
---- ------ -------- ----
   1      1 Static   Write-Host
   2      1 Static   Write-Host

Damit haben Sie die Positionen der Aufrufe, deren Name Write-Host ist. Die Zuweisung in Zeile 3 ist nicht enthalten. Damit ist das Problem vermieden, mit dem wir begonnen haben: dass Kommentare und gewöhnliche Zeichenfolgen in die Suchergebnisse geraten.

Zeile 4 fällt dagegen aus diesem Filter heraus. Nicht weil dort kein Aufruf stünde, sondern weil der Name unbestimmt ist. Prüfen Sie die unbestimmten Zeilen getrennt.

$calls |
    Where-Object NameKind -eq 'Unresolved' |
    Format-Table Line, Column, NameKind, Name -AutoSize
Line Column NameKind   Name
---- ------ --------   ----
   4      1 Unresolved

Static ist ein Vermerk mit der Bedeutung „die Namenszeichenfolge konnte ermittelt werden“. Es ist keine Garantie, dass der Befehl existiert oder dass Sie wissen, welche Implementierung aufgerufen wird. echo etwa kommt als echo zurück und Microsoft.PowerShell.Utility\Write-Host als qualifizierter Name, sodass keines von beiden in der obigen Suche nach exakter Übereinstimmung auftaucht. Sind auch Aliase und qualifizierte Aufrufe Teil Ihrer Untersuchung, muss die Suchbedingung dazu passen.11

Beachten Sie: Per Dot-Sourcing eingebunden wurde das Analysewerkzeug. demo.ps1 wird nur mit ParseFile gelesen und nie gestartet.

6. Die Suchergebnisse sind kein Ausführungsprotokoll

Diese Suche untersucht, wie etwas im Code geschrieben ist. Aufrufe stehen auch in nie benutzten Funktionen und in if ($false) { Write-Host ... }, also erscheinen sie in der Liste. Über die Ausführungsreihenfolge oder die Häufigkeit sagt sie nichts.

Auch Zeichenfolgen werden unterschiedlich behandelt: 'Write-Host' und "Today: $(Get-Date)" sind nicht dasselbe. In Letzterer ist ein Ausdruck eingebettet, sodass das darin enthaltene Get-Date gefunden wird. Code, der in einer gewöhnlichen Zeichenfolge steht, wird dagegen nie als eigenes Skript erneut geparst.2

Ein .NET-Methodenaufruf wie [Console]::WriteLine(...) ist eine andere Knotenart als CommandAst. Diese Liste deckt weder jede Operation ab noch beweist sie irgendetwas über Sicherheit. Sie ist ein Werkzeug, um selbst verwaltete Skripte zu durchleuchten. Aliase zur Laufzeit oder gleichnamige Funktionen zu bestätigen erfordert außerdem die Umgebung, in der das Skript läuft.1211

Im Mittelpunkt dessen, was wir hier genutzt haben, stehen die mit GetType() bestätigten Objekte und Extent.Text. Wann immer Sie ein anderes Sprachkonstrukt untersuchen wollen, können Sie genauso beginnen: ein kurzes Stück Code an ParseInput übergeben und diese beiden nebeneinanderstellen. Wenn Ihr Ziel die Qualitätsprüfung anhand bestehender Regeln ist, ist PSScriptAnalyzer die bessere Wahl.

Der vollständige Code zum Durchsuchen von Dateien

Unten steht das gesamte Get-ScriptCommand aus Abschnitt 5. Der Teil, der die Syntax liest, ist derselbe wie in Abschnitt 3; darum herum liegen das Beschaffen der Datei, die Behandlung von Syntaxfehlern und die Eingabe mehrerer Dateien.

function Get-ScriptCommand {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [ValidateNotNullOrEmpty()]
        [string[]] $LiteralPath
    )

    process {
        foreach ($path in $LiteralPath) {
            $file = Get-Item -LiteralPath $path -Force -ErrorAction Stop
            if ($file -isnot [System.IO.FileInfo]) {
                throw "A file is required: $path"
            }

            $tokens = $null
            $parseErrors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile(
                $file.FullName, [ref] $tokens, [ref] $parseErrors)
            if ($parseErrors.Count -gt 0) {
                $first = $parseErrors[0]
                throw ('Parse error: {0}:{1}:{2} ({3})' -f $file.FullName,
                    $first.Extent.StartLineNumber,
                    $first.Extent.StartColumnNumber, $first.ErrorId)
            }

            $commands = $ast.FindAll({
                    param($node)
                    $node -is [System.Management.Automation.Language.CommandAst]
                }, $true)
            foreach ($command in ($commands | Sort-Object { $_.Extent.StartOffset })) {
                $name = $command.GetCommandName()
                $kind = if ($null -eq $name) { 'Unresolved' } else { 'Static' }
                [pscustomobject]@{
                    Path     = $file.FullName
                    Line     = $command.Extent.StartLineNumber
                    Column   = $command.Extent.StartColumnNumber
                    NameKind = $kind
                    Name     = $name
                }
            }
        }
    }
}

Es beschafft die tatsächliche Datei mit Get-Item -LiteralPath und übergibt dann FullName an ParseFile. Ein Name wie draft[1].ps1 wird nicht als Platzhaltermuster behandelt. Verzeichnisse und nicht vorhandene Dateien lösen einen Fehler aus.

Gibt es einen Syntaxfehler, hält es an, bevor es für diese Datei Ergebnisse ausgibt. Ein teilweise zurückgegebener AST zählt nicht als erfolgreiche Analyse. Es geht darum, „korrekt gelesen und null gefunden“ von „konnte nicht gelesen werden“ zu trennen.

Der Rückgabewert wird nicht zu einer Tabelle formatiert: Es ist ein Objekt mit Path, Line, Column, NameKind und Name. Neben dem Filtern wie in Abschnitt 5 können Sie die Ergebnisse mehrerer Dateien als CSV speichern. Weil es Eingaben auch über eine Eigenschaft namens FullName annimmt, können Sie die von Get-ChildItem zurückgegebenen FileInfo-Objekte direkt durchreichen.

Get-ChildItem -LiteralPath .\scripts -Filter *.ps1 -File -Recurse |
    Get-ScriptCommand |
    Export-Csv -LiteralPath .\commands.csv -NoTypeInformation -Encoding UTF8 -NoClobber

-NoClobber verhindert das Überschreiben einer vorhandenen CSV. Schlägt eine Datei mittendrin fehl, sind die Ergebnisse davor womöglich bereits in der CSV gelandet. Nehmen Sie die Existenz der Datei nicht als Zeichen dafür, dass jeder Eintrag gelungen ist — prüfen Sie auch die Fehler.

Die zum Parsen verwendete Grammatik folgt der PowerShell-Version, die das Werkzeug ausführt. Erfolgreiches Parsen unter 7.x heißt nicht, dass das Skript unter 5.1 läuft. Wenn Sie unter 5.1 auch Dateien mit japanischem Text lesen, berücksichtigen Sie die Zeichencodierung, etwa UTF-8 mit BOM.13

Beispiele und Überprüfung

Das Analysewerkzeug, die Beispiele und die Tests (ZIP) enthalten die fertige Funktion, Beispiele zum Analysieren und Pester-Tests. Die Funktion selbst entspricht dem hier gezeigten Code; die verteilte Fassung ergänzt Hilfekommentare. Können Sie das ZIP nicht beziehen, lässt sich der vollständige Code oben weiterhin speichern und verwenden.

Die fertige Funktion und die vorhandenen 26 Pester-Fälle sind gegenüber der vorherigen Fassung unverändert. Für diese Überarbeitung wurden die 12 aus dem Text extrahierten PowerShell-Codeblöcke unter Windows PowerShell 5.1 und PowerShell 7.x ausgeführt und Typnamen, ursprünglicher Code, Aufrufpositionen und gefilterte Ergebnisse gegeneinander geprüft. Die genauen Versionen und der Umfang der Überprüfung sind in der README des Beispielpakets dokumentiert.

Verwandte Artikel

  1. Microsoft Learn, Parser.ParseInput Method. Zur API, die aus einer Zeichenfolge einen AST zurückgibt und Token und Syntaxfehler über Ausgabeargumente liefert.  2

  2. Microsoft Learn, about_Quoting_Rules. Zu Here-Strings in einfachen Anführungszeichen und zu Teilausdrücken in erweiterbaren Zeichenfolgen.  2

  3. Microsoft PowerShell Team, Using abstract syntax trees (ASTs) with ISE to make scripting more productive. Zum Zugriff auf den Syntaxbaum aus PowerShell und zur Suche nach Knoten wie Funktionsdefinitionen.  2

  4. Microsoft Learn, NamedBlockAst Class. Zu Blöcken, deren Name nicht ausgeschrieben ist, und zu Statements, das die Anweisungen enthält. 

  5. Microsoft Learn, PipelineAst.PipelineElements Property. Zu den Elementen, aus denen eine Pipeline besteht. 

  6. Microsoft Learn, IScriptExtent Interface. Zum Bereich in der Quelle, zur Startposition und dazu, dass Zeilen und Spalten 1-basiert sind.  2

  7. Microsoft Learn, CommandAst.GetCommandName Method. Dazu, dass bei Aufrufen, deren Name sich nicht statisch ermitteln lässt, null zurückgegeben wird.  2 3

  8. Microsoft Learn, CommandAst.CommandElements Property. Zu Syntaxelementen wie dem Aufrufnamen und den Argumenten. 

  9. Microsoft Learn, Ast.FindAll Method. Zum Durchlaufen der zu einer Bedingung passenden Knoten und zur Option, verschachtelte Funktionen und Skriptblöcke zu durchsuchen. 

  10. Microsoft Learn, Parser.ParseFile Method. Zur API, die eine Datei parst und AST, Token und Syntaxfehler liefert. 

  11. Microsoft Learn, about_Command_Precedence. Zur Rangfolge gleichnamiger Befehle, Aliase, Funktionen und Ähnliches zur Laufzeit.  2

  12. Microsoft Learn, InvokeMemberExpressionAst Constructors. Zu den Knoten, die Instanz- und statische Methodenaufrufe darstellen. 

  13. Microsoft Learn, about_Character_Encoding. Dazu, wie Windows PowerShell Skripte liest, und zur Behandlung des UTF-8-BOM. 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was ist der PowerShell-AST?
Es ist ein abstrakter Syntaxbaum, der Code als Knoten je Sprachkonstrukt darstellt, etwa eine Zuweisung, einen Befehlsaufruf oder eine Funktionsdefinition. Damit lassen sich dieselben Zeichen, wenn sie in einem Kommentar oder einer Zeichenfolge stehen, von denselben Zeichen als Name eines Aufrufs unterscheiden.
Muss ich die analysierten .ps1-Dateien ausführen?
Das Werkzeug in diesem Artikel liest sie mit Parser.ParseFile; es startet das Ziel weder noch bindet es es per Dot-Sourcing ein. Es ist jedoch keine Sandbox, die Sicherheit garantiert — es dient der Bestandsaufnahme selbst verwalteter Skripte. Keinen Aufruf zu finden ist ebenfalls kein Beweis für Sicherheit.
Bekomme ich den Namen eines Befehls, der über eine Variable aufgerufen wird?
Wenn GetCommandName den Namen nicht statisch ermitteln kann, gibt es null zurück. Das Werkzeug in diesem Artikel verwirft diese Zeile nicht: Es behält sie mit der NameKind Unresolved. Es verfolgt keine Variablenzuweisungen und wertet keine Ausdrücke aus, um den Namen zu erraten.
Funktioniert das unter Windows PowerShell 5.1?
Das hier vorgestellte Werkzeug zielt auf 5.1 und die 7.x-Reihe. Die zum Parsen verwendete Grammatik ist jedoch die der PowerShell, die das Werkzeug ausführt. Erfolgreiches Parsen unter 7.x ist kein Beweis für Kompatibilität mit 5.1. Wenn Sie unter 5.1 auch Dateien mit japanischem Text lesen, berücksichtigen Sie zudem die Zeichencodierung, etwa UTF-8 mit BOM.
Wie verhält sich das zu PSScriptAnalyzer?
Das Werkzeug in diesem Artikel gibt es, um Namen und Positionen von Aufrufen aufzulisten. Wenn Sie die Qualität anhand bestehender Regeln prüfen und die Warnungen verwalten wollen, nutzen Sie PSScriptAnalyzer. Die hier gezeigte Art, den AST zu lesen, ist ein Einstieg, um Ergebnisse statischer Analyse und die Funktionsweise eigener Regeln zu verstehen.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog