UnicornsがRTSに䟵入OpenRA゜ヌスコヌドの分析

image1.png


この蚘事では、PVS-Studio静的アナラむザヌを䜿甚しおOpenRAプロゞェクトを確認する方法に぀いお説明したす。OpenRAずは䜕ですかこれは、リアルタむムの戊略ゲヌムを䜜成するためのオヌプン゜ヌスのゲヌム゚ンゞンです。この蚘事では、分析がどのように実行されたか、プロゞェクト自䜓のどの機胜が発芋されたか、PVS-Studioがどのような興味深いトリガヌを提䟛したかに぀いお説明したす。そしおもちろん、ここでは、プロゞェクトの怜蚌プロセスをより快適にするアナラむザヌの機胜のいく぀かを怜蚎したす。



OpenRA



image2.png


レビュヌ甚に遞択されたプロゞェクトは、CommandConquerRedAlertのようなゲヌムスタむルのRTSゲヌム゚ンゞンです。詳现に぀いおは、りェブサむトをご芧ください。゜ヌスコヌドはCで蚘述されおおり、リポゞトリで衚瀺および䜿甚できたす。



OpenRAは、3぀の理由でレビュヌのために遞択されたした。たず、倚くの人が興味を持っおいるようです。いずれにせよ、リポゞトリは8000を超える星を収集しおいるため、これはGitHubの䜏民に適甚されたす。次に、OpenRAコヌドベヌスには1285個のファむルが含たれおいたす。通垞、この量は、うたくいけば、それらの䞭で興味深いトリガヌを芋぀けるのに十分です。そしお第䞉に...ゲヌム゚ンゞンはかっこいいです。



䜙分なポゞティブ



私はPVS-Studioを䜿甚しおOpenRAを分析し、最初はその結果に觊発されたした。



image3.png


非垞に倚くの高譊告の䞭で、私は確かに非垞に異なる応答をたくさん芋぀けるこずができるず決めたした。そしおもちろん、圌らに基づいお、私は最もクヌルで最も興味深い蚘事を曞きたす。しかし、それはありたせんでした



譊告自䜓を芋るだけで、すべおがすぐに実行されたした。 1306の高譊告のうち、1,277がV3144蚺断に関連付けられおいたした。 「このファむルはcopyleftラむセンスでマヌクされおいるため、掟生゜ヌスコヌドを開く必芁がありたす」などのメッセヌゞが衚瀺されたす。この蚺断に぀いおは、こちらで詳しく説明しおいたす。



OpenRAはすでにオヌプン゜ヌスプロゞェクトであるため、明らかに、そのような蚈画の仕組みは私にはたったく興味がありたせんでした。したがっお、ログの残りの郚分の衚瀺を劚げないように、非衚瀺にする必芁がありたした。Visual Studioのプラグむンを䜿甚しおいたので、簡単に行えたした。V3144アラヌトの1぀を右クリックし、開いたメニュヌで[すべおのV3144゚ラヌを非衚瀺]を遞択する必芁がありたした。



image5.png


アナラむザヌオプションの[怜出可胜な゚ラヌC]セクションに移動しお、ログに衚瀺する譊告を遞択するこずもできたす。



image7.png


Visual Studio 2019のプラグむンを䜿甚しおそれらに移動するには、トップメニュヌの[拡匵機胜]-> [PVS-スタゞオ]-> [オプション]をクリックする必芁がありたす。



詊隓結果



V3144トリガヌがフィルタヌで陀倖された 埌、ログに蚘録される譊告は倧幅に少なくなりたす。



image8.png


それにもかかわらず、私たちはそれらの䞭で興味深い瞬間を芋぀けるこずができたした。



無意味な条件



かなりの数のトリガヌが䞍芁なチェックを瀺しおいたした。通垞、人々はそのようなコヌドを意図的に蚘述しないため、これぱラヌを瀺しおいる可胜性がありたす。ただし、OpenRAでは、これらの䞍芁な条件が意図的に远加されたように芋えるこずがよくありたす。䟋えば



public virtual void Tick()
{
  ....

  Active = !Disabled && Instances.Any(i => !i.IsTraitPaused);
  if (!Active)
    return;

  if (Active)
  {
    ....
  }
}


アナラむザヌの譊告V3022匏「アクティブ」は垞に真です。 SupportPowerManager.cs 206



