Skip to content

refactor: Rewrite - #9

Merged
gaojunran merged 166 commits into
mainfrom
dev-rust
Nov 3, 2026
Merged

gaojunran merged 166 commits into
mainfrom
dev-rust

Conversation

@gaojunran

Copy link
Copy Markdown
Member

No description provided.

junrangao added 30 commits October 22, 2026 14:55
删掉 1.x 的 TS 实现。重写为 workspace:
- skylint-core:与 minidev 的唯一接触面(Project::load)把项目目录变成可被规则消费的
  事实(路由、四件套实际源文件、组件闭包、Skyline 页面判定),只读原始 JSON;
  Diagnostic/Report 形状在这里定义,不依赖上游 DTO。
- skylint-cli:clap,--format text/json,退出码 0/1/2。

minidev 以普通版本依赖声明,开发期由 .cargo/config.toml 的 [patch.crates-io]
指向本地 checkout(实测 git 依赖 + [patch] 仍会 fetch 原地址,[source] 替换 git→git
不被支持)。公开仓库因此不出现内网地址。

规则尚未接入。计划与进度见 docs/PLAN.md。
AppConf::comps 只做词法解析(usingComponents 的值直接拼到引用方目录),
my-lib/button 这类 npm 裸名会被静默丢掉——miniprogram_npm 回溯只存在于
check_component_path。lint 不能依赖这个不完整的闭包。

改为以 AppConf::comps 为种子(兜住 appBar / custom-tab-bar / agent.skills
等非声明式入口),再从页面与 app 的 usingComponents 出发 BFS,npm 分支交给
编译器的 resolve_component_path,保证与 build 同语义。

minidev 的 collect_comps 不动(疑似 parity 区,缺少 devtools oracle 无法判定)。
真实工程对照:31 页混合项目输出 31 页 / 3 skyline,与独立脚本逐页统计一致。
视图(input/):只做「解析 + 定位」,不含 Skyline 语义判断。
- wxml.rs 基于 glass-easel-template-compiler,结构包装层(block/wx:if/wx:for)
  摊平进父级,使规则看到的是渲染树;位置把 UTF-16 列转成 1-based 字符列。
- wxss.rs 基于 cssparser 的轻量 CST:选择器原文、声明(名/值/important/位置)、
  @media 上下文、@import 目标。容错优先,坏块丢弃而非整文件失败。

规则(rules/):
- skyline/no-inline-text:文本与元素混排时报(纯文本子节点不算缺陷——
  生产工程里 <view>{{text}}</view> 在 Skyline 上正常渲染,知识库的唯一反例也是
  混排)。真实工程校准:miniprogram-novel 122 → 2,miniprogram-skit_1 196 → 0。
- skyline/no-pseudo:白名单为知识库 changelog 明确支持的
  ::before/::after/:not/:only-child/:empty/:host。
- skyline/box-sizing:只支持 content-box/border-box(padding-box 不支持),
  并提示默认值从 WebView 的 content-box 变成 border-box。
- skyline/flex-direction:display:flex 同规则内未声明 flex-direction 时报,
  Skyline 默认 column 会把 WebView 的横向布局变纵向。

规则不按 is_skyline 过滤页面:这是迁移工具,受众是还在 WebView 上的工程;
只查已迁移页面会让纯 WebView 项目一条都不报。is_skyline 保留为工程视图元数据。
- skyline/unsupported-component:组件黑白名单来自 skyline-overview 的
  component-support.md(54 支持 / 9 不支持),数据放在 rules/data/components.txt,
  不在 Rust 里手写 match。黑名单带替代方案,直接进 help。
- skyline/scroll-view-type:缺 type 报 warning(性能退化),静态非法值报 error,
  动态值不报。
- input/wxml.rs 补属性遍历:原名(含 bind:/catch:/aria-*)、静态值、
  「是否为静态值」、属性与标签名位置。
- --rules 从 Registry 生成 markdown 规则表,README 不再手写规则清单。

真实工程:miniprogram-novel 1 error(open-webview 页的 web-view)、
businessview 9 error(navigation-bar ×8、web-view ×1),均为真阳性。
`<!-- skylint-disable-next-line [规则...] -->` 抑制紧接的下一行;
WXSS 侧用 `/* */` 同样识别(两种外壳统一接受,不按扩展名分支)。
读取失败的文件原样保留诊断——宁可多报也不静默丢。
规则从 6 条扩到 14 条,按判断形状拆成三个文件:
- baseline.rs:手写判定的 no-pseudo / box-sizing / flex-direction
- dict.rs:数据驱动的 unsupported-property / unsupported-value
- shape.rs:值结构类 gradient-limit / multi-value-limit / filter-limit /
  text-decoration-line / calc-angle / mask-image-gradient

