大量Python代码的静态分析:Instagram经验。 第一部分

Instagram服务器代码仅用Python编写。 好吧,基本上是这样。 我们使用了一个小的Cython,并且依赖项包括很多C ++-可以像C扩展一样从Python操作代码。



我们的服务器应用程序是一个整体,它是一个大型代码库,包含数百万行,包括数千个Django端点( 这里是有关在Instagram上使用Django的讨论)。 所有这些都已加载并用作单个实体。 从整体中分配了一些服务,但是我们的计划不包括对整体的强烈分离。

我们的服务器系统是一个经常变化的整体。 每天,数百名程序员对代码进行数百次提交。 我们不断部署这些更改,每隔七分钟执行一次。 结果,该项目每天大约要部署一百次。 我们努力确保从提交到master分支到在生产环境中部署相应代码之间的时间少于一小时( 这是在PyCon 2019上进行的讨论)。

维护这个庞大的整体代码库非常困难,每天要对其进行数百次提交,同时又不会使其陷入完全混乱的状态。 我们希望使Instagram成为程序员提高生产力并能够快速准备系统新有用功能的地方。

本材料重点介绍如何使用linting和自动重构来简化管理Python代码库的过程。

如果您有兴趣尝试本材料中提到的一些想法,那么您应该知道我们最近转移到了开源项目LibCST类别,该项目是我们许多用于lint和自动代码重构的内部工具的基础。

第二部分

棉绒:在需要的地方出现文件


Linting帮助程序员发现和诊断开发人员自己可能在不注意代码的情况下可能不了解的问题和反模式。 这对我们来说很重要,因为与代码设计有关的想法越难以分发,程序员在项目上的工作就越多。 就我们而言,我们正在谈论数百名专家。


各种棉绒

Linting只是我们在Instagram上使用的多种静态代码分析类型之一。

实现棉绒规则的最原始方法是使用正则表达式。 正则表达式易于编写,但是Python 不是“正则”语言 。 结果,很难(有时甚至是不可能)使用正则表达式可靠地搜索Python代码中的模式。

如果我们谈论实现Linter的最复杂和最先进的方法,那么将有诸如mypyPyre之类的工具。 这是两个用于静态检查可以执行深度程序分析的Python代码类型的系统。 Instagram使用Pyre。 这些是功能强大的工具,但是很难扩展和自定义。

当我们在Instagram上谈论linting时,通常是指基于抽象语法树的简单规则。 正是这样的东西构成了我们自己的服务器代码插入规则的基础。

当Python执行模块时,它首先启动解析器并将源代码传递给它。 因此,创建了一个分析树-一种具体的语法树(CST)。 该树是输入源代码的无损表示。 每个细节都保存在该树中,例如注释,方括号和逗号。 基于CST,您可以完全还原原始代码。


lib2to3生成的Python 解析树(CST的变体)

不幸的是,这种方法导致了复杂树的创建,这使得难以从中提取我们感兴趣的语义信息。

Python将解析树编译成抽象语法树(AST)。 在转换过程中,有关源代码的某些信息会丢失。 我们正在谈论“其他语法信息”,例如注释,方括号和逗号。 但是,AST中代码的语义得以保留。


ast模块生成的Python抽象语法树

我们开发了LibCST-一个可以让我们充分了解CST和AST的库。 它给出了存储所有有关它的信息的代码表示形式(如在CST中),但是很容易从这样的代码表示中提取有关它的语义信息(如使用AST时)。


特定LibCST语法树的表示

我们的整理规则使用LibCST 语法树在代码中查找模式。 这个语法树在较高层次上很容易研究,它使您摆脱了“不规则”语言带来的与工作有关的问题。

假设在某个模块中由于类型导入而存在循环依赖性。 Python通过将类型导入命令放在if TYPE_CHECKING块中来解决此问题。 这样可以防止在运行时导入任何内容。

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType 

后来,有人在代码中添加了另一个类型导入和另一个if块。 但是,执行此操作的人员可能不知道模块中已经存在这种机制。

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType if TYPE_CHECKING: #   !    from other_package import OtherType 

您可以使用linter规则摆脱这种冗余!

让我们首先初始化代码中找到的“保护”块的计数器。

 class OnlyOneTypeCheckingIfBlockLintRule(CstLintRule):    def __init__(self, context: Context) -> None:        super().__init__(context)        self.__type_checking_blocks = 0 

然后,在满足相应条件的情况下,我们增加计数器,并检查代码中是否有不超过一个这样的块。 如果不满足此条件,我们将在代码中的适当位置生成警告,并调用用于生成此类警告的辅助机制。

 def visit_If(self, node: cst.If) -> None:    if node.test.value == "TYPE_CHECKING":        self.__type_checking_blocks += 1        if self.__type_checking_blocks > 1:            self.context.report(                node,                "More than one 'if TYPE_CHECKING' section!"            ) 

通过查看LibCST树并收集信息,可以使用类似的整理规则。 在我们的linter中,这是使用Visitor模式实现的。 您可能已经注意到,规则覆盖了visit方法,并保留了与节点类型相关联的方法。 这些“访客”以特定顺序被调用。

 class MyNewLintRule(CstLintRule):    def visit_Assign(self, node):        ... #      def visit_Name(self, node):        ... #        def leave_Assign(self, name):        ... #      


在访问节点的后代之前,将调用访问方法。 拜访所有后代后,将调用离开方法

我们坚持工作原则,首先要解决简单的任务。 我们自己的第一个linter规则是在单个文件中实现的,其中包含一个“访问者”并使用了共享状态。


一个文件,一个“访问者”,使用共享状态

Single Visitor类必须具有与我们所有与其不相关的整理规则的状态和逻辑有关的信息。 而且,并不总是很明显哪个状态对应于特定规则。 这种方法在确实存在一些自己的掉毛规则的情况下很好地展示了自己,但是我们有大约一百条这样的规则,这大大增加了single-visitor模式的支持。


很难知道每种检查与哪些状态和逻辑相关联。

当然,作为解决此问题的可能方法之一,我们可以考虑几个“访客”的定义以及这样一个工作计划的组织,以使他们每个人每次都可以看整个树。 但是,这将导致生产率严重下降,并且短绒棉绒是一个程序,应该可以快速运行。


每个短绒规则可以重复遍历一棵树。 处理文件时,规则将按顺序执行。 但是,这种通常遍历树的方法将导致性能严重下降。

我们没有在自己内部实现类似的功能, 而是从其他编程语言的生态系统中使用的短毛绒 (例如来自JavaScript的ESLint)得到启发,并创建了一个“访客”集中注册器(“访客注册表”)。


“访客”集中登记册。 我们可以有效地确定哪个节点对linter的每个规则感兴趣,从而节省了对它不感兴趣的节点的时间。

初始化linter规则后,规则方法的所有替代都存储在注册表中。 当我们绕着树走时,我们查看所有注册的“访客”并称呼他们。 如果未实现该方法,则意味着您无需调用它。

当添加新的插入规则时,这可以减少系统对计算资源的消耗。 我们通常会用短绒棉毛检查一些最近修改的文件。 但是我们可以在短短26秒内并行检查整个Instagram服务器代码库上的所有规则。

解决性能问题之后,我们创建了一个测试框架,旨在遵循高级编程技术,要求在某些事物应具有一定质量的情况下以及在某些事物应具有某些质量的情况下进行测试应该。

 class MyCustomLintRuleTest(CstLintRuleTest):    RULE = MyCustomLintRule       VALID = [        Valid("good_function('this should not generate a report')"),        Valid("foo.bad_function('nor should this')"),    ]       INVALID = [        Invalid("bad_function('but this should')", "IG00"),    ] 

延期→ 第二部分

亲爱的读者们! 你用棉短绒吗?


Source: https://habr.com/ru/post/zh-CN473766/


All Articles