PVS-Studioは、Activeがfalseの堎合、実行が到達しないため、2番目のチェックは無意味であるず正しく指摘しおいたす。ここは間違いかもしれたせんが、わざず曞いたものだず思いたす。䜕のためにさお、なぜですか



おそらく、私たちの前にある皮の䞀時的な解決策があり、その改蚂は「埌で」残されおいたす。そのような堎合、アナラむザヌが開発者にそのような欠陥を思い出させるのは非垞に䟿利です。



「䞇が䞀に備えお」もう1぀のチェックを芋おみたしょう。



Pair<string, bool>[] MakeComponents(string text)
{
  ....

  if (highlightStart > 0 && highlightEnd > highlightStart)  // <=
  {
    if (highlightStart > 0)                                 // <=
    {
      // Normal line segment before highlight
      var lineNormal = line.Substring(0, highlightStart);
      components.Add(Pair.New(lineNormal, false));
    }
  
    // Highlight line segment
    var lineHighlight = line.Substring(
      highlightStart + 1, 
      highlightEnd - highlightStart – 1
    );
    components.Add(Pair.New(lineHighlight, true));
    line = line.Substring(highlightEnd + 1);
  }
  else
  {
    // Final normal line segment
    components.Add(Pair.New(line, false));
    break;
  }
  ....
}


アナラむザヌの譊告V3022匏 'highlightStart> 0'は垞にtrueです。 LabelWithHighlightWidget.cs 54



繰り返したすが、再チェックが完党に無意味であるこずは明らかです。ハむラむトスタヌト倀は2回チェックされ、隣接する行でチェックされたす。゚ラヌ条件の1぀で、テスト甚に間違った倉数が遞択された可胜性がありたす。いずれにせよ、これが䜕であるかを確実に蚀うのは難しいです。 1぀明らかなこずは、コヌドを調査しお修正する必芁があるか、䜕らかの理由で远加の怜蚌が必芁な堎合は説明を残す必芁があるずいうこずです。



別の同様のポむントがありたす



public static void ButtonPrompt(....)
{
  ....
  var cancelButton = prompt.GetOrNull<ButtonWidget>(
    "CANCEL_BUTTON"
  );
  ....

  if (onCancel != null && cancelButton != null)
  {
    cancelButton.Visible = true;
    cancelButton.Bounds.Y += headerHeight;
    cancelButton.OnClick = () =>
    {
      Ui.CloseWindow();
      if (onCancel != null)
        onCancel();
    };

    if (!string.IsNullOrEmpty(cancelText) && cancelButton != null)
      cancelButton.GetText = () => cancelText;
  }
  ....
}


アナラむザヌの譊告V3063条件匏の䞀郚は、評䟡されるず垞にtrueになりたすcancelButton= Null。GetOrNullメ゜ッドによっお返される倀がこの倉数に曞き蟌たれるため、ConfirmationDialogs.cs



78cancelButtonは実際にnullになる可胜性がありたす。ただし、条件ステヌトメントの本文でcancelButtonがnullに倉わるこずはないず考えるのは論理的です。それにもかかわらず、ただチェックがありたす。倖郚条件に泚意を払わないず、䞀般に奇劙な状況が発生したす。最初に倉数のプロパティにアクセスし、次に開発者が確認するこずにしたした-それはただnullかどうか



最初は、プロゞェクトが「== "挔算子」のオヌバヌロヌドに関連する特定のロゞックを䜿甚しおいる可胜性があるず想定したした。私の意芋では、プロゞェクトの参照タむプに類䌌したものを実装するこずは物議を醞すアむデアです。それでも、異垞な動䜜により、他の開発者がコヌドを理解するのが難しくなりたす。同時に、そのようなトリックを免れない状況を想像するのは難しいです。堎合によっおは、これが䟿利な解決策になる可胜性がありたす。



たずえば、Unityゲヌム゚ンゞンでは、「== "」挔算子がUnityEngine.Objectクラスに察しお再定矩されたす。リンクで入手可胜な公匏ドキュメントは、このクラスのむンスタンスをnullず比范するこずを瀺しおいたす通垞どおり動䜜したせん。たあ、確かに開発者はそのような珍しいロゞックを実装する理由がありたした。



OpenRAでそのようなものは芋぀かりたせんでした:)。したがっお、以前に怜蚎されたnullのチェックに意味がある堎合、それは別のもので構成されたす。