支持表 rules/data/wxss_support.txt:20 个完全不支持的属性 + 27 个不支持的值,
每条行尾标出处(SKILL.md / references/*.md)。整值精确匹配,含 var()/env()/{{}}
的值跳过,不做简写展开与级联——宁可漏报。

真实工程:businessview 新增 7 个 error(overflow-y:auto ×3、font-weight:bolder ×4,
均为 weui 代码里真实存在的不支持写法)。
结构规则(rules/wxml/structure.rs,数据表 rules/wxml/data/scroll_family.txt):
- scroll-view-family:sticky-section 必须在 custom scroll-view 内、list-view/grid-view 的
  挂载位置、nested-scroll-header/body 必须在 nested 里、draggable-sheet 内部 scroll-view
  的 associative-container。
- horizontal-scroll-enable-flex:scroll-x 未开 enable-flex。
- 撤销 sticky-header 位置约束:sticky.md 与 scroll-view.md:54 自相矛盾,生产工程里
  <nested-scroll-header><sticky-header> 正常工作。

支持表按引擎源码逐条核对(skyline/css_properties/*.cc + css_parser_impl.h):
- 删 direction / overflow-x / overflow-y —— 引擎接受并生效,文档说错了;
  分轴 overflow 的 auto/scroll 仍被拒,改按值收录。
- 补 mask-position、display:inline-grid、mask-repeat: space/round。
- contain 的替代方案 -wx-contain 同样 kInvalid,文档推荐了不可用的属性。

真实工程:miniprogram-skit_1 0 error(此前 sticky-header 那条是误报),
businessview 16 error / 91 warning,miniprogram-novel 1 error / 11 warning。
默认输出从单行 `path:line:col  severity  rule  message` 换成 miette 的图形报告,
和 minidev 的错误出口同一套(GraphicalReportHandler + unicode/unicode_nocolor):
规则 id 作为 code 头部,片段头给 path:line:col,下划线盖住出问题的 token,help 在末尾。

几个实现要点:
- 用 NamedSource 带上文件名,否则 miette 只渲染 `╭─[2:4]`,没有路径;
- miette 的 SourceSpan 是「起点 + 长度」,不是起止偏移——按起止传会画出跨十几行的块;
- 跨行区间收窄到起始行尾:<scroll-view> 的属性常常换行,整段下划线读起来像整块代码错了;
- 只给起点(工程级 warning)时标出该字符,caret 才有落点;
- 关掉行折行:WXML 常整段一行,折成 ╭─▶ 多段反而读不出来;
- 非 TTY 自动关色,CI 日志不留转义序列。

TTY 下按 COLUMNS 决定片段宽度,缺省 100。
pages/open-webview/open-webview.json 里写的是 "renderer": "webview",
这页永远不会跑 Skyline,对它报「不支持 <web-view>」是误报。

和不按 is_skyline 过滤不矛盾,两者是不同的东西:
- app 级 webview / 页面没表态 → 只是还没迁,照样检查(目标用户就是他们);
- 页面 json 自己写了 renderer=webview → 明确的「这页不迁」,整页跳过。

PageUnit 加 opted_out 单独记这一位,wxml_files 里按页过滤。
组件 wxml 没有 renderer 可言,一律检查;wxss 无法按页归属(@import/app.wxss 共享),不跳。

真实工程验证(miniprogram-novel):修改前 1 error(正是 pages/open-webview/
open-webview.wxml:3:6 的 <web-view>),修改后 0 error / 11 warning,其余诊断不变。
三件事,一起落地:

1. **规则层拆文件**:16 条规则原本挤在 5 个文件里(baseline/structure/dict/shape),
   最长的 shape.rs 989 行。现在一条规则一个文件(文件名 = 规则 id 去掉 skyline/ 前缀),
   共用部分收进 4 个模块:wxml/mod.rs(遍历 + 组件表 + 测试脚手架)、
   wxss/mod.rs(visit + 测试脚手架)、wxss/dict_table.rs(支持表解析)、
   wxss/value_util.rs(值分词)。判定逻辑、id、severity、文案逐字未改,130 个测试全过。

2. **样式图**(Project.styles):wxss 不再是「单元一个文件」。页面 / 组件 / app.wxss
   作为入口,@import 展开传递闭包,每条样式文件都带上「会被哪些页面/组件的 wxml 消费」。
   跨文件规则要的正是这张表;Project::scope_wxml / global_wxml 提供作用域 → wxml 的映射。
   顺带修掉一个真问题:@import 进来的样式文件以前从未被检查。
   真实工程实测:businessview 16e/91w → 50e/538w,新增全部在 @import 目标(weui.wxss 等)。

3. **skyline/text-overflow-ellipsis**:text-overflow: ellipsis 在 Skyline 下只对 <text>
   生效。判定是「选择器命中的节点及其后代有没有 <text>」(声明可继承)。新增
   wxss/selector.rs 只解析能静态判定的形状(tag / .class / #id),组合符、伪类、
   属性选择器、动态属性一律 Unknown → 放过(宁可漏报)。逗号分隔的选择器各判各的。

已知盲区(记在 PLAN §4 P3):三个真实工程的样式源几乎都是 .less/.scss,
cssparser 不实现嵌套展开,所以样式规则目前只作用于 .wxss 源文件。
样式规则只解析 .wxss:cssparser 不展开 .less/.scss 的嵌套与变量,硬解析会静默漏判。
对 .less/.scss 工程来说,这就成了「0 问题」的假象——三个真实工程里有两个属于这种
(miniprogram-skit_1 只有 0 个 wxss、67 个 less)。

所以在摘要后加一行,只统计**可达**的样式源(页面/组件/app.wxss 入口解析到的,
未引用的文件本来也不会被编译):

  未检查:40 个 .less 样式源(暂不支持预处理器,这些文件的样式规则没有执行)

JSON 对应 summary.skippedStyles: {"less": 40},为空时字段不出现(对既有消费者无影响)。
不解析预处理器、不 lint 编译产物——两条路线都留待单独决策。
知识库的迁移 FAQ 里有一条「Skyline 下 tag 和 id 选择器不支持跨组件匹配」,修法是
rendererOptions.skyline.tagNameStyleIsolation: "legacy"。落地前先读了引擎源码确认边界
(skyline/style/selector_matcher.cc):

- 页面/组件样式表走 MatchScopedRules,命中前要求 node.StyleScopeId() == rule_scope_id,
  标签选择器在 legacy 下改为查全局标签规则表(GetGlobalTagNameRuleSet)放宽;
- id 选择器那段**没有 legacy 分支**,所以 id 跨组件在 Skyline 下无解,只能改 class;
- app.wxss 走 MatchGlobalRules,tag/id/class 都按全局匹配,本来就不分作用域,
  因此规则只在 page / component 样式表里判——app.wxss 报了就是纯误报。

判定依据是「选择器命中了别的单元的节点」而不是「用了标签选择器」:后者会在
`page {}` 这类正确写法上误报。为此 Project 记下 deps(单元 → 直接引用的组件),
新增 page_components() / component_pages() 两个传递闭包(跨文件规则都要用这张图,
原计划里 minidev M7 已撤销,skylint 自己建)。

8 个测试覆盖:页面标签命中组件、id 命中组件(legacy 也照报)、组件标签命中页面、
class 不报、app.wxss 不报、未引用的组件不算、`page {}` 不算、legacy 放行标签。

真实工程(4 个)0 命中,核对后确认是真的没这种写法:非全局 wxss 里的纯标签选择器
只有 `page {}`。规则是防御性的,只在确有跨单元命中时开口。
三个真实工程的样式源几乎都是预处理器(novel 27 less / 1 wxss,skit_1 67 less / 0 wxss,
businessview 82 scss / 13 wxss),而样式规则只解析 .wxss——对它们等于一条都没跑。

原计划是「cssparser 解不了嵌套,列成已知盲区 + 诚实告警」。实测后判定这条路不可修:
不是「缺一个嵌套展开」,而是 cssparser 的容忍模型和预处理器语法冲突——SASS 允许声明不写
分号、LESS 用 // 注释,cssparser 遇到就整块丢弃,再叠加不展开嵌套。两个前提都不成立。

改成自写扫描器,替换 cssparser(依赖表因此少一项)。同一套代码吃 .wxss / .less / .scss,
差异只在摊平嵌套:

- `.a { .b { … } }` → `.a .b`(后代语义),`&` 原地替换(`.block { &-title }` → `.block-title`)
- `.a { @media … { … } }` → 选择器上下文保留
- `@mixin` / `@if` / `@each` / `@include` 透明;顶层函数体没有选择器,产出一条空选择器规则
- `.mixin(@x) { … }` 识别为函数体,不当选择器;`#{…}` / `@{…}` 插值整体跳过
- `//` 行注释仅对 .less/.scss 生效(.wxss 里不是注释)
- 声明与限界的扫描都跳过字符串/嵌套括号/注释;空语句 `;;` 不会卡住循环

不做变量求值 / mixin 展开 / @extend / 条件求值。代价是保守:值里含 `$` / `@` / `#{` 就不判
(normalize_value 与 is_dynamic 都加了这条);无选择器的函数体只做属性级检查,
pairwise 规则(display:flex 配 flex-direction)跳过,因为片段里两者可能分散在定义与调用处。

@import 解析改成扩展名感知:预处理器可省后缀、可指向 `_partial.scss`,按
「同名 + 同后缀 → 下划线 partial」取第一个存在的文件。

顺带修掉预处理器独有的重复:`&:hover` 在裸块和 @media 块各写一次时摊平出两条同选择器规则,
选择器类规则会重复报同一条。Report::new 排序后 dedup——全字段相同的诊断不携带信息。

真实工程:novel 0e/11w → 0e/95w,skit_1 0e/3w → 1e/101w,businessview 50e/538w → 59e/766w。
抽查确认新命中都是真的(float、mask-position、filter: drop-shadow、嵌套块 display:flex 缺方向),
选择器摊平正确(`.cnt .subscribe-list .subscribe-item:last-child`)。套件 162 passed,
新增 8 条预处理器解析测试。
拿 grass 做对照实验(businessview 82 个 scss,逐条比较「选择器 → 声明集合」)时暴露的两个 bug,
都属于「不报错但少数据」,也就是最危险的一类:

1. `url(https://…)` 里的 `//` 被当成 LESS 行注释,整行剩余内容被吃掉,导致该声明和后续
   声明一起丢失,块内花括号计数错位后连后面的规则也一起消失。修法:`url(` 整体当作不透明
   内容跳过(括号、引号、转义都在内部处理),`at_url` 前面做标识符边界判断,避免误伤
   `myurl(`。

2. `}` 前省略最后一个分号(SASS 的常见写法)时,那份待处理文本被直接丢弃。修法:遇到
   `}` / EOF 时先尝试把它当声明解析,再退出块。

两处都有回归测试。修复后 businessview 从 59e/766w 变成 59e/811w——新增的 45 条全是原先
被吞掉的声明。

对照实验结论:排除 39 个含 @import 的文件(grass 无法解析项目的别名路径,报
"Can't find stylesheet to import"),其余 39 个文件里 37 个逐条一致,剩余 2 个只差组合器
两侧的空格(grass 会规范化 `+` / `>`),语义一致。
词法/语法交给 oxc(CSS / SCSS / Sass / Less 一个 Syntax 枚举),input/wxss.rs 只保留
「把嵌套摊平成规则」这一层。对外类型与 parse 签名未变,规则层零改动。
wxss.rs 970 → 812 行(生产 720 → 470,净 -238)。

换的依据是差分实验,不是「更正统」:
- 172 个真实预处理器文件(skit_1 67 less + novel 27 less + businessview 78 scss)逐条
  比较「声明起点 + 属性名」,自写扫描器与 oxc 170 个一致,其余 2 个差异全在 @Keyframes
  内部(按设计跳过)。
- grass 编译 businessview 39 个不含 @import 的 scss 与自写摊平结果对照,37 个逐条一致,
  2 个只差组合器两侧空格。

同一批实验还抓出并修掉了自写扫描器的两个静默漏判(前一个 commit cf5ef1b)。

顺带补上一个此前就有的盲区:解析失败原本是 `let Ok(doc) = ... else { continue }` 静默跳过,
换成 oxc 后失败面变大(缺冒号的声明、`.wxss` 里的 `//` 都是硬错误),一份坏样式连同它的
@import 子图会无声消失。现在 ParseError 分 Empty(空文件/仅注释,静默跳过)与
Syntax(报 skylint/unparsed-style 告警,带原因与「这份文件的 @import 也不会被展开」提示)。

oxc 是递归下降解析器,调试构建下 64 层嵌套就能吃掉几 MB 栈,200 层直接 abort。解析前
粗扫花括号深度:>64 拒绝(Err 而不是崩溃),>16 换到 16 MiB 栈的线程里解析,普通样式表
零额外开销。新增测试锁住 64 通过 / 65 拒绝。

验证:cargo test 173 passed(含新增的告警冒泡与深度保护用例)、clippy 零 warning;
四个真实工程的诊断与改造前逐字段完全一致(novel 95 / businessview 870 / skit_1 102 /
plugin_1 7),不是只比总数。
`text-overflow-ellipsis` 与 `tag-selector-isolation` 都有:

    if !doc.path().ends_with(".wxss") { continue; }

这是拿文件后缀字符串代替「这份样式是否可分析」的语义。预处理器现在能完整摊平,守卫已经
过期——而三个真实工程的样式源几乎全是 .less / .scss,等于这两条规则在迁移目标工程上
从来不会生效。

去掉后 businessview +10 条(两个规则 2 / 8),其余三个工程不变。逐条核对为真:
- b2c-bind/pages/bind-employee.scss:10 的 `.ellipsis { text-overflow: ellipsis }` 作用在
  `<view class="nickname ellipsis">{{nickname}}</view>` 上,Skyline 下不会生效;
- phone-numbers/style/default-box-sizing.scss:2 是 `page, view, text, image, …` 的标签
  选择器全局重置,命中组件内部节点——Skyline 下标签选择器不跨组件,组件内全部丢
  box-sizing。老 WebView 工程最典型的坑。

每个规则补一条 .scss 源的回归测试(fixture 用 useCompilerPlugins:["sass"],minidev 的
Resolver 认的是 sass 而不是 scss)。
规则层不再自己解析字符串。`WxssStructure` 现在暴露解析器原语:

- `WxssDeclaration.tokens: Vec<ValueToken>`(Function / Dimension / Number / Percentage /
  Color / Url / Ident / Str / Comma / Dynamic / Other),由 oxc 的 `ComponentValue`
  递归转成 owned 值;`value_is_raw`(自定义属性、`progid:`)整值落成单个 `Other`。
- `WxssSelector.parts: Vec<SelectorPart>`(Tag / Universal / Class / Id / Other),
  来自 `ComplexSelector` / `CompoundSelector`;组合符、伪类、属性选择器、插值在构造阶段
  就落成 `Other`,`SimpleSelector::parse` 只做一次折叠——策略不变,仍是宁可漏报。

删除的手写状态机:`value_util` 的 283 行字符扫描(split_top_level / split_tokens /
top_level_calls / all_calls / 单位识别全部直接读词元)、`selector` 的 145 行选择器扫描
(ident_len 等)。6 条值规则与 2 条选择器规则只改调用点,`gradient_limit` 的方向/尺寸/
停止位置改成读 `Ident("to")` / `Dimension` / `Percentage`,消息文本用原文重建。

生产代码净增约 225 行(AST → owned 词元映射 + 词元携带成本),换来的是不再维护两套
字符串状态机;`url("a b")`、引号里的逗号、嵌套函数这些输入以前靠手工维护 quote/depth,
现在由解析器保证。

验证:诊断逐字段完全一致(novel 95 / businessview 880 / skit_1 102 / plugin_1 7,零差异),
cargo test 183 passed(新增 15 条词元与选择器部件用例,原有断言一条未改),clippy 零 warning。

两处刻意保留的旧口径:`progid:` 值按整值原文当 Other;`is_dynamic` 的 var(/env(/{{/$/#{
仍在任意位置判定、@ 仍只认值开头。唯一放宽:预处理器变量出现在函数参数里
(`translate(@w, 0)`)现在也算动态——方向是少报,不会多报。
scroll-view-margin:知识库 changelog 的 1.0.0(2023-05-11)已声明「scroll-view 组件直接
子节点支持 CSS margin」,旧项目 2024-05-31 也删掉了这条规则;现代基线上实现只会误报。

类名可达性:死样式在 WebView 里同样是死的,不是 Skyline 语义差异,越过规则边界(只报
WebView 能跑、Skyline 不能或不一样),且对 weui.wxss 这类公共样式噪声极大。
规则:
- skyline/component-attribute:组件属性取值的数据表(枚举白名单 / WebView-only
  取值 / 不支持的属性 / 数值范围),24 个「组件.属性」键,静态属性才判。
- skyline/scroll-view-family:从 5 条挂载约束扩到 17 条结构约束,新增 first-child /
  unique / before / renders-first / requires-attr / allowed-values / within 七种关系,
  全部走同一张数据表。
- skyline/text-decoration-target:text-decoration* 只对 <text>/<span>/<input> 生效,
  且不继承(computed_style.h:831 在 non_inherited_data_),容器里有 <text> 不能豁免。

修正:
- skyline/flex-direction 之前不看 rendererOptions.skyline.defaultDisplayBlock。开启该
  配置时 Skyline 的 flex-direction 默认值就是 row(对齐 WebView),前提不成立。四个
  真实工程全部开启,实测 424 条误报(novel 79 / businessview 256 / skit_1 85 /
  plugin_1 4)。Project::renders_as_block 按 app 级 + 页面级配置判定,组件按用到它的
  页面判定。
- 同选择器分散在多条规则(含 @media)里声明 flex-direction 时不再误报。

真实工程:novel 95→16、businessview 878→622、skit_1 102→18、plugin_1 7→3。
254 tests / clippy 干净。
`wx:if` / `wx:elif` / `wx:else` 的各分支在摊平后是兄弟节点,此前 `unique` /
`before` / `first-child` 会把互斥分支里的节点算成「同时存在」,误报「有两个
nested-scroll-body」「header 排在 body 之后」。

现在 `WxmlElement` 带 `branches: Vec<Branch>`(从外到内的条件分支栈,
`Branch { group, arm }`),`If` 的每个 arm 在栈顶追加一层,`Pure` / `For` 透传。
判定「互斥」= 任一层的 group 相同而 arm 不同;嵌套 if 靠栈逐层比对,不丢外层信息。

265 tests / clippy 干净;四个真实工程计数不变(novel 16 / businessview 622 /
skit_1 18 / plugin_1 3,scroll-view-family 均 0)。
minidev 只校验页面 json,app.json 与 project.config.json 完全没有 schema 校验
(实测:app.json 的 rendererOptions 写成字符串、tagNameStyleIsolation 写错、
libVersion 低于 Skyline 要求,`minidev build` 全部静默通过)。

- skyline/lib-version(error):按 rendererOptions 表计算工程实际需要的最高基础库
  版本(Skyline 渲染 2.30.4 / app 级 rendererOptions 2.31.1 / 页面级 3.1.0 /
  convertRpxToVw 3.3.0),只判具体版本号,latest/trial 等跳过。
- skyline/renderer-options(warning):类型不符、skyline 下未知键、隔离模式取值非法、
  页面级把开关写在 rendererOptions 顶层——运行时都是静默取默认(`|| false`、
  `=== 'isolated' ? ... : Legacy`),所以不是 error。

新增 input/json.rs 的 JsonPositions:只做「键 → 行列」定位,取值仍走 serde_json,
不写第二套 JSON 解析器。

289 tests / clippy 干净;四个真实工程 0 命中(libVersion 均 ≥ 要求、rendererOptions
取值合法)。
与 unsupported-value(error,「值根本无效」)分开,这条是 warning:属性受支持,
但这个值被降级 / 映射 / 无视。

数据表 value_degraded.txt 四个分区:
- [exact] animation-fill-mode none/backwards→forwards、word-break keep-all/break-word、
  background-repeat/mask-repeat space/round→repeat
- [only] will-change 仅 auto/contents
- [function] content 的 url() 被丢弃
- [env] env() 仅四个 safe-area 变量

font-weight 数值走代码判定而非数据表(数据表表达不了两段语义):引擎在
font_weight.cc 里 round 到百位,再在 computed_style.h 的 SetFontWeight(int) 里
只映射 100..900、其余回落 normal。所以 450→500、50→100、950→400。

顺带三处口径统一:
- background-repeat/mask-repeat 的 space/round 由 error 降为 warning(是降级不是
  无效),space/round 与 word-break: break-word 从 wxss_support.txt 迁出
- will-change 的 scroll-position 也挪进 [only],与自定义 ident 同一条规则同一个
  严重度,去掉跨表去重
- [env] 跳过 --* 自定义属性(只是存值,报在定义处是噪音)

304+6+2 tests / clippy 干净。真实工程:novel 16→18、businessview 622(3 条
word-break 由 error 变 warning,总数不变)、skit_1 18→19、plugin_1 3,新增逐条核对。
skyline/shorthand-partial(warning):简写本身生效,但值里属于不受支持子属性的那一段
会被静默丢弃。background 的 attachment / 盒关键字、mask 的盒关键字 / no-clip、
text-decoration 的 thickness,共 5 条数据行。与 unsupported-property 不重叠——
那条判属性名,这条判简写值里的 token。

skyline/transition-property(warning):transition / transition-property 里的属性名必须
在 animation.md 的可动画白名单里(65 个),否则那段过渡不生效。跳过厂商前缀
(-webkit-transform 等,引擎行为无证据)、CSS 全局关键字、时间函数关键字与动态值。

真实工程:novel 18→21、businessview 622→626、skit_1 19→21、plugin_1 3。
新增全部是 visibility / color 被用在 transition 上(真:Skyline 下文本与字体属性
不产生过渡)。shorthand-partial 四工程 0 命中。

313+6+2 tests / clippy 干净。
skyline/component-children(数据表驱动):
- navigator 只能放 <text> 或文本节点(error)
- 8 个手势组件必须且只能有一个直接子元素(error)
- scroll-view 的 refresher-default-style="none" 必须配 slot="refresher" 的直接子节点(warning)

计数口径是「能同时渲染的直接子元素」:同一 wx:if 的互斥分支合并成一个。
裸摊平计数会让 skit_1 的 index-block.wxml:60 报出假 error(swiper 的 wx:if /
wx:else 两个分支各包一个子节点,运行期只渲染一个),与 scroll_view_family 的
unique / before / first-child 同一口径。

skyline/share-element-key(error):缺 key 报,同文件内静态 key 重复报第二个及之后。
跨文件的页面内唯一不做——key 是运行期配对语义、组件实例数静态不可知。

真实工程:plugin_1 3→5(两处 navigator 包 view,与文档 ❌ 示例一致),其余三个工程
计数不变;skit_1 的 8 处手势组件全部单子节点。361+6+2 tests / clippy 干净。
新增 [requires] 分区表达「A 属性的取值决定 B 属性是否生效」:
- transformer-type 需要 layout-type="transformer"
- stackLeft/stackRight/tinder 布局下 indicator-type 只能是 normal
- scrollFixedCenter/swap/swapYRotation 不支持 circular

要求列支持 `attr=v` 与 `attr=!(a|b)`;缺失属性的语义按「不存在」处理——对
`!(...)` 视为满足(layout-type 缺省 normal 不在禁止集合里),对 `=v` 视为不满足。
触发列 `attr=*` 能把无值布尔属性(`<swiper circular>`)算作触发。

另补 [enum] 的 native-view(8 个手势组件共用,scroll-view|swiper)与 [unsupported] 的
input placeholder-class / safe-password-*(后者按前缀,文档没枚举具体名字)。
组件列与 [unsupported] 属性名都支持逗号/前缀多值。

真实工程:businessview 626→630,4 条 <input placeholder-class="input-placeholder">
(类在 scss 里确有定义,迁 Skyline 后占位符样式整块丢);其余三个工程不变。
361+6+2 tests / clippy 干净。
两条:
- app.json 的 window.disableScroll —— disableScroll 是页面级配置,写在 window 里
  没有任何代码读取(page-config.md:49)
- project.config.json 的 setting.skylineRenderEnable 非布尔 —— 开发者工具按真值
  判断,字符串 "false" 反而会开启

都是静默失效,所以是 warning;位置落在键名上。四工程 0 命中(四份 app.json 的
window 里都没有 disableScroll,四份 skylineRenderEnable 都是布尔)。

371+6+2 tests / clippy 干净。
Skyline 的默认值与 WebView 不同:display 默认 flex、flex-direction 默认 column、
align-items 默认 stretch、box-sizing 默认 border-box。知识库里的 @skyline 提示
(SKILL.md:163-169)说的就是这几条,但没有任何规则把它们变成可执行的检查。

只有工程里存在 skyline 页面时才判,最多两条 info:
- 未开 defaultDisplayBlock → display/flex-direction/align-items 三条漂移;position 默认
  relative、text-align 默认 start 并进同一条消息(它们没有开关能对齐)
- 未开 defaultContentBox → box-sizing 默认 border-box

配置读取复用 project.rs 的 renderer_flag:defaultDisplayBlock app 级顶层与 skyline 下都认
(运行时 index.ts:139-141 就是两个 ||),defaultContentBox 只认 skyline 下
(index.ts:145)。只判 app 级、不逐页合并 rendererOptions——逐页会变成配置合并器,
收益只有「页面级开了一半」这种情形。

四个真实工程都开了两个开关,0 命中;389+6+2 tests / clippy 干净。
选 oxc 而不是 swc:oxc_allocator 0.155 已经在依赖树里(oxc-css-parser 用的就是它),
同一版本线复用一套 allocator,与 CSS 前端同族。代价是 +24 个包(cargo tree 408→520 行)。

input/js.rs 只做两件事:解析 .js/.ts,以及按「函数体指令序言」判定 has_worklet。
指令序言用 FunctionBody.directives 判,不是文本搜索——函数体中间那句 'worklet'、
注释里的 worklet 都不会误判。

遍历用 oxc_ast_visit::Visit 而不是手写 walk(Statement/Expression 有数百个变体,
手写必然漏),只覆写 6 个钩子,名字从 variable_declarator / object_property /
method_definition / property_definition 取,pending 只在初始化值本身是函数时设置,
所以 `const f = wrap(() => {})` 里的箭头不会误叫 f。

解析失败一律整份跳过(与 wxss 不同):JS 里错位之后的节点形状不可信,在错的 AST 上
报的告警多半是误报。

389+6+2 tests / clippy 干净;四个工程诊断数不变。
四个 Fixture 共用 `{prefix}-{name}-{pid}` 作临时目录,同一进程里两个用例用同一个
name 就同目录。实测遇到过一次:text_decoration_target 与 text_overflow_ellipsis 的
dynamic_class 用例都叫 "dynamic",并行跑时一个 remove_dir_all 把另一个的工程删掉,
两条单测随机 FAILED。

改成 `{prefix}-{name}-{测试线程名}-{pid}`。Rust 的测试线程名就是完整用例路径,天然唯一。
junrangao added 29 commits October 28, 2026 20:53
补 1 的豁免只问「width 是否本来就等于内容宽」,没问「丢掉这条声明后还剩什么」。同元素还有别的 width 声明时,丢弃 fit-content 会落到前一条上,只在落到 auto 时才算无差异。新增 matched_declarations 暴露整个级联,resolved_property 改用它实现。
Ident::from 原先无条件 to_ascii_lowercase,而 has_class / has_id 按 selectors 给的 CaseSensitivity::CaseSensitive 比较,导致 .btnItem 永远命中不了 class="btnItem"——元素级取值(display / width / position)整条断链,P16 闸门还会因此误判整块样式没人用。改为保留原文,标签名 / 属性名在比较点忽略大小写,class/id/属性值敏感;顺带删掉 any_selector_matches 里『含大写就当可达』的止血分支。
旧判据是「flex-grow>0 且父是列向 flex 且父无定高」这个症状代理,漏掉了最常见的一类:scroll-view 根本没写高度(只写 max-height)。按引擎源码重写:不开 enableScrollViewAutoSize 时 scroll-view 的 yoga 节点在 fp 里是叶子(子元素各自 SetAsRoot 独立布局,ScrollView::AppendChild 不碰 fp),也没有任何 measure 兜底(SetLayoutNode 只做失效通知),Dart 侧拿的是 loose 约束——所以高度只能来自 CSS。判据覆盖自身 height、百分比沿父链、绝对定位上下拉伸、flex 主轴 grow / 交叉轴 stretch,全部不成立即报。swiper-item / nested-scroll-* 属框架定尺寸,按确定处理。另补 display: contents 到不支持值表(fp 的 Display 枚举无此值,整条声明被丢弃)。六工程 +20 条:19 条 scroll-view-height(含短剧插件那个半屏空白 bug 的两处、businessview 5 处只写 max-height 的)、1 条 display: contents。
1) scroll-view-height: 只有 max-height 的仍报(上限不是尺寸),配 height 才放行——这批是六工程 5 条真实命中的共同形态,之前只被隐式覆盖;2) unsupported-value: fit-content 豁免要读父级的 display,父类名大写开头时旧实现(标识符统一小写)取不到 → 误报,补一条父级 camelCase 的用例。
- 新增 wxml_graph:跨 <slot> 与组件边界求逻辑祖先(DAG,含无解边界标记)
- Project 增加 tag_targets(单元 WXML → 标签 → 组件路径)
- WxmlElement 记录 slot_name(glass-easel 把 name 从普通属性摘出去了)
- structure::check_element 的 Direct/Ancestor/Within 改判渲染树祖先
- 新规则 skyline/scroll-gesture-conflict:滚动容器与祖先 touchmove 冲突
- 新增 skyline/native-layout-ignored:没有 CSS 盒的内置组件(list-view / grid-view /
  list-builder / grid-builder / sticky-section / sticky-header / nested-scroll-*)上
  布局类声明(width/height/margin/display/flex/position/gap/overflow)不生效;padding
  仍由 native-padding 负责,两处共用新的 natives 表。
- fixed-z-index-inversion 按「遮挡方」聚合:同一层压住多个上层只报一条,诊断落到遮挡方,
  消息里列出被压住的元素(path:line 与自身键,最多 4 个)。
- scroll-gesture-conflict 的门控豁免不再认死 dragHide:名字带 drag 且是动态值即视为
  已门控;显式排除 bind/catch/capture 事件绑定(否则 bind:dragmove 会被误当成开关)。
- wxs-sync-query:getComputedStyle 的第二个实参是 undefined/null/false/0/'' 时同样算
  「没写回调」(原来只看参数个数)。
- native-padding 的 help 补一句:组件 padding 是 sliver padding,滚动方向上也占位。

六工程逐字段 diff:businessview / miniprogram-skit_1 全等;miniprogram-novel 只有 help
文案变化;weapp-novel-plugin_1 118→119、_2 118→122、短剧插件 15→12,差异全部来自
z-index 聚合(逐条核对过)。
祖先链原先按「子节点 slot 属性 = <slot name>」无条件匹配,只对 `multipleSlots: true`
的组件成立。对照 glass-easel 源码补齐四件事:

- slot 模式:`shadow_root.ts` 的 `getContainingSlot()` 在 `SlotMode.Single`(默认)下
  所有子节点都进模板里第一个 `<slot>`、`slot` 属性被忽略;`Multiple` 才按名字匹配;
  `Dynamic` 静态不可知。模式从组件 JS 的 `options` 里读,缺省 Single。
- 没有匹配槽位的子节点不参与合成(不渲染),按「不可知」处理而不是挂在宿主上。
- `virtualHost`:`component.ts` 不创建宿主元素,模板顶层渲染在宿主所在位置;但父级的
  `<slot>` 投影仍要接上(`weapp-novel-plugin` 的 `write-comment` 就是 virtualHost)。
- `<template name>` 定义体不再被当成组件模板的顶层:它渲染在每个 `<template is>`
  调用处(调用点不产生元素节点,用「所在元素」表示)。

`ancestors` 的首个父边是 Unknown 时现在正确置 `open`(原来返回默认值丢掉了它);
顶层「不认识也不是内置」的标签沿用旧口径置 open。

六工程逐字段 diff 与改动前完全一致;单测 821 通过,新增 4 条(单槽忽略名字、缺槽未知、
动态槽名未知、virtualHost 透传 slot)。
片段文件原先不进图,它们的元素祖先一律按「不可知」处理,`<import>` 进来的模板定义
也无法从引入方解析。按编译器语义补上:

- `<include src>` 是整段内联(`proc_gen/tag.rs:1293-1316`:在引入位置生成
  `G[path]('')(...)` 并把结果挂到当前位置),所以片段顶层元素渲染在 `<include>`
  所在元素内部;`<include>` 写在文档顶层时接该文档的顶层。
- `<import src>` 只把目标文件的 `<template name>` 定义带进作用域(`imports` 不传递、
  `<include>` 也不带定义),因此定义体的可见范围 = 本文件 + 直接 `<import>` 它的文件。
- 互相 `<include>` 的片段按深度上限截断(防环),超出即不可知。

`render_docs` 也沿 `<include>` 展开(它渲染在页面里),`<import>` 不算。

六工程逐字段 diff 与改动前完全一致;新增 2 条单测(片段顶层元素挂在 include 所在
元素内、import 后模板定义在引入方的 `<template is>` 处渲染)。
`element_docs` 之前按页面顺序去重、先到的页面决定 `Reach`:同一份组件被 WebView 页面
和 Skyline 页面同时引用时,只要 WebView 页面排在前面,它就被标成「只在 WebView 下
渲染」,`help` 里多出一段错误的说明。另外它按 `usingComponents` 声明判断可达
(`page_components` 把 app.json 的全局声明算进每个页面),声明了却没人用的组件也被
当成 Skyline 可达。

现在改为:

- 可达性从渲染树算(`WxmlGraph::render_docs`:沿真实标签用法 + `<include>` 展开),
  一个文档只要被任一非 opt-out 页面渲染到就是 `Reach::Skyline`,否则 WebviewOnly;
- 顺序仍是页面 WXML 在前(页面顺序),其余文档按路径排序,输出稳定;
- 被 `<include>` 引入的片段进入元素级检查(原来只有页面与组件)。

六工程逐字段 diff 无变化;新增单测覆盖「只被 Skyline 页面用 / 只被 WebView 页面用 /
两边都用 / 页面→组件→组件传递」,并验证过旧实现下「两边都用」会失败。
`type` 缺失 / 空值映射成 `kNone`、`default` 映射成 `kFlex`,而
`package/skyline/lib/widgets/skyline_scroll_view.dart:1648-1675` 把这个两类走同一条
分支:`children: [firstChild]` —— 只渲染第一个子元素,其余直接丢弃。原来一律按
Info「性能退化」报,低估了后果。

现在按 `max_rendered_children`(已处理 wx:if 分支互斥)区分:≥2 个可渲染子元素报
Error 并说明「其余子元素不会显示」,只有一个才保留 Info。
- matcher::subject 只统计最右侧 compound:`.a view` 的主体是 `view`,
  祖先的 class 不该让 dead-selector 以为「主体带 class」。
  businessview 的 `.weui-agree navigator`×2 不再被误判死。
- no-inline-text:文本被投影进组件的 `<text>`、或组件没有匹配的槽位(不渲染)时跳过;
  脱离文档流的兄弟不计入混排。businessview −3(2 slot + 1 absolute)。
- transition-property:fp 的属性名闭集里没有任何 `-webkit-*`,
  含它们的 transition 整条解析失败。删掉原来的「厂商前缀不报」,
  并把 text-shadow/font-* 等归入同一条回滚路径;`line-height` 单独按「只丢这一项」。
  businessview +12(webview 有过渡、skyline 完全没有)。
- scroll-view-family:sticky-section / nested-scroll-header / nested-scroll-body
  改 Direct(引擎按直接子节点取),sticky-header 补 RendersFirst。
- component-children:`<slot>` 不参与 ChildrenOnly;Exactly / RequiresChildTag
  遇 slot 按不可知放过。
- worklet-directive:`method(fn)` / `listener(fn)` 是身份包装,能拿到处理器名
  (此前 9 处真实绑定全部失检)。
- 删除已无使用者的 Relation::Ancestor 及其实现。
- gradient-unsupported:`-webkit-*gradient()` 让 fp 整条声明回滚,现在会报;
  同一声明组里已存在标准渐变兜底(autoprefixer 产物)时不报。
- multi-value-limit:`background` / `mask` 简写也按层数判上限,文案点明
  「超出后整条背景/遮罩图失效」。短剧插件 +1(store-countdown 三层背景)。
- shorthand-partial:`background: … text`(渐变文字)同样会被丢弃。
- transition-animation-conflict:`@-webkit-keyframes` / `-moz-` / `-o-` 的名字能取到了;
  `transition: .3s` 按 fp 语义等价 `all .3s`(排除 `transition: none`)。
- wxs-sync-query:`getBoundingClientRect(undefined/null/void 0)` 与不传等价。
- scope_script:排除同 scope 的 `.wxs`,避免 worklet 规则整单元跳过。
- text-overflow-ellipsis:宿主自己没有行内内容时不构成差异;`white-space` 按继承查祖先。
- text-hard-break-ellipsis:只递归文本节点容器,块级子元素的换行不影响外层末行。
- text-decoration-target:只认会画线的声明(color/style 单独出现不报);子树无文本不报。
- implicit-color:`color: inherit/currentColor/unset` 沿祖先链继续解析。
- horizontal-scroll-enable-flex:改用级联胜出的 `display`(`style="display:block"`
  覆盖时不再要求 enable-flex)。
- fixed-layer:`contain` 只认 layout / paint / strict / content(size / style 不建立包含块)。
- draggable-sheet-scroll-to:`size: undefined` 不算同时传入(引擎按 `JS_IS_NUMBER` 判)。
- flex-direction:并列选择器逐项豁免,`.b, .c` 里只有 `.c` 有方向时 `.b` 仍然要报。
- legacy-flexbox:同一选择器的多块合并后取最终 `display`;`-webkit-box` 与 `flex`
  语义不同,仍按差异报。
- scroll-view-height:`aspect-ratio` + 宽度确定算作定高;顶层 `flex: 1` 在未开
  `defaultDisplayBlock` 时由页面根 flex column 分到整屏。
- calc-min-max:按 fp 的折叠规则逐参数判(同单位 `+`/`-` 可折叠;裸数字参数折不了,
  引擎按 0 求值)。
- matcher:`:where()` 与 `:is()` 同样递归;Skyline 方言的伪类表去掉 `:is`/`:where`
  (fp 解析器直接拒绝,整条规则被丢弃)。
- root-scope-class:页面节点上的 `~x`/`^x` 与普通类名等价(不再报);样式表同时被
  页面作用域引入时也不报。
- `skyline/calc-angle` 删除:能触发的写法都是 CSS 类型错误(角度混长度),
  WebView 同样整条拒绝,不构成渲染器差异;顺带清掉为此保留的 angle 工具函数。
- lib-version:全 WebView 工程里的 app 级 `rendererOptions` 不抬 2.31.1
  (Skyline 运行时不实例化,配置无人读)。
- docs/COVERAGE.md 追加「已知边界」:genericsImplementation 注入的组件、
  `-webkit-*` 属性整体入表、自定义组件宿主尺寸、`share-element` 的 wx:for、
  conic 停止位、内联 template 的组合选择器。
devtools 的 postcss / minifyWXSS 任一开启时,prod 构建会跑 autoprefixer:
往 transition 值里塞 `-webkit-transform`、额外生成 `@-webkit-keyframes`,而 dev 构建
整段跳过。Skyline 不认这些,WebView 认——但 autoprefixer 正常会成对输出,成对时无害。

新规则只报没有兜底的形态:
- `@-webkit-keyframes X` 且没有无前缀的 `@keyframes X`;
- `-webkit-animation*` / `-webkit-transition*` 属性名,且同一规则里没有标准属性。

顺带把 `@keyframes` 的源码扫描抽到 `wxss/keyframes.rs`,`transition-animation-conflict`
改用同一份实现(原来各扫一遍)。`-moz-` / `-o-` / `-ms-` 前缀两端都不认,不算迁移差异。

docs/COVERAGE.md 补「本轮修复(四批)」与 autoprefix 的背景说明。
源码里没有注入痕迹(注入只发生在 prod 产物的 wxss 里),能静态看见的只有
project.config.json 的 `setting.postcss` / `setting.minifyWXSS`:任一为真就会把
wxss 插件拉起来,而插件里 `autoPrefix` 默认 true,按 `iOS >= 8, Chrome >= 37`
补前缀且 `remove: false`。

规则给一条工程级 Info(只在有 Skyline 页的工程上报),说明 prod 产物会带上
`transition` 值内前缀与 `@-webkit-keyframes`,dev 构建跳过、本地看不出来,
并指向 `skyline/vendor-prefix` 复查产物。纯 WebView 工程不报。
@gaojunran
gaojunran merged commit 2d22f31 into main Nov 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant