SQLFluff 规则系统内部机制:sqlfluff.core.rules.base基类架构与自定义规则开发指南
【免费下载链接】sqlfluffA modular SQL linter and auto-formatter with support for multiple dialects and templated code.项目地址: https://gitcode.com/GitHub_Trending/sq/sqlfluff
本篇文章以 SQLFluff 官方文档中 Internal API - Rules 页面为骨架展开。该页面通过 Sphinx
automodule指令将sqlfluff.core.rules.base模块的完整 docstring 渲染为 API 参考文档,是面向插件开发者、规则开发者以及 SQLFluff 贡献者的核心参考页。本文在此基础上深入源码,系统梳理规则引擎的设计思想、基类体系、注册装配流程,并给出可落地的自定义规则开发路径。
SQLFluff 的规则系统负责在解析器产出的语法树上“爬行”并对特定节点求值,最终产出违规报告与自动修复建议。sqlfluff.core.rules.base就是这一切的根基:BaseRule基类、LintResult结果对象、RuleSet/RulePack注册装配机制以及元类层面的命名规范与文档自动生成逻辑全部集中于此。读完本文,你将理解 SQLFluff 的规则是如何被定义、注册、过滤、实例化与执行的,并具备动手编写一条自定义规则的完整知识储备。
文档定位:这份 Internal API 页面向谁服务
在 docs/source/reference/internals/index.rst 中,rules与config、functional、reflow一起组成了 “Internal API” 章节。该章节的开篇说明明确指出:
Anything within this section should only be necessary for people who are developing plugins or rules to interact with SQLFluff on a deeper level or people who've decided to help the project by contributing to SQLFluff.
也就是说,本文所述的规则基类体系是 SQLFluff 的“内部 API”,普通用户一般只需要在配置文件中启用/禁用规则,而插件作者、规则开发者与项目贡献者才需要深入这层 API。这与 docs/source/guides/setup/developing_custom_rules.rst 等开发向导互为表里——前者讲“怎么用 API 写规则”,本页则讲“这套 API 本身长什么样、为什么这么设计”。
规则引擎的核心设计思想
sqlfluff.core.rules.base模块开头的模块级 docstring 一句话概括了规则引擎的本质:
Rules crawl through the trees returned by the parser and evaluate particular rules. The intent is that it should be possible for the rules to be expressed as simply as possible, with as much of the complexity abstracted away.
即:规则在解析器返回的语法树上“爬行”(crawl),并对特定节点求值(evaluate)。设计目标是把尽可能多的复杂性抽象掉,让每条规则的表达尽量简单。
该 docstring 还给出了一个对规则开发者至关重要的定位约定:
The evaluation function should take enough arguments that it can evaluate the position of the given segment in relation to its neighbors, and that the segment which finally "triggers" the error, should be the one that would be corrected OR if the rule relates to something that is missing, then it should flag on the segment FOLLOWING, the place that the desired element is missing.
- 触发错误的锚点段(anchor)应当是“将被修复的那个段”,让报错位置与修复位置对齐;
- 如果规则针对的是“缺失的元素”(例如缺空格、缺逗号),则应该在缺失位置之后紧邻的那个段上打点报错,而不是指向“本来应该存在却不存在”的虚无位置。
这个约定贯穿了LintResult.anchor、LintFix.anchor以及后续锚点自动调整逻辑的设计。
RuleMetaclass:命名规范、代码提取与文档自动生成
BaseRule使用自定义元类RuleMetaclass(见 src/sqlfluff/core/rules/base.py)来驱动三个关键机制:规则命名校验、代码与描述提取、docstring 自动富化。
规则命名规范(自动提取 rule code)
规则类必须遵循严格的命名格式,元类通过预编译正则Rule_?([A-Z]{1}[a-zA-Z]+)?_([A-Z0-9]{4})校验并提取信息:
- 核心规则:
Rule_LLNN,其中L为字母、N为两位数字,例如Rule_CP01(CP = CaPitalisation); - 兼容旧格式:单字母 + 三位数字的
LNNN,例如历史上L010; - 插件规则:
Rule_PluginName_LL23格式,PluginName部分会被拼进 code,形成如CP01_plugin_name的插件专属规则码。
如果类名不符合规范,元类会抛出SQLFluffUserError。规则描述(description)则取类 docstring 的第一行(将反引号替换为单引号后截取)。
docstring 自动富化(为 Sphinx 文档服务)
元类会扫描规则类的 docstring,在预编译正则匹配到的Anti-pattern / note / Configuration标记处,自动插入以下内容块:
- 若
is_fix_compatible = True,标注 “This rule issqlfluff fixcompatible.”; - Name:规则的
name属性(如capitalisation.keywords); - Aliases:规则别名列表(如
L010); - Groups:规则所属分组(如
all、core、capitalisation); - Configuration:遍历
config_keywords,从config_info中取出每个配置项的 definition 与 validation,自动生成配置文档。
这正是automodule渲染出的规则文档中 “Configuration” 一节内容的来源。同时元类还会:
- 从父类继承
groups与config_keywords(避免 CP02 这类继承规则在文档中丢失分组信息); - 校验
name必须是全小写 snake_case,可用.表达命名空间(如layout.spacing); - 若插件规则在插件加载完成前被导入,会输出性能警告日志,提示插件应在
get_rules()方法内导入规则定义。
BaseRule:一切规则的基类
BaseRule(src/sqlfluff/core/rules/base.py)定义了规则的完整生命周期,先看它暴露的类级属性(规则子类通过覆写这些属性来声明自身行为):
| 属性 | 默认值 | 作用 |
|---|---|---|
name | "" | 规则的人类可读名称,如layout.spacing,作为配置查找引用 |
groups | () | 规则分组元组,用于批量选择规则,必须包含"all" |
aliases | () | 规则别名,通常用于兼容旧规则码(如 LT01 的L001) |
code/description | 由元类自动设置 | 规则码与描述,不应手动赋值 |
is_fix_compatible | False | 是否支持sqlfluff fix自动修复 |
config_keywords | [] | 该规则支持的自定义配置项名列表 |
crawl_behaviour | 必须覆写 | 规则使用的爬虫(crawler)实例 |
lint_phase | "main" | 规则执行阶段;"post"表示“不期望再触发下游规则”的规则(如大小写修复),在主阶段首轮与第二轮 linter pass 中运行 |
_works_on_unparsable | True | 是否在无法解析的段上工作 |
_adjust_anchors | False | 是否对修复锚点做自动上提(hoisting)调整 |
targets_templated | False | 规则是否针对模板化代码段 |
template_safe_fixes | False | 声明该规则的修复在模板元素附近是安全的,可跳过默认安全检查 |
实例化时(__init__),所有从配置传入的 kwargs 会被逐一写入实例的__dict__,供规则方法直接访问;同时会校验每个config_keywords声明的选项确实出现在 kwargs 中,否则抛出ValueError并提示补充到default_config.cfg或插件配置。
规则求值接口_eval
def _eval(self, context: RuleContext) -> EvalResultType: """Evaluate this rule against the current context. Returns: :obj:`LintResult`, list of :obj:`LintResult` or :obj:`None`. """_eval是每条规则必须覆写的方法,基类实现直接抛出NotImplementedError。它接收一个RuleContext,返回三种结果之一:
None:无问题(也意味着不传递 memory);- 单个
LintResult:一个违规(可能附带修复与 memory); list[LintResult]:多个违规,memory 取自列表最后一个元素。
方法名刻意用_eval而非eval,是为了配合 Sphinx autodoc 让文档自动生成更友好。规则开发者需要显式声明自己依赖的 context 字段,同时为了兼容性应接受**kwargs。
主执行入口crawl
crawl()是规则对整棵语法树执行一次的入口,返回四元组:(violations, raw_stack, fixes, memory)。其流程为:
- 构造根
RuleContext(携带 dialect、fix 标志、templated_file、文件路径、segment、config); - 尝试Rust 原生分发(见下文):若
core.use_rust_rules开启且解析产物带 Rust arena(_rs_tree),则调用_eval_rust; - 否则进入 Python 路径:遍历
self.crawl_behaviour.crawl(root_context)产生的每个子上下文,将上一段的memory注入当前上下文后调用_eval; - 对每个
LintResult依次执行_adjust_anchors_for_fixes(锚点调整)与_process_lint_result(模板安全校验、noqa 掩码过滤、不可解析过滤),最终汇聚为SQLLintError列表与LintFix列表。
crawl内部对规则执行过程中的任何异常做了可恢复处理:异常不会被直接抛出导致整个文件 lint 失败,而是被记录为一条SQLLintError(描述中包含 “Unexpected exception”,并提示用户可用-- noqa: <code>忽略),保证用户至少能得到部分结果。bdb.BdbQuit与KeyboardInterrupt除外,二者会被重新抛出。
模板安全与锚点调整
_process_lint_result在最终上报前会检查:违规锚点及其所有父段是否通过 crawler 的passes_filter(即是否位于不可解析区域);discard_unsafe_fixes负责丢弃“不安全”的修复:触及模板化代码的修复(fix.has_template_conflicts),以及跨越多个模板块(block)的修复(对应 issue #3079)会被整体丢弃——此时违规仍会报告,但被标记为不可自动修复;_adjust_anchors_for_fixes与_choose_anchor_segment实现锚点“上提”(hoist):当规则(如 LT02/LT05 这类空白处理规则)返回的锚点位于语法树过深的叶节点时,会沿父链向上寻找“允许非代码端点”(can_start_end_non_code)的祖先,把create_before/create_after修复挂到更可靠的锚点上,避免破坏解析树结构。
Rust 原生分发(_eval_rust)
作为实验性加速路径,规则可以覆写_eval_rust,在core.use_rust_rules开启且解析产生 Rust arena 时,一次性对整个 arena 计算全部LintResult,结果与_eval结果走相同的后处理管线。未实现 Rust 路径的规则(返回None)或使用 Python 解析器(无 arena)时会自动回退到 Python 爬行路径。CP01/CP03/CP04 等大小写类规则已提供该实现(见 src/sqlfluff/rules/capitalisation/CP01.py)。
LintResult:一次求值的结果
LintResult(src/sqlfluff/core/rules/base.py)是_eval的返回值对象,构造函数参数如下:
| 参数 | 说明 |
|---|---|
anchor | 表示问题位置的段。anchor 为None意味着“没有问题” |
fixes | 可修复此问题的LintFix列表;缺省表示该问题需手动修复 |
memory | 规则的工作记忆,会被传递给下一个被爬行的段 |
description | 覆盖规则默认描述的自定义问题描述 |
source | 结果来源标识字符串,在 reflow 等大型库中用于追踪结果来源 |
LintResult.to_linting_error(rule)将结果转换为SQLLintError(src/sqlfluff/core/errors.py):使用description or rule.description作为违规描述,若 anchor 为空则返回None(不产生违规)。其__repr__以LintResult(<empty>)表示空结果,带修复时以+NF后缀标明修复数量,方便在日志中排查。
LintFix:修复操作对象
LintFix(src/sqlfluff/core/rules/fix.py)描述一次修复动作,核心字段为:
edit_type:create_before、create_after、replace、delete四种之一,其余取值直接抛ValueError;anchor:修复应用位置的段——删除时表示被删段;创建时表示插入位置(原有元素将被推到编辑之后);替换时表示被替换段;edit:replace/create类型要插入的段的可迭代对象;source:提供代码来源的段,linter 依赖它防止从模板区域复制内容。
构造时的关键校验与行为:
- 创建类型修复必须非空且每个 edit 段的 raw 非空;
replace修复不得“用段替换它自己”;- edit 段会被deep copy并剥离预设的
pos_marker(位置标记由后续 realignment 统一计算,避免污染原始解析结构); - 对替换成同 raw 的“纯 source 编辑”(
is_just_source_edit)有专门识别,用于安全地传播模板 source fix。
to_dict()将修复序列化为结构化 dict(含行号/列号),是sqlfluff fix输出与测试断言的基础。
RuleContext:传给_eval的上下文
RuleContext(src/sqlfluff/core/rules/context.py)是一个 dataclass,分两组字段:
- 文件内不变:
dialect、fix(是否处于修复模式)、templated_file、path(文件路径)、config(FluffConfig); - 文件内随爬行变化:
segment(当前段)、parent_stack(从根到当前段的祖先链)、raw_stack(截至当前的原始段序列)、memory(规则任意存储)、segment_idx(当前段在父节点中的索引)。
并提供了两个便捷属性:siblings_pre(当前段之前的兄弟段)与siblings_post(之后的兄弟段)。在SegmentSeekerCrawler中,为性能考虑会原地修改同一个RuleContext实例(避免每个段都新建对象产生海量短生命周期对象),并在递归返回时重置字段。
爬虫体系:crawlers.py
规则在树上的“爬行策略”被抽象为爬虫(crawler)类(src/sqlfluff/core/rules/crawlers.py):
BaseCrawler:抽象基类,核心方法是passes_filter(segment)——默认拒绝unparsable类型的段(除非works_on_unparsable=True),该过滤在爬行与结果处理两处都会用到;RootOnlyCrawler:只在文件根段上求值一次,用于 LT01 这类“全局视角”的规则;SegmentSeekerCrawler(types, provide_raw_stack, allow_recurse):按段类型集合高效搜索目标段:- 通过
descendant_type_set求交集做激进剪枝——子树中不含目标类型时直接跳过整棵子树; allow_recurse=False时,一旦某段匹配,其子段不再返回(适合“一个根段在同一轮求值中检查所有子段”的场景);provide_raw_stack=True时维护原始段栈(涉及较多元组操作,默认关闭以节省开销);
- 通过
ParentOfSegmentCrawler:搜索“直接子段包含指定类型”的父段,基于direct_descendant_type_set匹配。
规则通过覆写crawl_behaviour选择爬虫:CP01 使用SegmentSeekerCrawler({"keyword", "binary_operator", "date_part"})(并排除literal及其 data_type 等父类型),LT01 则使用RootOnlyCrawler并以ReflowSequence统一处理整段间距。
RuleSet/RulePack/RuleManifest:注册、过滤与装配
注册:RuleSet.register
标准规则集在 src/sqlfluff/core/rules/init.py 的_load_standard_rules()中构建:创建RuleSet(name="standard", config_info=get_config_info())后,遍历插件管理器(pluggy)的get_rules()hook,把每个规则类注册进标准集。get_ruleset()每次调用都会重建并返回一份 copy,以支持运行时动态规则变更。
register装饰器(src/sqlfluff/core/rules/base.py)完成:
- 代码冲突检测(同一规则码二次注册直接抛错);
- 强制要求规则属于
"all"组; - 将类元数据封装为
RuleManifest(code / name / description / groups / aliases / rule_class)存入注册表。
@myruleset.register class Rule_LT01(BaseRule): "Description of rule." def eval(self, **kwargs): return LintResult()引用映射:codes > names > groups > aliases
rule_reference_map()构建规则引用映射:先以 code 自映射,再并入 name 映射、group 映射、alias 映射。优先级为 codes > names > groups > aliases——当出现碰撞时,低优先级引用(如别名与规则名重复)会被丢弃并给出警告。该映射让用户在配置中既可用规则码(LT01)、也可用规则名(layout.spacing)、分组(capitalisation)、别名(L001)乃至glob 通配(LT0*)来批量选择规则。
装配:get_rulepack
get_rulepack(config)是配置到规则实例的关键转换函数,流程为:
- 校验通用配置项的取值合法性(
_validate_config_options,非法值抛SQLFluffUserError); - 校验配置文件中是否有针对未知规则的配置段,发现则告警(提示正确的
sqlfluff:rules:<name>写法); - 计算 allowlist(默认取全部规则码)与 denylist,并对未知引用发出警告;
- 通过
_expand_rule_refs用fnmatch展开 glob 引用,最终得到过滤后的规则码列表; - 对每条规则:合并通用规则配置与非 dict 的特定规则配置段,注入
code与经过变量替换(.format(**kwargs))的description,最后实例化规则类; - 返回
RulePack(rules, reference_map)。
RulePack是“过滤后待应用的规则包”,之所以在主进程中完成过滤与实例化,是为了在多进程模式下让用户自定义规则可以被安全地引用(pickle 传递)。其reference_map与codes()方法配合 noqa 注释解析使用。
实战:结合实例理解一条规则的全貌
以 CP01(src/sqlfluff/rules/capitalisation/CP01.py)为例,看一条真实规则的声明方式:
class Rule_CP01(BaseRule): """Inconsistent capitalisation of keywords. **Anti-pattern** In this example, ``select`` is in lower-case whereas ``FROM`` is in upper-case. .. code-block:: sql select a FROM foo **Best practice** Make all keywords either in upper-case or in lower-case. .. code-block:: sql SELECT a FROM foo -- Also good select a from foo """ name = "capitalisation.keywords" aliases = ("L010",) groups: tuple[str, ...] = ("all", "core", "capitalisation") is_fix_compatible = True lint_phase = "post" crawl_behaviour = SegmentSeekerCrawler({"keyword", "binary_operator", "date_part"}) config_keywords = ["capitalisation_policy", "ignore_words", "ignore_words_regex"]可以看到 docstring 中Anti-pattern(反例)与Best practice(正例)的双栏结构——这正是元类插入 Name/Aliases/Groups/Configuration 元数据的锚点位置,也是 Sphinx 文档中规则示例的来源。而 LT01(src/sqlfluff/rules/layout/LT01.py)则展示了“一个规则整合多个历史规则”的别名模式(aliases = ("L001", "L005", "L006", "L008", "L023", "L024", "L039", "L048", "L071")),其_eval借助 src/sqlfluff/utils/reflow/sequence.py 的ReflowSequence完成间距重置。
如果你要写一条自定义规则,推荐路径为:
- 阅读 docs/source/guides/setup/developing_custom_rules.rst 与 docs/source/guides/contributing/rules.rst;
- 参考官方示例插件 plugins/sqlfluff-plugin-example,在插件的
get_rules()中返回规则类,规则码按Rule_PluginName_LL23命名; - 按本文所述约定实现
_eval(或_eval_rust)、声明crawl_behaviour、groups、config_keywords; - 在插件的
default_config.cfg或pyproject.toml中补充规则配置项,并在config_info中登记定义与校验取值(见 src/sqlfluff/core/rules/config_info.py 中的STANDARD_CONFIG_INFO_DICT,如ignore_words、blocked_words等通用项); - 用 test/fixtures/rules/std_rule_cases 中的 YAML 用例驱动测试(参考 test/rules/std_test.py 与 test/rules/yaml_test_cases_test.py)。
小结
SQLFluff 规则系统的精巧之处在于“约定优于配置”与“抽象掉复杂度”:
- 元类自动完成命名解析、描述提取、文档富化与配置校验,让每条规则的 docstring 同时成为用户文档;
BaseRule._eval+ 爬虫把“在树上找什么、怎么看”拆分为两个正交维度,规则开发者只需关注求值逻辑;LintResult/LintFix/RuleContext构成一套声明式的结果协议,模板安全与锚点修正等易错细节由框架统一兜底;RuleSet/RulePack以 codes/names/groups/aliases 多级引用 + glob 展开完成规则的过滤、配置注入与实例化,支撑了配置文件中的灵活选择语法。
对想要深入 SQLFluff 内部或为其编写插件的开发者而言,docs/source/reference/internals/rules.rst 所指向的sqlfluff.core.rules.base模块是必读的起点;配合 functional(函数式遍历 API)与 reflow(重排引擎)两个相邻的 Internal API 页面,即可完整掌握 SQLFluff 的规则开发全景。
【免费下载链接】sqlfluffA modular SQL linter and auto-formatter with support for multiple dialects and templated code.项目地址: https://gitcode.com/GitHub_Trending/sq/sqlfluff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考