PVS-Studioは、さらにいく぀かの同様の瞬間を怜出できたしたが、ここにすべおをリストする必芁はありたせん。同じポゞティブを芋るのはただ退屈です。幞いなこずにたたはそうではなく、アナラむザヌは他の奇劙なこずも芋぀けるこずができたした。



到達䞍胜なコヌド



void IResolveOrder.ResolveOrder(Actor self, Order order)
{
  ....
  if (!order.Queued || currentTransform == null)
    return;
  
  if (!order.Queued && currentTransform.NextActivity != null)
    currentTransform.NextActivity.Cancel(self);

  ....
}


アナラむザヌの譊告V3022匏 'Order.Queued && currentTransform.NextActivity= Null'は垞にfalseです。 TransformsIntoTransforms.cs44



ここでも無意味なチェックがありたす。ただし、前のものずは異なり、ここでは远加の条件だけでなく、実際に達成できないコヌドを瀺したす。以前に考えられおいた垞に真のチェックは、実際にはプログラムの動䜜に圱響を䞎えたせんでした。それらはコヌドから削陀するこずも、残すこずもできたす-䜕も倉曎されたせん。



ここで、奇劙なチェックは、コヌドの䞀郚が実行されおいないずいう事実に぀ながりたす。同時に、ここで修正ずしおどのような倉曎を行うべきかを掚枬するこずは困難です。最も単玔で最も快適なケヌスでは、到達䞍胜なコヌドは単に実行されるべきではありたせん。そうすれば間違いはありたせん。しかし、プログラマヌが矎のためだけに意図的にこの行を曞いたのではないかず思いたす。



コンストラクタヌの初期化されおいない倉数



public class CursorSequence
{
  ....
  public readonly ISpriteFrame[] Frames;

  public CursorSequence(
    FrameCache cache, 
    string name, 
    string cursorSrc, 
    string palette, 
    MiniYaml info
  )
  {
    var d = info.ToDictionary();

    Start = Exts.ParseIntegerInvariant(d["Start"].Value);
    Palette = palette;
    Name = name;

    if (
      (d.ContainsKey("Length") && d["Length"].Value == "*") || 
      (d.ContainsKey("End") && d["End"].Value == "*")
    ) 
      Length = Frames.Length - Start;
    else if (d.ContainsKey("Length"))
      Length = Exts.ParseIntegerInvariant(d["Length"].Value);
    else if (d.ContainsKey("End"))
      Length = Exts.ParseIntegerInvariant(d["End"].Value) - Start;
    else
      Length = 1;

    Frames = cache[cursorSrc]
      .Skip(Start)
      .Take(Length)
      .ToArray();

    ....
  }
}


アナラむザヌの譊告V3128「フレヌム」フィヌルドは、コンストラクタヌで初期化される前に䜿甚されたす。CursorSequence.cs35



非垞に䞍快な瞬間。初期化されおいない倉数からLengthプロパティの倀を取埗しようずするず、必然的にNullReferenceExceptionがスロヌされたす。通垞の状況では、このような゚ラヌが芋過ごされるこずはほずんどありたせんが、クラスのむンスタンスを䜜成できないこずは簡単に明らかになりたす。ただし、ここでは、条件が次の堎合にのみ䟋倖がスロヌされたす



(d.ContainsKey("Length") && d["Length"].Value == "*") || 
(d.ContainsKey("End") && d["End"].Value == "*")


本圓になりたす。



すべおがうたく機胜するようにコヌドを修正する必芁があるかどうかを刀断するのは難しいです。関数は次のようになっおいるはずです。



public CursorSequence(....)
{
  var d = info.ToDictionary();

  Start = Exts.ParseIntegerInvariant(d["Start"].Value);
  Palette = palette;
  Name = name;
  ISpriteFrame[] currentCache = cache[cursorSrc];
    
  if (
    (d.ContainsKey("Length") && d["Length"].Value == "*") || 
    (d.ContainsKey("End") && d["End"].Value == "*")
  ) 
    Length = currentCache.Length - Start;
  else if (d.ContainsKey("Length"))
    Length = Exts.ParseIntegerInvariant(d["Length"].Value);
  else if (d.ContainsKey("End"))
    Length = Exts.ParseIntegerInvariant(d["End"].Value) - Start;
  else
    Length = 1;

  Frames = currentCache
    .Skip(Start)
    .Take(Length)
    .ToArray();

  ....
}


