今天,我们发布了材料翻译的第二部分,该材料致力于对Instagram中大量服务器端Python代码进行静态分析。

→
第一部分厌倦棉绒的程序员
考虑到我们有大约一百条自己的掉毛规则,对这些规则发布的建议进行花哨的计算会很快导致开发人员浪费时间。 最好花一些时间来整理代码样式或摆脱过时的模式来创建新内容并开发项目。
我们发现,当程序员看到来自linter的太多通知时,他们开始忽略所有这些消息。 这也适用于重要通知。
假设我们决定声明
fn
函数已过时,并使用具有更好名称的函数
add
代替。 如果您不将此事告知开发人员,他们将不知道他们不再需要使用
fn
函数。 更糟糕的是,他们不知道该使用什么功能。 在这种情况下,您可以创建一个林特规则。 但是任何大型代码库都将包含许多规则。 结果,重要的linter通知很可能会在次要bug的通知堆中丢失。
林特太挑剔了,“有用的信号”很容易在“噪音”中迷失我们该怎么办?
您可以自动修复短绒检测到的许多问题。 如果可以将lint本身与需要的地方出现的文档进行比较,那么这种自动更正就是重构在需要的地方执行的代码。 鉴于有大量使用Instagram的开发人员,几乎不可能对每个人进行我们最好的代码编写技术培训。 在系统中添加自动代码纠正功能,使我们可以在开发人员不了解新技术的情况下对其进行培训。 这有助于我们快速使开发人员保持最新状态。 此外,自动更正允许我们使程序员专注于重要的事情,而不是专注于单调的次要代码更改。 通常,应该指出,在培训开发人员方面,自动代码更正比简单的lint通知更有效和有用。
那么,如何创建一个自动代码校正系统呢? 基于语法树的棉绒为我们提供了有关功能障碍节点的信息。 结果,我们不需要创建逻辑来检测问题,因为我们已经有了关于linter的相应规则! 由于我们知道哪个特定节点不适合我们以及其源代码位于何处,因此我们可以在不冒险破坏某些东西的情况下,例如用
add
替换
fn
函数的名称。 这非常适合于纠正检测到违规时所执行规则的单一违规。 但是,如果我们为linter引入新规则,那意味着在代码库中可能有数百个不符合该规则的代码片段,该怎么办? 是否可以预先纠正所有这些不一致之处?
代码修改
Codemod只是发现问题并更改源代码的一种方式。 Codemod基于脚本。 可以将Codemod视为“类固醇重构”。 通过代码模式解决的任务范围非常广泛:从简单的任务(如在函数中重命名变量)到复杂的任务(如重写函数以采用新参数)。 使用codemod时,使用的概念与linter的操作相同。 但是,代码模式没有像lint那样将问题通知程序员,而是自动解决了这个问题。
如何编写一个codemod? 考虑一个例子。 在这里,我们要停止使用
get_global
。 在这种情况下,您可以使用linter,但是不知道修复整个代码将花费多长时间,此外,此任务将分配给许多开发人员。 同时,即使项目使用自动代码更正系统,也可能需要一些时间来处理所有代码。
我们希望摆脱使用get_global并改用实例变量为了解决这个问题,我们可以与检测它的linter规则一起编写一个codemod。 我们认为,允许过时的模式和API逐渐离开代码将分散开发人员的注意力并降低代码的可读性。 我们更喜欢立即删除过时的代码,而不关注它如何从项目中逐渐消失。
鉴于我们的代码量和活跃的开发人员的数量,这通常意味着自动消除过时的设计。 如果我们能够从过时的模式中快速清除代码,则意味着我们可以保持所有Instagram开发人员的工作效率。
那么,如何制作一个codemod? 如何在保留注释,缩进和其他所有内容的同时仅替换我们感兴趣的代码片段? 有一些基于特定语法树的工具(例如LibCST创建的工具),这些工具使您能够以手术精度修改代码并将所有辅助结构保存在其中。 结果,如果我们需要将函数的名称从
fn
更改为在下面的树中
add
,那么我们可以在
Name
节点中写名称
add
而不是
fn
,然后将树写入磁盘!
可以通过将名称add写入Name节点而不是名称fn来完成代码模式。 然后,可以将更改后的树写入磁盘。 您可以在LibCST 文档中阅读有关此内容的更多信息。现在,我们对代码mod有了一些熟悉,让我们看一个实际的例子。 Instagram员工正在努力使项目的代码库完全键入。 科德莫迪在这件事上认真地帮助了他们。
如果我们需要确定一组未类型化的函数,我们可以尝试通过通常的类型推断来生成它们返回的类型! 例如,如果一个函数仅返回一种原始类型的值,我们只需将此类型的返回值分配给该函数。 如果该函数返回逻辑类型的值,例如,如果该函数将某物与某物进行比较或检查某物,则可以为其分配返回值类型
bool
。 我们发现,在使用Instagram代码库进行实际操作的过程中,这是一个非常安全的操作。
找出函数返回的值的类型但是,如果函数没有显式返回任何值,或者隐式返回
None
怎么办? 如果该函数未明确返回任何内容,则可以将其分配为
None
类型。
与前面的示例不同,由于存在开发人员使用的通用模式,这可能更加危险。 例如,在基类方法中,可以引发
NotImplemented
异常,而在重写此方法的子类方法中,可以返回字符串。 重要的是要注意,所有这些技术都是启发式的,但是其应用的结果常常是正确的。 结果,它们可以被认为是有用的。
什么都不返回的函数用Pyre扩展代码模块
让我们更进一步。 Instagram使用Pyre,一种成熟的静态类型检查系统,类似于mypy。 使用Pyre允许我们检查代码库中的类型。 如果我们使用Pyre生成的数据来扩展codemods的功能怎么办? 以下是此类数据的示例。 很容易看出,几乎所有需要自动修复类型注释的东西!
$ pyre ƛ Found 2 type errors! testing/utils.py:7:0 Missing return annotation [3]: Returning `SomeClass` but no return type is specified. testing/utils.py:10:0 Missing return annotation [3]: Returning `testing.other.SomeOtherClass` but no return type is specified.
工作期间的Pyre对每个功能的执行顺序进行详细分析。 结果,该工具有时可能会以很高的概率假设应返回未注释的函数。 这意味着,如果Pyre认为该函数返回简单类型,我们将为该函数分配返回类型。 但是,现在,有可能,我们还需要处理导入命令。 这意味着我们需要知道是否在本地导入或声明了某些内容。 稍后,我们将简要讨论该主题。
通过自动添加易于在代码中显示的类型信息,我们可以获得什么好处? 好吧,类型就是文档! 如果该函数是完全键入的,则开发人员将不必阅读其代码即可查找其调用的功能以及使用其返回内容的功能。
def get_description(page: WikiPage) -> Optional[str]: if page.draft: return None return page.metadata["description"]
我们许多人都遇到过类似的Python代码。 Instagram代码库也有类似的东西。 如果未
get_description
函数,则需要查看几个模块才能找出返回的内容。 同时,即使我们谈论的是更简单的函数,其返回值的类型都易于导出,但与未类型化的函数相比,它们的类型化变体更容易被感知。
此外,如果未完全标注功能,则Pyre不会验证功能主体的正确操作。 在以下示例中,对
some_function
的调用将失败。 最好在代码投入生产之前了解这一点。
def some_function(in: int) -> bool: return in > 0 def some_other_function(): if some_function("bla"):
在这种情况下,我们可以在代码投入生产后很好地找到类似的错误。 事实是
some_other_function
没有返回类型注释。 如果我们使用启发式机制使用自动推导的类型
None
对其进行注释,那么我们将在类型引起问题之前就发现了问题。 这当然是人为的例子,但在Instagram上,此类问题很严重。 如果您有数百万行代码,那么在代码审查过程中,您很可能会错失在一个简单示例中看起来完全显而易见的事情。
在Instagram中,上述基于自动推断类型的方法允许键入大约10%的函数。 结果,人们不再需要手动编辑成千上万的功能。 类型化代码的优点是显而易见的,但是在我们的对话中,这带来了另一个重要的优点。 完全类型化的代码库为使用codemods处理代码提供了更大的可能性。
如果我们信任类型注释,则意味着Pyre可以为我们打开更多的可能性。 让我们再次看一下重命名函数的示例。 如果我们要重命名的实体由类方法而不是全局函数表示,该怎么办?
函数是类方法如果将从Pyre收到的类型信息和重命名函数的代码模式结合在一起,则可以意外地对函数的调用位置和声明的位置进行更正! 在此示例中,由于我们知道
a.fn
构造左侧的
a.fn
,因此我们也知道将此构造更改为
a.add
是安全的。
更高级的静态分析
Python具有四种类型的范围:全局范围,类和函数级范围,嵌套范围范围分析使我们可以使用功能更强大的代码模块。 还记得上面的例子之一,我们谈到添加类型注释也可能意味着需要使用导入命令这一事实? 如果系统分析了范围,则意味着借助于导入命令,我们可以知道文件中使用了哪些类型,哪些是在本地声明的,哪些是缺失的。 同样,如果您知道全局变量被函数参数覆盖,则可以避免在重命名全局变量时意外更改此类参数的名称。
总结
为了更正Instagram代码中的所有错误,我们了解了一件事。 其原因在于,搜索需要修复的代码通常比修复本身更重要。 程序员通常必须解决简单的任务-例如重命名函数,向方法添加参数或将模块分成多个部分。 所有这些都是司空见惯的,但是我们代码库的大小意味着一个人将无法找到需要更改的每一行。 这就是为什么将codemods的功能与可靠的静态分析相结合如此重要的原因。 这使我们可以更自信地找到需要更改的代码部分,这意味着我们可以使代码模式更安全,更强大。
亲爱的读者们! 您是否使用代码模块?
