fix(gate): M299 r1 —— tabs 判据改测闭包本身 + 反例判据抗重排
P2-1:tabs 的样式闭包抽成 tab_style(..)(返回闭包本身,照 chip_style / choice_style 的写法),单测喂的就是交给 button 的那个闭包。上一版只测闭包里的助手 tab_fill(..), 复审把 fill 挪到闭包外、闭包写回 |_theme, _status| 时用例仍全绿——本 mission 在 tabs 上 唯一的代码修复当时没有判据。现在同一变异红(hover 必须叠状态层)。 P2-2:反例判据改成「去空白 + 去花括号」后比对 .or_else(||Some(ui.color( 与 .unwrap_or_else(||ui.color(,换行 / 缩进 / 闭包加括号都躲不过;扫描跳过 .../tests/ 与 tests.rs;新增 KNOWN_COPIES 豁免表(slider handle,scope 外,已报 finding)与 every_known_copy_is_still_there(豁免不许挂空条目)。自证:名单外第 12 个站点用 换行+花括号写法 ⇒ 红(两种形态各一次),删掉后三条全绿。 行数:tab_style 拆到 components/navigation/tabs/style.rs(tabs.rs 398 → 370)。 像素:new 与 new2 两套 24 张逐张 md5 相同 ⇒ 行为中性。
This commit is contained in:
1 parent
8144714f30
commit
fa36ccbb66
4 files changed
+214
-56
No files matched your search
@@ -40,11 +40,10 @@
|
|||||||
|
|
||||||
use iced::widget::text::{LineHeight, Wrapping};
|
use iced::widget::text::{LineHeight, Wrapping};
|
||||||
use iced::widget::{button, container, scrollable, space, text, Row, Stack};
|
use iced::widget::{button, container, scrollable, space, text, Row, Stack};
|
||||||
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow};
|
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels};
|
||||||
|
|
||||||
use unlucky_design::color::Role;
|
use unlucky_design::color::Role;
|
||||||
use unlucky_design::icons::IconKind;
|
use unlucky_design::icons::IconKind;
|
||||||
use unlucky_design::shape::ShapeScale;
|
|
||||||
use unlucky_design::typography::TypeRole;
|
use unlucky_design::typography::TypeRole;
|
||||||
|
|
||||||
use crate::icons;
|
use crate::icons;
|
||||||
@@ -54,6 +53,9 @@ use super::super::interaction::{self, Base, Interaction};
|
|||||||
use super::super::layout;
|
use super::super::layout;
|
||||||
use super::geometry::{TabsGeometry, TABS_ICON_HEIGHT};
|
use super::geometry::{TabsGeometry, TABS_ICON_HEIGHT};
|
||||||
|
|
||||||
|
mod style;
|
||||||
|
use style::tab_style;
|
||||||
|
|
||||||
/// 两种 Tabs(`research/12b` §6;M3 的 `PrimaryTabRow` / `SecondaryTabRow`)。
|
/// 两种 Tabs(`research/12b` §6;M3 的 `PrimaryTabRow` / `SecondaryTabRow`)。
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum TabsVariant {
|
pub enum TabsVariant {
|
||||||
@@ -215,11 +217,12 @@ fn secondary_line_color(ui: &Ui) -> Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 一行标签页(自带横向滚动)。
|
/// 一行标签页(自带横向滚动)。
|
||||||
|
///
|
||||||
/// ## 状态层画在哪儿(M299)
|
/// ## 状态层画在哪儿(M299)
|
||||||
///
|
///
|
||||||
/// tab 容器**没有**选中实底(选中的标记是下方那条指示条),所以选中与未选中都一样:状态层叠在
|
/// tab 容器**没有**选中实底(选中的标记是下方那条指示条),所以选中与未选中都一样:状态层叠在
|
||||||
/// `base` 上(`Tabs::base(..)`,默认画布)。M299 之前这一层是在样式闭包**外**算的,真实
|
/// `base` 上(`Tabs::base(..)`,默认画布)。M299 之前这一层是在样式闭包**外**算的,真实
|
||||||
/// hover / pressed 一个像素都不变;现在由 [`tab_fill`] 在闭包里跟 `button::Status` 求值。
|
/// hover / pressed 一个像素都不变;现在由 [`style::tab_style`] 在闭包里跟 `button::Status` 求值。
|
||||||
pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M> {
|
pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M> {
|
||||||
let geometry = TabsGeometry::of(ui);
|
let geometry = TabsGeometry::of(ui);
|
||||||
let primary = tabs.variant.is_primary();
|
let primary = tabs.variant.is_primary();
|
||||||
@@ -270,25 +273,6 @@ pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M>
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// tab 容器的状态层颜色(**M299**:`status` 必须在样式闭包**里面**用掉)。
|
|
||||||
///
|
|
||||||
/// 修的是:此前 `tab_cell` 在闭包**外**把 `state` 算成 `forced.unwrap_or(Enabled)`、闭包又写成
|
|
||||||
/// `|_theme, _status|` ⇒ 真实指针的 hover / pressed **一个像素都不变**(只有 gallery 的
|
|
||||||
/// `Tab::state(..)` 强制态画得出来),全仓的 tab 一起中招。
|
|
||||||
///
|
|
||||||
/// 解析顺序:`forced`(取证 / 矩阵的 `.state(..)`)优先,否则 [`Interaction::from_status`]——
|
|
||||||
/// 与 `components/selection/chip.rs` 同一条。颜色经 [`interaction::layer_color`](唯一入口,A8 起
|
|
||||||
/// 带过渡时间轴):没有取样控件时**逐位**等于 `overlay_color`,取证图因此一字不变。
|
|
||||||
fn tab_fill(
|
|
||||||
ui: &Ui,
|
|
||||||
base: Base,
|
|
||||||
forced: Option<Interaction>,
|
|
||||||
status: button::Status,
|
|
||||||
) -> Option<Color> {
|
|
||||||
let state = forced.unwrap_or_else(|| Interaction::from_status(status));
|
|
||||||
interaction::layer_color(ui, state, Role::OnSurface, base.role())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 一个 tab 单元(内容 + 底部条)。
|
/// 一个 tab 单元(内容 + 底部条)。
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn tab_cell<'a, M: Clone + 'a>(
|
fn tab_cell<'a, M: Clone + 'a>(
|
||||||
@@ -374,19 +358,7 @@ fn tab_cell<'a, M: Clone + 'a>(
|
|||||||
let button = button(stack)
|
let button = button(stack)
|
||||||
.height(Length::Fixed(height))
|
.height(Length::Fixed(height))
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.style(move |_theme, status| button::Style {
|
.style(tab_style(ui, base, forced, color));
|
||||||
// M299:状态层在**闭包里**跟 `status` 求值(此前 `state` 在闭包外算、`status` 被丢掉,
|
|
||||||
// 真实 hover / pressed 因此一个像素都不变)。
|
|
||||||
background: tab_fill(ui, base, forced, status).map(Background::Color),
|
|
||||||
text_color: color,
|
|
||||||
// ContainerShape = CornerNone(tab 本身是直角;圆角只在指示条上)
|
|
||||||
border: Border {
|
|
||||||
radius: ui.radius_dp(ShapeScale::None),
|
|
||||||
..Border::default()
|
|
||||||
},
|
|
||||||
shadow: Shadow::default(),
|
|
||||||
snap: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
match tab.on_press {
|
match tab.on_press {
|
||||||
Some(message) => button.on_press(message).into(),
|
Some(message) => button.on_press(message).into(),
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//! tab 按钮的样式闭包(M299 r1:从 `tabs.rs` 拆出来)。
|
||||||
|
//!
|
||||||
|
//! ## 为什么拆文件
|
||||||
|
//!
|
||||||
|
//! 只有行数:`tabs.rs` 加了这个函数之后到 413 行(AGENTS.md §3-1 的单文件 400 行硬线)。拆法是
|
||||||
|
//! 「整段搬走 + 一条 `pub(super) use`」,行为零变化。
|
||||||
|
//!
|
||||||
|
//! ## 为什么返回**闭包**而不是普通函数
|
||||||
|
//!
|
||||||
|
//! 单测要拿到的必须是「交给 `button` 的那个闭包」,不是它里面的助手:M299 r1 复审把一个
|
||||||
|
//! `tab_fill(..)` 形状的助手留在闭包外先算、闭包写回 `|_theme, _status|`,那条只测助手的用例
|
||||||
|
//! **仍然全绿**——本 mission 在 tabs 上唯一的代码修复当时等于没有判据。照
|
||||||
|
//! `pages/settings/general/theme.rs` 的 `chip_style` / `appearance/section` 的 `choice_style`
|
||||||
|
//! 同一条写法,这里把闭包整个交出去,测试直接喂 `button::Status`。
|
||||||
|
//!
|
||||||
|
//! ## 修的是什么
|
||||||
|
//!
|
||||||
|
//! 此前 `tab_cell` 在闭包**外**把 `state` 算成 `forced.unwrap_or(Enabled)`、闭包又写成
|
||||||
|
//! `|_theme, _status|` ⇒ 真实指针的 hover / pressed **一个像素都不变**(只有 gallery 的
|
||||||
|
//! `Tab::state(..)` 强制态画得出来),全仓的 tab 一起中招。
|
||||||
|
//!
|
||||||
|
//! ## 解析顺序
|
||||||
|
//!
|
||||||
|
//! `forced`(取证 / 矩阵的 `.state(..)`)优先,否则 [`Interaction::from_status`]——与
|
||||||
|
//! `components/selection/chip.rs` 同一条。颜色经 [`interaction::layer_color`](唯一入口,A8 起
|
||||||
|
//! 带过渡时间轴):没有取样控件时**逐位**等于 `overlay_color`,取证图因此一字不变。
|
||||||
|
|
||||||
|
use iced::widget::button;
|
||||||
|
use iced::{Background, Border, Color, Shadow, Theme};
|
||||||
|
|
||||||
|
use unlucky_design::color::Role;
|
||||||
|
use unlucky_design::shape::ShapeScale;
|
||||||
|
|
||||||
|
use crate::app::components::interaction::{self, Base, Interaction};
|
||||||
|
use crate::theme::Ui;
|
||||||
|
|
||||||
|
/// tab 按钮的样式闭包(**M299**:状态层在闭包里跟 `status` 求值)。
|
||||||
|
pub(super) fn tab_style<'a>(
|
||||||
|
ui: &'a Ui,
|
||||||
|
base: Base,
|
||||||
|
forced: Option<Interaction>,
|
||||||
|
color: Color,
|
||||||
|
) -> impl Fn(&Theme, button::Status) -> button::Style + 'a {
|
||||||
|
move |_theme, status| {
|
||||||
|
let state = forced.unwrap_or_else(|| Interaction::from_status(status));
|
||||||
|
button::Style {
|
||||||
|
background: interaction::layer_color(ui, state, Role::OnSurface, base.role())
|
||||||
|
.map(Background::Color),
|
||||||
|
text_color: color,
|
||||||
|
// ContainerShape = CornerNone(tab 本身是直角;圆角只在指示条上)
|
||||||
|
border: Border {
|
||||||
|
radius: ui.radius_dp(ShapeScale::None),
|
||||||
|
..Border::default()
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,18 +134,33 @@ fn icon_tabs_use_the_taller_container() {
|
|||||||
assert!(TABS_ICON_HEIGHT > TabsGeometry::of(&ui).height);
|
assert!(TABS_ICON_HEIGHT > TabsGeometry::of(&ui).height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **样式闭包必须吃 `status`**(M299):真实 hover / pressed 之前一个像素都不变。
|
/// **交给 `button` 的那个样式闭包必须吃 `status`**(M299)。
|
||||||
///
|
///
|
||||||
/// 抓得住:①回到「`state` 在闭包外算 + `|_theme, _status|`」⇒ hover / pressed 都等于静息(前三条
|
/// 这条用例调的是 [`tab_style`] 返回的**闭包本身**,不是它里面的助手——M299 r1 复审实测过:
|
||||||
/// 红);②hover 与 pressed 用同一个 α ⇒ 第四条红;③`forced` 不再优先(取证图会跟着指针漂)⇒
|
/// 只测助手的那一版,把「闭包外先算 fill、闭包写回 `|_theme, _status|`」放回来仍然全绿,
|
||||||
/// 后两条红。
|
/// 于是本次在 tabs 上唯一的代码修复等于没有判据(这就是 P2-1)。
|
||||||
|
///
|
||||||
|
/// 抓得住:①闭包里把 status 丢掉(`forced.unwrap_or(Enabled)` / 用 `Status::Active` 先算好)⇒
|
||||||
|
/// hover / pressed 都等于静息,前三条红;②hover 与 pressed 用同一个 α ⇒ 第四条红;③`forced`
|
||||||
|
/// 不再优先(取证图会跟着指针漂)⇒ 后两条红。
|
||||||
/// 抓不住:像素。gallery 的 tabs 五态行拍的是 **`forced` 支**(矩阵每格 `.state(..)`),
|
/// 抓不住:像素。gallery 的 tabs 五态行拍的是 **`forced` 支**(矩阵每格 `.state(..)`),
|
||||||
/// 它与本函数同源但不是 `status` 支;`status` 支的真像素要真实指针(取证通道开了指针穿透,
|
/// 它与本函数同源但不是 `status` 支;`status` 支的真像素要真实指针(取证通道开了指针穿透,
|
||||||
/// 本机无桌面会话 ⇒ 拍不到)。报告里把这条写成「图证明的是哪一层」。
|
/// 本机无桌面会话 ⇒ 拍不到)。报告里把这条写成「图证明的是哪一层」。
|
||||||
#[test]
|
#[test]
|
||||||
fn the_style_closure_consumes_the_live_status() {
|
fn the_style_closure_consumes_the_live_status() {
|
||||||
let ui = ui();
|
let ui = ui();
|
||||||
let fill = |forced, status| tab_fill(&ui, Base::CANVAS, forced, status);
|
let theme = ui.theme();
|
||||||
|
// 这里拿到的是**闭包**(不是颜色):`tab_cell` 交给 `button` 的就是它。
|
||||||
|
let fill =
|
||||||
|
|forced, status| match tab_style(&ui, Base::CANVAS, forced, ui.color(Role::OnSurface))(
|
||||||
|
&theme, status,
|
||||||
|
)
|
||||||
|
.background
|
||||||
|
{
|
||||||
|
Some(Background::Color(color)) => Some(color),
|
||||||
|
Some(other) => panic!("背景必须是实色,得到 {other:?}"),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let rest = fill(None, button::Status::Active);
|
let rest = fill(None, button::Status::Active);
|
||||||
let hover = fill(None, button::Status::Hovered);
|
let hover = fill(None, button::Status::Hovered);
|
||||||
@@ -180,6 +195,20 @@ fn the_style_closure_consumes_the_live_status() {
|
|||||||
pressed,
|
pressed,
|
||||||
"forced=pressed 时必须画出 pressed"
|
"forced=pressed 时必须画出 pressed"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 文字色是**静息内容色**(A1 的内容提档没做,见 findings),与 status 无关
|
||||||
|
let ink = ui.color(Role::OnSurface);
|
||||||
|
for status in [
|
||||||
|
button::Status::Active,
|
||||||
|
button::Status::Hovered,
|
||||||
|
button::Status::Pressed,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
tab_style(&ui, Base::CANVAS, None, ink)(&theme, status).text_color,
|
||||||
|
ink,
|
||||||
|
"text_color 必须原样透传({status:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 3 个 tab 的树能建出来(含选中 / 禁用 / 带图标三条路径)。
|
/// 3 个 tab 的树能建出来(含选中 / 禁用 / 带图标三条路径)。
|
||||||
|
|||||||
@@ -13,23 +13,28 @@
|
|||||||
//! M296 的变异实验就是这么发现外壳那两处的(改助手 ⇒ 外壳药丸不动)。所以这里量的是**接线**,
|
//! M296 的变异实验就是这么发现外壳那两处的(改助手 ⇒ 外壳药丸不动)。所以这里量的是**接线**,
|
||||||
//! 不是值。
|
//! 不是值。
|
||||||
//!
|
//!
|
||||||
//! ## 两条判据
|
//! ## 三条判据
|
||||||
//!
|
//!
|
||||||
//! 1. **每个站点都必须出现 `selected_fill(`**(正例):删掉调用 = 退回私有拷贝,红;
|
//! 1. **每个站点都必须出现 `selected_fill(`**(正例):删掉调用 = 退回私有拷贝,红;
|
||||||
//! 2. **除 `interaction/selected.rs` 之外,产品代码里不许出现 `or_else(|| Some(ui.color(`**
|
//! 2. **产品代码里不许出现旧私有拷贝的两种形态**(反例):`.or_else(|| Some(ui.color(..))` 与
|
||||||
//! (反例):那正是旧私有拷贝的形态。整份源码里现在一处都没有,加回来就红。
|
//! `.unwrap_or_else(|| ui.color(..))`。比对前把空白全部删掉,所以换行 / 缩进 / 加括号都躲不过
|
||||||
|
//! (M299 r1 P2-2:上一版是整串字面量,只换排版就能绕过);
|
||||||
|
//! 3. **[`KNOWN_COPIES`] 里的豁免必须还活着**(豁免不许挂空条目)。
|
||||||
//!
|
//!
|
||||||
//! ## 抓得住 / 抓不住(据实写)
|
//! ## 抓得住 / 抓不住(据实写)
|
||||||
//!
|
//!
|
||||||
//! 抓得住:①某个站点被改成 `Some(ui.color(Role::SecondaryContainer))`(用户点名的那条缺陷)——
|
//! 抓得住:①某个站点被改成 `Some(ui.color(Role::SecondaryContainer))`(用户点名的那条缺陷)——
|
||||||
//! 反例判据与正例判据至少一条红;②新的第十一处网点直接抄私有拷贝——反例判据红;
|
//! 反例判据与正例判据至少一条红;②名单外的第十二个站点直接抄私有拷贝(含换行 / 加括号的写法)
|
||||||
//! ③`selected_fill` 被改名 / 移走——全部正例红。
|
//! ——反例判据红(M299 r1 实测过);③`selected_fill` 被改名 / 移走——全部正例红;
|
||||||
//! 抓不住:①**语义上正确但写法不同**的私有拷贝(比如 `match layer_color(..) { Some(c) => c,
|
//! ④有人把某处收口了却忘了删豁免——判据 3 红。
|
||||||
//! None => ui.color(..) }`)——它不命中 `or_else` 形态,只能靠人读 diff;
|
//! 抓不住:①**语义上正确但形态不同**的私有拷贝(比如 `match layer_color(..) { Some(c) => c,
|
||||||
|
//! None => ui.color(..) }`,或先 `let Some(c) = .. else { return ui.color(..) }`)——它不是
|
||||||
|
//! `or_else` / `unwrap_or_else` 形态,只能靠人读 diff;
|
||||||
//! ②像素(那是 `selection/probe.rs` 与 `DevDocs/artifacts/m299/` 的图);
|
//! ②像素(那是 `selection/probe.rs` 与 `DevDocs/artifacts/m299/` 的图);
|
||||||
//! ③站点内部把 `selected_fill` 的返回值丢了不用(正例只证明「调了」)。
|
//! ③站点内部把 `selected_fill` 的返回值丢了不用(正例只证明「调了」);
|
||||||
|
//! ④字符串字面量里恰好含 `//` 的行会被截断(判定只会更松,不会误报)。
|
||||||
//!
|
//!
|
||||||
//! 这三条盲区写在这里,是因为「门禁绿」最容易被读成「这事有人管」。
|
//! 这几条盲区写在这里,是因为「门禁绿」最容易被读成「这事有人管」。
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -63,10 +68,10 @@ fn product_part(text: &str) -> String {
|
|||||||
let mut lines = text.lines();
|
let mut lines = text.lines();
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
if !line.trim_start().starts_with("#[cfg(test)]") {
|
if !line.trim_start().starts_with("#[cfg(test)]") {
|
||||||
// 注释行不算代码:文档里解释「旧写法长什么样」不该被判成旧写法。
|
// 注释不算代码:文档里解释「旧写法长什么样」不该被判成旧写法。整行注释与行尾注释
|
||||||
if !line.trim_start().starts_with("//") {
|
// 都剥掉(`//` 之后的半行同样不算;字符串里恰好有 `//` 只会让判定更松,不会误报)。
|
||||||
out.push_str(line);
|
let code = line.split("//").next().unwrap_or_default();
|
||||||
}
|
out.push_str(code);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -108,6 +113,53 @@ fn product_source(relative: &str) -> String {
|
|||||||
product_part(&text)
|
product_part(&text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 旧私有拷贝的**两种**形态(去掉空白后比对:换行 / 缩进 / 括号排版都躲不过)。
|
||||||
|
///
|
||||||
|
/// 第一种是「状态层有就给、没有就兜底实色」,第二种是 `selected_fill` 自己的函数体写法
|
||||||
|
/// (`unwrap_or_else(|| ui.color(container))`)——两者都是「选中项的底」那份拷贝。
|
||||||
|
const PRIVATE_COPY_NEEDLES: [&str; 2] =
|
||||||
|
[".or_else(||Some(ui.color(", ".unwrap_or_else(||ui.color("];
|
||||||
|
|
||||||
|
/// 把源码归一成「只留标识符与标点」的形态:删空白,**再删闭包的花括号**。
|
||||||
|
///
|
||||||
|
/// 于是下面三种写法归一成同一个字面量:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// .or_else(|| Some(ui.color(..)))
|
||||||
|
/// .or_else(|| Some(ui.color(..))
|
||||||
|
/// .or_else(|| { Some(ui.color(..)) })
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// 删花括号是必要的:M299 r1 P2-2 的绕过写法正是 `or_else(|| { Some(..) })`——只删空白时那个
|
||||||
|
/// `{` 会把针挡掉。针本身以 `||` 打头并紧贴 `Some(`/`ui.color(`,所以「中间隔着别的语句」不会误命中
|
||||||
|
/// (`;let y=` 之类的标记仍在)。
|
||||||
|
fn squeeze(text: &str) -> String {
|
||||||
|
text.split_whitespace()
|
||||||
|
.collect::<String>()
|
||||||
|
.replace(['{', '}'], "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 命中哪一个针(没有就 `None`)。
|
||||||
|
fn private_copy(text: &str) -> Option<&'static str> {
|
||||||
|
let squeezed = squeeze(text);
|
||||||
|
PRIVATE_COPY_NEEDLES
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|needle| squeezed.contains(needle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 这个文件是不是**只**在测试里编译。
|
||||||
|
///
|
||||||
|
/// 两种形态:`.../tests/xxx.rs`(父模块 `#[cfg(test)] mod tests;` 声明的目录)与 `xxx/tests.rs`
|
||||||
|
/// (同名子模块文件)。它们整体是测试代码——扫它们只会把测试助手(比如 `java/marker.rs` 里
|
||||||
|
/// 「容器为空时按 surface 算对比度」的兜底)当成私有拷贝。
|
||||||
|
fn is_test_only(path: &Path) -> bool {
|
||||||
|
path.file_name().is_some_and(|name| name == "tests.rs")
|
||||||
|
|| path
|
||||||
|
.components()
|
||||||
|
.any(|component| component.as_os_str() == "tests")
|
||||||
|
}
|
||||||
|
|
||||||
/// 十个「当前项」站点,全部必须经 [`selected_fill`] 拿底。
|
/// 十个「当前项」站点,全部必须经 [`selected_fill`] 拿底。
|
||||||
///
|
///
|
||||||
/// 前四行是 M296 收口的,后七行是 M299 收口的(`shell/nav.rs` 与 `shell/nav/account.rs` 各一处)。
|
/// 前四行是 M296 收口的,后七行是 M299 收口的(`shell/nav.rs` 与 `shell/nav/account.rs` 各一处)。
|
||||||
@@ -144,6 +196,13 @@ fn every_selected_item_goes_through_the_single_entry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 判据 2(反例):旧私有拷贝的形态在产品代码里一处都不许留。
|
/// 判据 2(反例):旧私有拷贝的形态在产品代码里一处都不许留。
|
||||||
|
///
|
||||||
|
/// ## 为什么比对「去掉空白之后」的形态(M299 r1 P2-2)
|
||||||
|
///
|
||||||
|
/// 上一版拿整串 `"or_else(|| Some(ui.color("` 直接 `contains`:复审把同一形态写成
|
||||||
|
/// `or_else(|| { Some(ui.color(..)) })`(**只换排版**)就绕过去了——语义完全一样,字面量却不同。
|
||||||
|
/// 现在比对前把源文件里的空白全部删掉(`split_whitespace().collect()`),针也缩成无空白的形态;
|
||||||
|
/// 换行、缩进、括号里的换行都不再影响判定。
|
||||||
#[test]
|
#[test]
|
||||||
fn the_private_copy_idiom_is_gone() {
|
fn the_private_copy_idiom_is_gone() {
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
@@ -170,13 +229,13 @@ fn the_private_copy_idiom_is_gone() {
|
|||||||
|
|
||||||
let mut offenders = Vec::new();
|
let mut offenders = Vec::new();
|
||||||
for path in files {
|
for path in files {
|
||||||
if exempt.contains(&path) {
|
if exempt.contains(&path) || is_test_only(&path) || is_known_copy(&path) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let text = std::fs::read_to_string(&path)
|
let text = std::fs::read_to_string(&path)
|
||||||
.unwrap_or_else(|error| panic!("读不动 {}:{error}", path.display()));
|
.unwrap_or_else(|error| panic!("读不动 {}:{error}", path.display()));
|
||||||
if product_part(&text).contains("or_else(|| Some(ui.color(") {
|
if let Some(hit) = private_copy(&product_part(&text)) {
|
||||||
offenders.push(path.display().to_string());
|
offenders.push(format!("{}(命中 {hit})", path.display()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
@@ -186,3 +245,42 @@ fn the_private_copy_idiom_is_gone() {
|
|||||||
静息它就是实色容器,叠层交给它。"
|
静息它就是实色容器,叠层交给它。"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 已知的**同一形态、本次不动**的站点:`(相对路径, 为什么留着)`。
|
||||||
|
///
|
||||||
|
/// 单独列出来而不是「用更窄的针绕开」:绕开等于假装它不存在。这一处是滑块的 handle(静息底是
|
||||||
|
/// 实心 `Primary`,hover/focus/pressed 叠状态层 —— 与选中项**同一套语义**,换成
|
||||||
|
/// `selected_fill(ui, state, OnSurface, Primary)` 是逐位等价的)。
|
||||||
|
/// `components/inputs/**` 不在 M299 的 scope,已作为 finding 报给塔。
|
||||||
|
const KNOWN_COPIES: [(&str, &str); 1] = [(
|
||||||
|
"components/inputs/slider/paint.rs",
|
||||||
|
"滑块 handle 的静息底是实心 Primary,与选中项同一套语义;components/inputs/** 不在 M299 scope,已报 finding",
|
||||||
|
)];
|
||||||
|
|
||||||
|
/// 这个文件是不是 [`KNOWN_COPIES`] 里挂过豁免的那一个。
|
||||||
|
fn is_known_copy(path: &Path) -> bool {
|
||||||
|
let src = src_dir();
|
||||||
|
KNOWN_COPIES.iter().any(|(relative, _)| {
|
||||||
|
path.strip_prefix(&src)
|
||||||
|
.is_ok_and(|tail| tail == Path::new(relative))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判据 3(豁免要**活着**):[`KNOWN_COPIES`] 里的文件必须**仍然**含那个形态。
|
||||||
|
///
|
||||||
|
/// 守的是「豁免越挂越多」:谁把那处收口了,这条立刻红,逼他回来删掉豁免行;谁想给自己文件
|
||||||
|
/// 挂个空豁免,也红。
|
||||||
|
#[test]
|
||||||
|
fn every_known_copy_is_still_there() {
|
||||||
|
let stale: Vec<&str> = KNOWN_COPIES
|
||||||
|
.iter()
|
||||||
|
.map(|(relative, _)| *relative)
|
||||||
|
.filter(|relative| private_copy(&product_source(relative)).is_none())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
stale.is_empty(),
|
||||||
|
"这些豁免已经失效(文件里没有那个形态了):{stale:?}\n\
|
||||||
|
收口了就把它从 `KNOWN_COPIES` 里删掉——豁免表不许留空条目。"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user