このバヌゞョンでは、指定された問題はありたせんが、開発者だけが元のアむデアにどれだけ察応しおいるかを蚀うこずができたす。



朜圚的なタむプミス



public void Resize(int width, int height)
{
  var oldMapTiles = Tiles;
  var oldMapResources = Resources;
  var oldMapHeight = Height;
  var oldMapRamp = Ramp;
  var newSize = new Size(width, height);

  ....
  Tiles = CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero]);
  Resources = CellLayer.Resize(
    oldMapResources,
    newSize,
    oldMapResources[MPos.Zero]
  );
  Height = CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero]);
  Ramp = CellLayer.Resize(oldMapRamp, newSize, oldMapHeight[MPos.Zero]);  
  ....
}


アナラむザヌの譊告V31272぀の類䌌したコヌドフラグメントが芋぀かりたした。おそらくこれはタむプミスであり、「oldMapHeight」の代わりに「oldMapRamp」倉数を䜿甚する必芁がありたす。Map.cs964



アナラむザヌは、関数ぞの匕数の受け枡しに関連する疑わしい瞬間を怜出したした。呌び出しを個別に芋おみたしょう。



CellLayer.Resize(oldMapTiles,     newSize, oldMapTiles[MPos.Zero]);
CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero]);
CellLayer.Resize(oldMapHeight,    newSize, oldMapHeight[MPos.Zero]);
CellLayer.Resize(oldMapRamp,      newSize, oldMapHeight[MPos.Zero]);


奇劙なこずに、最埌の呌び出しはoldMapRampではなくoldMapHeightを枡したす。もちろん、そのようなすべおのケヌスが実際に゚ラヌであるわけではありたせん。ここにすべおが正しく曞かれおいる可胜性は十分にありたす。しかし、あなたはこの堎所が珍しいように芋えるこずを認めなければなりたせん。私はここで間違いが実際に行われたず信じる傟向がありたす。



同僚からのメモ-AndreyKarpov。そしお、私はこのコヌドに奇劙なこずは䜕も芋おいたせん。これは叀兞的な最埌の行の゚ラヌです



それでもここに゚ラヌがない堎合は、説明を远加する䟡倀がありたす。結局のずころ、瞬間が間違いのように芋える堎合、誰かが間違いなくそれを修正したいず思うでしょう。



真実、真実、そしお真実以倖の䜕物でもない



プロゞェクトには非垞に特殊なメ゜ッドが含たれおおり、その戻り倀はboolタむプです。それらの特城は、どのような条件䞋でも真に戻るずいう事実にありたす。䟋えば



static bool State(
  S server, 
  Connection conn, 
  Session.Client client, 
  string s
)
{
  var state = Session.ClientState.Invalid;
  if (!Enum<Session.ClientState>.TryParse(s, false, out state))
  {
    server.SendOrderTo(conn, "Message", "Malformed state command");
    return true;
  }

  client.State = state;

  Log.Write(
    "server", 
    "Player @{0} is {1}",
    conn.Socket.RemoteEndPoint, 
    client.State
  );

  server.SyncLobbyClients();

  CheckAutoStart(server);

  return true;
}


アナラむザヌの譊告V3009このメ゜ッドが垞に同じ倀の「true」を返すのは奇劙なこずです。LobbyCommands.cs 123



このコヌドは倧䞈倫ですか間違いはありたすかずおも奇劙に芋えたす。開発者がvoidを䜿甚しなかったのはなぜですか



アナラむザヌがそのような堎所を奇劙だず芋なすこずは驚くべきこずではありたせんが、それでもプログラマヌが実際にこのように曞く理由があったこずを認めなければなりたせん。どれ



このメ゜ッドがどこで呌び出され、垞に真の戻り倀が䜿甚されおいるかを確認するこずにしたした。同じクラス内にそれぞの参照が1぀しかないこずが刀明したした-commandHandlers蟞曞にはタむプがありたす



IDictionary<string, Func<S, Connection, Session.Client, string, bool>>


初期化䞭に、倀が远加されたす



{"state", State},
{"startgame", StartGame},
{"slot", Slot},
{"allow_spectators", AllowSpectators}


等



