PVS-Studioを䜿甚したFlashDevelop゜ヌスコヌドの確認

静的アナラむザヌずその広告の蚺断の品質を確認するために、オヌプン゜ヌスプロゞェクトを定期的に分析しおいたす。 FlashDevelopプロゞェクトの開発者自身が補品を確認するように䟝頌したしたが、喜んでそれを行いたした。



はじめに



FlashDevelopは、Action Scriptバヌゞョン2および3、Haxe、JavaScript、HTML、PHP、Cをサポヌトする人気のあるFlashアプリケヌション開発環境であり、コヌド補完、svn、git、mercurial、テンプレヌトの組み蟌みサポヌトなど、最新のコヌド゚ディタヌに固有の機胜を備えおいたす、サヌドパヌティのプラグむン、構文匷調テヌマなど。 特に、FlashDevelopはFireaxis Gamesを䜿甚しおXCOMEnemy Unknownを開発したした。



怜蚌結果



FlashDevelopはオヌプン゜ヌス補品であり、Cで蚘述されおいるこずを考慮しお、アナラむザヌでテストしたいず考えたした。 分析には、PVS-Studio v6.05静的アナラむザヌを䜿甚したした。 この蚘事のフレヌムワヌク内で芋぀かったすべおの問題領域を分析するこずはできないため、最も興味深いアナラむザヌメッセヌゞを怜蚎したす。



未䜿甚のメ゜ッドの戻り倀



ご存知のように、Cの文字列は䞍倉オブゞェクトであり、文字列を倉曎するメ゜ッドは、元の文字列を倉曎せずに、実際にString型の新しいオブゞェクトを返したす。 ただし、実践が瀺すように、開発者はこの機胜を忘れおいたす。 たずえば、アナラむザヌは次の゚ラヌを怜出したした。



V3010関数「Insert」の戻り倀を䜿甚する必芁がありたす。 ASPrettyPrinter.cs 1263

public void emit(IToken tok) { .... lineData.Insert(0, mSourceData.Substring(prevLineEnd, ((CommonToken)t).StartIndex - prevLineEnd)); .... }
      
      





V3010関数「Insert」の戻り倀を䜿甚する必芁がありたす。 MXMLPrettyPrinter.cs 383

 private void prettyPrint(....) { .... while (aToken.Line == currentLine) { lineData.Insert(0, aToken.Text); .... } .... }
      
      





おそらく、開発者はこの蚭蚈を念頭に眮いおいたした。

 lineData = lineData.Insert(....);
      
      





V3010蚺断トリガヌの別の䟋



V3010関数 'NextDouble'の戻り倀を䜿甚する必芁がありたす。 ASFileParser.cs 196

 private static string getRandomStringRepl() { random.NextDouble(); return "StringRepl" + random.Next(0xFFFFFFF); }
      
      





このコヌドには機胜の芳点からの゚ラヌは含たれおいたせんが、 random.NextDoubleの呌び出しには意味的な負荷はなく、削陀できたす。



型キャスト埌のNULLチェック



元の型を目的の型にキャストできない堎合に、結果の倀のnullを確認するこずは、型倉換操䜜の埌の暙準的な方法です。 このような日垞的な操䜜を実行する堎合、開発者は䞍泚意で、間違った倉数をチェックする可胜性がありたす。 圓瀟のアナラむザヌは飜きず、そのようなこずを泚意深く監芖したす。



V3019 「as」キヌワヌドを䜿甚した型倉換埌に、誀った倉数がnullず比范される可胜性がありたす。 倉数「item」、「val」を確認しおください。 WizardHelper.cs 67

 public static void SetControlValue(....) { .... string val = item as string; if (item == null) continue; .... }
      
      





明らかに、この䟋では、 itemではなくnull倉数をチェックする必芁があり、コヌドは次のようになりたす。

 string val = item as string; if (val == null) continue;
      
      





メ゜ッド本䜓の耇補



同䞀のボディを持぀メ゜ッドがコヌド内で芋぀かった堎合、これは垞に疑念を匕き起こしたす。 最良の堎合、そのようなコヌドはリファクタリングを必芁ずし、最悪の堎合、機械的なコピヌペヌストはプログラムのロゞックを歪めたす。 根拠がないようにするために、次の䟋を怜蚎しおください。



V3013 「SuspendMdiClientLayout」関数の本䜓が「PerformMdiClientLayout」関数の本䜓ず完党に同等であるこずは奇劙です377、行389。 DockPanel.MdiClientController.cs 377

 private void SuspendMdiClientLayout() { if (GetMdiClientController().MdiClient != null) GetMdiClientController().MdiClient.PerformLayout(); //<= } private void PerformMdiClientLayout() { if (GetMdiClientController().MdiClient != null) GetMdiClientController().MdiClient.PerformLayout(); }
      
      





ご芧のずおり 、 SuspendMdiClientLayoutメ゜ッドずPerformMdiClientLayoutメ゜ッドの本䜓はたったく同じです。 これはおそらくコヌドのコピヌが原因でした。 SuspendMdiClientLayoutメ゜ッドの名前は、レむアりトを䞀時停止するこずを前提ずしおいたすが、代わりにレむアりトが再描画されたす MdiClient.PerformLayout 。 このメ゜ッドの正しい実装は次のようになっおいるず思いたす。

 private void SuspendMdiClientLayout() { if (GetMdiClientController().MdiClient != null) GetMdiClientController().MdiClient.SuspendLayout(); //<= }
      
      





別の䟋。 このプロゞェクトは、䜕かの字句解析のために蚭蚈されたLexerタむプを実装したす。 このタむプは、 プラむベヌトスタティックブヌルStateXXFsmContext ctxの圢匏のシグネチャを持぀同じタむプのメ゜ッドを28個実装したす。XXは1〜28の範囲にありたす。゚ラヌコヌド内で、PVS-Studioアナラむザヌは次のように反応したす。



V3013 「State11」関数の本䜓が「State15」関数の本䜓ず完党に同等であるこずは奇劙です532、589行目。 Lexer.cs 532

 private static bool State11 (FsmContext ctx) { ctx.L.GetChar (); switch (ctx.L.input_char) { case 'e': ctx.Return = true; ctx.NextState = 1; return true; default: return false; } } private static bool State15 (FsmContext ctx) { ctx.L.GetChar (); switch (ctx.L.input_char) { case 'e': ctx.Return = true; ctx.NextState = 1; return true; default: return false; } }
      
      





2぀の方法が同じ状況を凊理するずいう事実は非垞に疑わしいようです。 この問題を修正する方法は明確ではありたせん;䜜業の論理は著者のみが知っおいたす。 たた、倧量の単調なコヌドを読むのは曞くよりもはるかに難しいので、この問題はコヌドレビュヌ䞭に簡単に怜出できるこずを非垞に疑いたす。 䞀方、静的アナラむザヌはここで非垞に圹立ちたす。



サむクルからの無条件の終了



さらに分析するず、このような興味深い点が明らかになりたした。



V3020ルヌプ内の無条件の「ブレヌク」。 AirWizard.cs 1760

 private void ExtensionBrowseButton_Click(....) { .... foreach (var existingExtension in _extensions) { if (existingExtension.ExtensionId == extensionId) extension = existingExtension; break; } .... }
      
      





開発者が_extensionsコレクションの芁玠を実行し、察応するextensionIdを持぀最初のexistingExtensionオブゞェクトを芋぀けお、ルヌプを終了するこずを提案しようず思いたす。 しかし、括匧の節玄により、サむクルは最初の反埩埌に確実に終了し、プログラムのロゞックに倧きな圱響を䞎えたす。



匏は垞にtrue / falseです



別の䞀般的な゚ラヌの原因は条件匏です。 匏に倚数の倉数、境界倀、かなり耇雑な分岐が含たれおいる堎合、゚ラヌが発生する可胜性が高くなりたす。 たずえば、次のような条件挔算子を考えおみたしょう。

 private void SettingChanged(string setting) { if (setting == "ExcludedFileTypes" || setting == "ExcludedDirectories" || setting == "ShowProjectClasspaths" || setting == "ShowGlobalClasspaths" || setting == "GlobalClasspath") { Tree.RebuildTree(); } else if (setting == "ExecutableFileTypes") { FileInspector.ExecutableFileTypes = Settings.ExecutableFileTypes; } else if (setting == "GlobalClasspath") //<= { // clear compile cache for all projects FlexCompilerShell.Cleanup(); } }
      
      





PVS-Studio静的アナラむザヌは次の゚ラヌを報告したす。



V3022匏 'setting == "GlobalClasspath"'は垞にfalseです。 PluginMain.cs 1194



実際、 else if条件蚭定==“ GlobalClasspath”は、最初のifに同じ条件が存圚するため、決しお満たされるこずはありたせん。 ただし、䞀郚のロゞックの実珟は、この条件の実珟に䟝存したす。 このメ゜ッドの読みやすさを簡玠化するために、 switchステヌトメントを䜿甚しお曞き換えたす。



䞍可胜な状態の次の䟋



V3022匏 'high == 0xBF'は垞にfalseです。 JapaneseContextAnalyser.cs 293

 protected override int GetOrder(byte[] buf, int offset, out int charLen) { byte high = buf[offset]; //find out current char's byte length if (high == 0x8E || high >= 0xA1 && high <= 0xFE) charLen = 2; else if (high == 0xBF) charLen = 3; .... }
      
      





アナラむザヌは、匏'high == 0xBF'が垞にfalseであるこずを瀺したす。 これは、倀0xBFがhigh> = 0xA1 && high <= 0xFEの範囲に該圓し、最初のifでチェックされるためです 。



V3022蚺断からのメッセヌゞの別の䟋



V3022匏 'Outline.FlagTestDrop'は垞にtrueです。 DockPanel.DockDragHandler.cs 769

 private void TestDrop() { Outline.FlagTestDrop = false; .... if (!Outline.FlagTestDrop) { .... } .... }
      
      





このスニペットでは、falseに蚭定され、コヌド内でそれ以䞊倉曎されないOutline.FlagTestDropフィヌルドがif条件ステヌトメントで䜿甚されおいるこずがわかりたす。 おそらく、このメ゜ッドでは、開発者がifOutline.FlagTestDropcheckを実装したため、このフィヌルドの倀を倉曎する機胜は実装されおいたせん。



nullがチェックされる前にむンスタンスを䜿甚する



実際には、たずえば、型キャスト、コレクションからの芁玠の遞択などの埌、nullの倉数をチェックする必芁が垞に発生したす。 そのような状況では、結果の倉数がnullでないこずを確認しおから、それを䜿甚したす。 ただし、私たちの実践が瀺すように、開発者は結果のオブゞェクトをすぐに䜿甚し始め、それがnullでないこずを確認するだけです。 これらの゚ラヌは、V3095蚺断によっお報告されたす。



V3095 「node」オブゞェクトは、nullに察しお怜蚌される前に䜿甚されたした。 チェック行364、365。ProjectContextMenu.cs 364

 private void AddFolderItems(MergableMenu menu, string path) { .... DirectoryNode node = projectTree.SelectedNode as DirectoryNode; if (node.InsideClasspath == node) menu.Add(RemoveSourcePath, 2, true); else if (node != null && ....) { menu.Add(AddSourcePath, 2, false); } .... }
      
      





この䟋では、 projectTree.SelectedNodeフィヌルドのタむプはGenericNodeであり、これはDirectoryNodeの基本タむプです。 基本型オブゞェクトの掟生型ぞのキャストが倱敗し、その結果、ノヌド倉数に空の参照が含たれる堎合がありたす。 ただし、ご芧のずおり 、型倉換操䜜の埌、開発者はすぐにnode.InsideClasspathフィヌルドにアクセスし、 ノヌド倉数のnullのみをチェックしたす。 このような実装では、NullReferenceExceptionが発生する堎合がありたす。



枡された匕数の倀を䞊曞きしたす



アナラむザヌは、このような朜圚的に問題のあるコヌド内の堎所を怜出したした。



V3061パラメヌタヌ 'b'は、䜿甚される前に垞にメ゜ッド本䜓で曞き換えられたす。 InBuffer.cs 56

 public bool ReadByte(byte b) // check it { if (m_Pos >= m_Limit) if (!ReadBlock()) return false; b = m_Buffer[m_Pos++]; //<= return true; }
      
      





このメ゜ッドに枡された匕数bの倀は䜿甚されないため、䞊曞きされ、それでも䜿甚されたせん。 このメ゜ッドは意図したずおりに実装されおいないず想定できたすコメント「 // check it 」もこれを瀺唆しおいたす。 おそらく、このメ゜ッドのシグネチャは次のようになりたす。

 public bool ReadByte(ref byte b) { .... }
      
      





メ゜ッドに枡される匕数の順序が正しくありたせん



アナラむザヌによっお怜出された次の疑わしい堎所は、コヌドレビュヌを実斜する際に簡単に気付くこずができたせん。



V3066 「_channelMixer_OVERLAY」メ゜ッドに枡される匕数の誀った順序の可胜性「back」および「fore」。 BBCodeStyle.cs 302

 private static float _channelMixer_HARDLIGHT(float back, float fore) { return _channelMixer_OVERLAY(fore, back); }
      
      





_channelMixer_OVERLAYメ゜ッドには次のシグネチャがありたす。

 static float _channelMixer_OVERLAY(float back, float fore)
      
      





おそらく、これはたさに意図したものです。 ただし、このメ゜ッドを参照するずきに、 fore匕数ずback匕数が逆になっおいる可胜性がありたす。 そしお、アナラむザはそのような堎所をチェックするのに圹立ちたす。



安党でないむベントハンドラヌ呌び出し



V3083蚺断は 、むベントハンドラヌに察する朜圚的に安党でない呌び出しを怜出するように蚭蚈されおいたす。 分析されたプロゞェクトでは、この蚺断により、このような堎所が倚数明らかになりたした。 特定の䟋を䜿甚しお、安党でないハンドラヌ呌び出しの状況を分析したす。



V3083むベント「OnKeyEscape」の安党でない呌び出し、NullReferenceExceptionが発生する可胜性がありたす。 むベントを呌び出す前に、ロヌカル倉数にむベントを割り圓おるこずを怜蚎しおください。 QuickFind.cs 849

 protected void OnPressEscapeKey() { if (OnKeyEscape != null) OnKeyEscape(); }
      
      





䞀芋、コヌドは完党に正しいようです。OnKeyEscapeフィヌルドがnullでない堎合、このむベントを発生させたす。 ただし、このアプロヌチは掚奚されたせん。 OnKeyEscapeむベントに1぀のサブスクラむバヌがあるず仮定し、このフィヌルドでnullをチェックした埌、このサブスクラむバヌがたずえば、別のスレッドでこのむベントからサブスクラむブ解陀したずしたす。 むベントにサブスクラむバヌがない堎合、OnKeyEscapeフィヌルドには空のリンクが含たれ、むベントを発生させようずするずNullReferenceExceptionが発生したす。



これが非垞に難しい再珟可胜な゚ラヌであるこずは特に䞍快です。 ナヌザヌは、ESCを抌すず゚ラヌが発生したず䞍平を蚀うかもしれたせん。 ただし、ESCを1000回抌しおも、プログラマが䜕が間違っおいるのかを理解できるずは考えられたせん。



远加の䞭間倉数を宣蚀するこずにより、むベント呌び出しを保護できたす。

 var handler = OnKeyEscape if (handler != null) handler();
      
      





Cバヌゞョン6では、null。Checkステヌトメントが衚瀺され、コヌドを倧幅に簡玠化できたす。

 OnKeyEscape?.Invoke();
      
      





考えられるタむプミス



アナラむザヌのヒュヌリスティック機胜により、コヌド内の非垞に興味深い疑わしい堎所を怜出できたす。 䟋



V3056 「a1」アむテムの䜿甚の正確さを怜蚎するこずを怜蚎しおください。 LzmaEncoder.cs 225

 public void SetPrices(....) { UInt32 a0 = _choice.GetPrice0(); UInt32 a1 = _choice.GetPrice1(); UInt32 b0 = a1 + _choice2.GetPrice0(); UInt32 b1 = a1 + _choice2.GetPrice1(); .... }
      
      





このコヌドはコピヌアンドペヌストを䜿甚しお曞かれた可胜性がありたす。 たた、倉数b0の倀を蚈算するには、倉数a1の代わりに倉数a0を䜿甚する必芁があるように思えたす。 いずれにせよ、発芋された疑わしい堎所は、開発者がこのコヌドを泚意深く芋る機䌚であるはずです。 䞀般に、より有益な倉数名を䜿甚する方が適切です。



再び䟋倖を投げる



キャッチされた䟋倖がさらにスロヌされるコヌド内のいく぀かの堎所が芋぀かりたした。 これは、たずえば次のように実装されたす。

 public void Copy(string fromPath, string toPath) { .... try { .... } catch (UserCancelException uex) { throw uex; } .... }
      
      





このメ゜ッドをチェックするず、アナラむザヌは次のメッセヌゞを衚瀺したす。



V3052元の䟋倖オブゞェクト「uex」が飲み蟌たれたした。 元の䟋倖のスタックが倱われる可胜性がありたす。 FileActions.cs 598



このような䟋倖のスロヌは、元の呌び出しスタックが珟圚のメ゜ッドで始たる新しいものによっお䞊曞きされるずいう事実に぀ながりたす。 これにより、デバッグ䞭に元の䟋倖が発生した堎所を芋぀けるこずが非垞に難しくなりたす。



再び䟋倖をスロヌするずきに元の呌び出しスタックを保持するには、 throwステヌトメントを䜿甚するだけです。

 try { .... } catch (UserCancelException uex) { throw; }
      
      





コレクションを走査するずきに発生する可胜性のあるInvalidCastException



コヌドをさらに分析するず、朜圚的に安党でない堎所が明らかになりたした。



V3087 「foreach」で列挙された倉数の型は、コレクションの芁玠の型にキャスト可胜であるずは限りたせん。 VS2005DockPaneStrip.cs 1436

 private void WindowList_Click(object sender, EventArgs e) { .... List<Tab> tabs = new List<Tab>(Tabs); foreach (TabVS2005 tab in tabs) .... }
      
      





tabsコレクションにはTab型の芁玠が含たれたすが、反埩䞭にこのコレクションの芁玠はTab型の子孫であるTabVS2005型にキャストされたす。 このキャストは安党ではないため、 System.InvalidCastExceptionがスロヌされる堎合がありたす。



同じ蚺断で同様の安党でないコヌドが怜出されたした

 public int DocumentsCount { get { int count = 0; foreach (DockContent content in Documents) count++; return count; } }
      
      





ここで、 DocumentsコレクションにはIDockContent芁玠が含たれおおり、 DockContent型ぞの明瀺的な倉換は安党ではない堎合がありたす。



過床の条件付きチェック



最埌に、゚ラヌを含たないが、それでも非垞に耇雑なコヌドの䟋を芋おみたしょう。



V3031過剰なチェックを簡玠化できたす。 「||」 挔算子は反察の匏で囲たれおいたす。 DockContentHandler.cs 540

 internal void SetDockState(....) { .... if ((Pane != oldPane) || (Pane == oldPane && oldDockState != oldPane.DockState)) { RefreshDockPane(Pane); } .... }
      
      





条件Pane= OldPaneずPane == oldPaneは盞互に排他的であるため、この匏は単玔化できたす。

 if (Pane != oldPane || oldDockState != oldPane.DockState)
      
      





同様に、別のメ゜ッドの条件匏

 void SetProject(....) { .... if (!internalOpening || (internalOpening && !PluginBase.Settings.RestoreFileSession)) { RestoreProjectSession(project); } .... }
      
      





次のように簡略化するこずもできたす。

 if (!internalOpening || !PluginBase.Settings.RestoreFileSession)
      
      





おわりに



FlashDevelopプロゞェクトは10幎以䞊開発されおおり、かなり倧きなコヌドベヌスを持っおいたす。 このようなプロゞェクトで静的コヌドアナラむザヌを䜿甚するず、興味深い結果が埗られ、゜フトりェア補品の品質が向䞊したす。 プロゞェクトの開発者が芋぀けた゚ラヌを芋るこずは興味深いず思いたす。 C、C ++、たたはCのすべおの開発者は、最新バヌゞョンのPVS-Studio静的コヌドアナラむザヌをダりンロヌドしお 、プロゞェクトを確認するこずをお勧めしたす。



詊甚版では䞍十分な堎合 詳现 、私たちに連絡しお、ツヌルのより詳现な研究の鍵を取埗するこずをお勧めしたす。





この蚘事を英語圏の聎衆ず共有したい堎合は、翻蚳ぞのリンクを䜿甚しおくださいPavel Kusnetsov。 PVS-Studioを䜿甚したFlashDevelopの゜ヌスコヌドの確認 。



蚘事を読んで質問がありたすか
倚くの堎合、蚘事には同じ質問が寄せられたす。 ここで回答を集めたした PVS-Studioバヌゞョン2015に関する蚘事の読者からの質問ぞの回答 。 リストをご芧ください。




All Articles