静的型付けによっお問題が発生するずいうたれな信じたいケヌスが衚瀺されたす。結局のずころ、倀が異なる眲名を持぀関数になる蟞曞を䜜成するこずは...少なくずも問題がありたす。commandHandlersは、InterpretCommandメ゜ッドでのみ䜿甚されたす。



public bool InterpretCommand(
  S server, Connection conn, Session.Client client, string cmd
)
{
  if (
    server == null || 
    conn == null || 
    client == null || 
    !ValidateCommand(server, conn, client, cmd)
  )  return false;

  var cmdName = cmd.Split(' ').First();
  var cmdValue = cmd.Split(' ').Skip(1).JoinWith(" ");

  Func<S, Connection, Session.Client, string, bool> a;
  if (!commandHandlers.TryGetValue(cmdName, out a))
    return false;

  return a(server, conn, client, cmdValue);
}


どうやら、開発者の目暙は、実行された特定の操䜜の文字列を䞀臎させる普遍的な胜力でした。圌が遞んだ方法はそれだけではないず思いたすが、そのような状況でもっず䟿利で正しいものを提䟛するのはそれほど簡単ではありたせん。特に、ある皮のダむナミックたたは類䌌のものを䜿甚しない堎合。このテヌマに぀いお䜕かアむデアがあれば、コメントを残しおください。この問題を解決するためのさたざたなオプションを怜蚎するこずは私にずっお興味深いこずです。このクラスの垞にtrueのメ゜ッドに



関連付けられおいる譊告は、おそらくfalseであるこずがわかりたす。それでも...あなたを怖がらせるのはこの「最も可胜性が高い」こずです。その䞭には確かに間違いがあるかもしれないので、あなたはそのようなこずに泚意する必芁がありたす。



このようなすべおの陜性は泚意深くチェックし、必芁に応じお停ずしおマヌクする必芁がありたす。これは非垞に簡単に行われたす。アナラむザヌによっお瀺された堎所に特別なコメントを残す必芁がありたす。



static bool State(....) //-V3009


別の方法がありたす。falseずしおマヌクする必芁があるポゞティブを遞択し、コンテキストメニュヌで[遞択したメッセヌゞをFalseアラヌムずしおマヌクする]をクリックしたす。



image10.png


このトピックの詳现に぀いおは、ドキュメントをご芧ください。



nullの远加チェック



static bool SyncLobby(....)
{
  if (!client.IsAdmin)
  {
    server.SendOrderTo(conn, "Message", "Only the host can set lobby info");
    return true;
  }

  var lobbyInfo = Session.Deserialize(s); 
  if (lobbyInfo == null)                    // <=
  {
    server.SendOrderTo(conn, "Message", "Invalid Lobby Info Sent");
    return true;
  }

  server.LobbyInfo = lobbyInfo;

  server.SyncLobbyInfo();

  return true;
}


アナラむザヌの譊告V3022匏 'lobbyInfo == null'は垞にfalseです。 LobbyCommands.cs851



垞にtrueを返す別のメ゜ッド。ただし、今回は別のタむプのトリガヌを調べおいたす。これが単なる冗長なコヌドであるずいう事実からはほど遠いので、そのようなこずを十分に泚意深く研究する必芁がありたす。しかし、たず最初に。Deserialize



メ゜ッドがnullを返すこずはありたせん。コヌドを確認するこずで、これを簡単に確認できたす。



public static Session Deserialize(string data)
{
  try
  {
    var session = new Session();
    ....
    return session;
  }
  catch (YamlException)
  {
    throw new YamlException(....);
  }
  catch (InvalidOperationException)
  {
    throw new YamlException(....);
  }
}


読みやすくするために、メ゜ッドの゜ヌスコヌドを短くしたした。リンクをたどるず完党に芋るこずができたす。さお、たたは私の蚀葉を借りれば、ここのセッション倉数はいかなる状況でもnullになりたせん。



䞀番䞋に䜕が芋えたすかDeserializeはnullを返したせん。䜕か問題が発生した堎合、䟋倖がスロヌされたす。呌び出し埌にnullのチェックを䜜成した開発者は、考え方が異なるようでした。ほずんどの堎合、䟋倖的な状況では、SyncLobbyメ゜ッドは珟圚実行されおいるコヌドを実行する必芁がありたす...はい、lobbyInfoがnullになるこずはないため、実行されるこずはありたせん。



if (lobbyInfo == null)
{
  server.SendOrderTo(conn, "Message", "Invalid Lobby Info Sent");
  return true;
}


私の代わりに、この「䜙分」のチェックを、あなたはただ䜿甚する必芁があるず信じおいるのtry -キャッチ。たたは、反察偎から行っお、TryDeserializeを蚘述したす。これは、䟋倖の堎合にnullを返したす。



考えられるNullReferenceException



public ConnectionSwitchModLogic(....)
{
  ....
  var logo = panel.GetOrNull<RGBASpriteWidget>("MOD_ICON");
  if (logo != null)
  {
    logo.GetSprite = () =>
    {
      ....
    };
  }

  if (logo != null && mod.Icon == null)                    // <=
  {
    // Hide the logo and center just the text
    if (title != null)
    title.Bounds.X = logo.Bounds.Left;

    if (version != null)
      version.Bounds.X = logo.Bounds.X;
    width -= logo.Bounds.Width;
  }
  else
  {
    // Add an equal logo margin on the right of the text
    width += logo.Bounds.Width;                           // <=
  }
  ....
}


アナラむザヌの譊告V3125'logo 'オブゞェクトは、nullに察しお怜蚌された埌に䜿甚されたした。チェック行236、222。ConnectionLogic.cs 236



ここに100の゚ラヌがあるず䜕かが教えおくれたす。GetOrNullメ゜ッドは、おそらく実際にnull参照を返す可胜性があるため、私たちの前に「䜙分な」チェックは絶察にありたせん。ロゎがnullの堎合はどうなりたすかBoundsプロパティを呌び出すず䟋倖がスロヌされたすが、これは明らかに開発者の蚈画には含たれおいたせんでした。



おそらく、フラグメントは次のように曞き盎す必芁がありたす。



if (logo != null)
{
  if (mod.Icon == null)
  {
    // Hide the logo and center just the text
    if (title != null)
    title.Bounds.X = logo.Bounds.Left;

    if (version != null)
      version.Bounds.X = logo.Bounds.X;
    width -= logo.Bounds.Width;
  }
  else
  {
    // Add an equal logo margin on the right of the text
    width += logo.Bounds.Width;
  }
}


このオプションは理解するのに十分簡単ですが、远加のネストはあたりクヌルに芋えたせん。より容量の倧きい゜リュヌションずしお、null条件挔算子を䜿甚できたす。



// Add an equal logo margin on the right of the text
width += logo?.Bounds.Width ?? 0; // <=


私はトップフィックスの方が奜きだずいうこずに泚意しおください。それを読むのは楜しいですし、疑問は生じたせん。しかし、䞀郚の開発者は簡朔さを高く評䟡しおいるので、2番目のオプションも䞎えるこずにしたした:)。



倚分それは結局OrDefaultですか



public MapEditorLogic(....)
{
  var editorViewport = widget.Get<EditorViewportControllerWidget>("MAP_EDITOR");

  var gridButton = widget.GetOrNull<ButtonWidget>("GRID_BUTTON");
  var terrainGeometryTrait = world.WorldActor.Trait<TerrainGeometryOverlay>();

  if (gridButton != null && terrainGeometryTrait != null) // <=
  {
    ....
  }

  var copypasteButton = widget.GetOrNull<ButtonWidget>("COPYPASTE_BUTTON");
  if (copypasteButton != null)
  {
    ....
  }

  var copyFilterDropdown = widget.Get<DropDownButtonWidget>(....);
  copyFilterDropdown.OnMouseDown = _ =>
  {
    copyFilterDropdown.RemovePanel();
    copyFilterDropdown.AttachPanel(CreateCategoriesPanel());
  };

  var coordinateLabel = widget.GetOrNull<LabelWidget>("COORDINATE_LABEL");
  if (coordinateLabel != null)
  {
    ....
  }

  ....
}


アナラむザヌの譊告V3063条件匏の䞀郚は、評䟡されるず垞にtrueになりたすterrainGeometryTrait= Null。 MapEditorLogic.cs35



このスニペットを分析しおみたしょう。WidgetクラスのGetOrNullメ゜ッドが䜿甚されるたびに、nullがチェックされるこずに泚意しおください。ただし、Getが䜿甚されおいる堎合、怜蚌は行われたせん。これは論理的です-Getメ゜ッドはnullを返したせん



public T Get<T>(string id) where T : Widget
{
  var t = GetOrNull<T>(id);
  if (t == null)
    throw new InvalidOperationException(....);
  return t;
}


芁玠が芋぀からない堎合は、䟋倖がスロヌされたす。これは劥圓な動䜜です。同時に、論理的なオプションは、GetOrNullメ゜ッドによっお返された倀がnull参照ず等しいかどうかを確認するこずです。



䞊蚘のコヌドでは、Traitメ゜ッドによっお返される倀がnullであるかどうかがチェックされたす。実際、Traitメ゜ッド内では、GetはTraitDictionaryクラスから呌び出されたす。



public T Trait<T>()
{
  return World.TraitDict.Get<T>(this);
}


このGetの動䜜は、前に説明したものずは異なる可胜性がありたすかしかし、クラスは異なりたす。確認しよう



public T Get<T>(Actor actor)
{
  CheckDestroyed(actor);
  return InnerGet<T>().Get(actor);
}


InnerGet メ゜ッドは、TraitContainer <T>のむンスタンスを返したす。このクラスでのGetの実装は、WidgetクラスのGetず非垞によく䌌おいたす。



public T Get(Actor actor)
{
  var result = GetOrDefault(actor);
  if (result == null)
    throw new InvalidOperationException(....);
  return result;
}


䞻な類䌌点は、ここでもnullが返されないこずです。䜕か問題が発生した堎合、InvalidOperationExceptionも同様にスロヌされたす。したがっお、Traitメ゜ッドは同じように動䜜したす。



はい、ここに远加のチェックがあるだけで、䜕の圱響もありたせん。奇劙に芋えるかもしれたせんが、そのようなコヌドが読者を倧いに混乱させるずは蚀えたせん。ただし、ここでチェックが必芁な堎合は、予期せず䟋倖がスロヌされるこずがありたす。悲しいです。



この時点で、TraitOrNullを呌び出す方が適切なようです。ただし、そのような方法はありたせん:)。しかし、GetOrNullに類䌌したTraitOrDefaultがありたすこの堎合。Get



メ゜ッドに関連する別の同様のポむントがありたす



public AssetBrowserLogic(....)
{
  ....
  frameSlider = panel.Get<SliderWidget>("FRAME_SLIDER");
  if (frameSlider != null)
  {
    ....
  }
  ....
}


アナラむザヌの譊告V3022匏 'frameSlider= Null'は垞にtrueです。AssetBrowserLogic.cs 128䞊蚘



のコヌドず同様に、ここで問題が発生しおいたす。チェックは本圓に䞍芁であるか、Getの代わりにGetOrNullを呌び出す必芁がありたす。



割り圓おを倱った



public SpawnSelectorTooltipLogic(....)
{
  ....
  var textWidth = ownerFont.Measure(labelText).X;
  if (textWidth != cachedWidth)
  {
    label.Bounds.Width = textWidth;
    widget.Bounds.Width = 2 * label.Bounds.X + textWidth; // <=
  }

  widget.Bounds.Width = Math.Max(                         // <=
    teamWidth + 2 * labelMargin, 
    label.Bounds.Right + labelMargin
  );
  team.Bounds.Width = widget.Bounds.Width;
  ....
}


アナラむザヌの譊告V3008'widget.Bounds.Width '倉数には、2回連続しお倀が割り圓おられたす。おそらくこれは間違いです。チェックラむン78、75 SpawnSelectorTooltipLogic.cs 78は、



あればずいうこずらしいtextWidth = CachedWidth条件がある真の、いく぀かの特定の倀が曞き蟌たれるたでwidget.Bounds.Width。ただし、この条件が真であるかどうかに関係なく、以䞋の割り圓おは文字列を奪いたす



widget.Bounds.Width = 2 * label.Bounds.X + textWidth;


あらゆる意味で。圌らは単に他のものをここに眮くのを忘れた可胜性がありたす



if (textWidth != cachedWidth)
{
  label.Bounds.Width = textWidth;
  widget.Bounds.Width = 2 * label.Bounds.X + textWidth;
}
else
{
  widget.Bounds.Width = Math.Max(
    teamWidth + 2 * labelMargin,
    label.Bounds.Right + labelMargin
  );
}


デフォルト倀の確認



public void DisguiseAs(Actor target)
{
  ....
  var tooltip = target.TraitsImplementing<ITooltip>().FirstOrDefault();
  AsPlayer = tooltip.Owner;
  AsActor = target.Info;
  AsTooltipInfo = tooltip.TooltipInfo;
  ....
}


アナラむザヌの譊告V3146「tooltip」のnull逆参照の可胜性。 'FirstOrDefault'は、デフォルトのnull倀を返すこずができたす。 Disguise.cs 192 Firstの代わりにFirstOrDefaultが



䞀般的に䜿甚されるのはい぀ですか遞択が空の堎合、FirstはInvalidOperationExceptionをスロヌしたす。FirstOrDefaultは䟋倖をスロヌしたせんが、参照タむプに察しおnullを返したす。 プロゞェクトでは、ITooltipむンタヌフェむスはさたざたなクラスによっお実装されたす。したがっお、target.TraitsImplementing <ITooltip>が空の遞択を返す堎合、nullがtooltipに曞き蟌たれたす。



..。このオブゞェクトのプロパティにさらにアクセスするず、NullReferenceExceptionが発生したす。



開発者が遞択範囲が空にならないこずが確実な堎合は、Firstを䜿甚する方が適切です。よくわからない堎合は、FirstOrDefaultによっお返される倀を確認する必芁がありたす。これがここにないのはかなり奇劙です。結局のずころ、前述のGetOrNullメ゜ッドによっお返される倀は垞にチェックされおいたした。なぜ圌らはここでそれをしなかったのですか



誰が知っおいる...ああ、たさに確かに、開発者はこれらの質問に答えるこずができたす。最埌に、圌はこのコヌドを線集する必芁がありたす。



結論



OpenRAはどういうわけか、チェックするのが楜しくお面癜いプロゞェクトであるこずがわかりたした。開発者は玠晎らしい仕事をし、同時に゜ヌスが簡単に習埗できるはずであるこずを忘れたせんでした。もちろん、ここにはさたざたな...物議を醞すポむントがありたすが、それらがない堎合。



同時に、すべおの努力をしおも、開発者alasは人間のたたです。考えられるポゞティブのいく぀かは、アナラむザヌを䜿甚せずに気付くこずは非垞に困難です。曞き蟌んだ盎埌でも゚ラヌを芋぀けるのが難しい堎合がありたす。久しぶりに芋぀けたずはどういうこずか。



明らかに、その結​​果よりも間違いを怜出する方がはるかに優れおいたす。このために、膚倧な数の新しい゜ヌスを手動で再チェックするのに䜕時間も費やすこずができたす。さお、叀いものは同時に芋えたす-突然、以前に間違いに気づかなかったのですかはい、レビュヌは本圓に䟿利ですが、倚くのコヌドを調べる必芁がある堎合は、時間の経過ずずもにいく぀かのこずに気付かなくなりたす。そしお、それに倚くの時間ず劎力が費やされおいたす。



image11.png


静的分析は、コヌドレビュヌなど、゜ヌスコヌドの品質をチェックする他の方法ぞの䟿利な远加です。 PVS-Studioは、開発者ではなく「単玔な」堎合によっおはそれだけではない゚ラヌを怜出し、人々がより深刻な問題に集䞭できるようにしたす。



はい、アナラむザヌは誀怜知を生成するこずがあり、すべおの゚ラヌをたったく芋぀けるこずができたせん。しかし、それを䜿甚するず、倚くの時間ず神経を節玄できたす。はい、圌は完璧ではなく、時には自分で間違いを犯したす。ただし、䞀般的に、PVS-Studioを䜿甚するず、開発プロセスがはるかに簡単になり、快適になり、さらに予想倖に安䟡になりたす。



実際、私の蚀葉を信じる必芁はありたせん。䞊蚘の真実を自分で確認する方がはるかに良いでしょう。リンクをたどっおアナラむザヌをダりンロヌドし、トラむアルキヌを入手しおください。どれだけ簡単ですか



たあ、それだけです。枅聎ありがずうございたしたコヌドず同じクリヌン゚ラヌログをクリヌンアップしおください。





この蚘事を英語を話す聎衆ず共有したい堎合は、翻蚳リンクを䜿甚しおくださいNikitaLipilin。ナニコヌンはRTSに䟵入したすOpenRA゜ヌスコヌドの分析。



All Articles