Create starlark attr crate Note for reviewers - I strongly advise reading the code in the following order: * globals.rs - Contains the starlark attr.* functions, which creates * schema.rs - Contains the schema of an attribute (all the metadata about the type), which is used to create an * attr.rs - Contains the value of an attribute, which can be converted to * value.rs - Converts an attribute to a starlark value (this part is done in a follow-up CL). Bug: 528225104 Change-Id: I14f205864735184f8a23295151f0a6596a6a6964 Reviewed-on: https://gn-review.googlesource.com/c/gn/+/23500 Commit-Queue: Matt Stark <msta@google.com> Reviewed-by: Philipp Wollermann <philwo@google.com> Reviewed-by: Cole Faust <colefaust@google.com> Reviewed-by: Matt Stark <msta@google.com> Reviewed-by: Richard Wang <richardwa@google.com>
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock index 512a232..f68f352 100644 --- a/src/gn/starlark/Cargo.lock +++ b/src/gn/starlark/Cargo.lock
@@ -109,6 +109,18 @@ ] [[package]] +name = "attr" +version = "0.1.0" +dependencies = [ + "allocative", + "either", + "starlark", + "starlark_derive", + "thiserror", + "types", +] + +[[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/src/gn/starlark/Cargo.toml b/src/gn/starlark/Cargo.toml index 57abfff..9d2a06d 100644 --- a/src/gn/starlark/Cargo.toml +++ b/src/gn/starlark/Cargo.toml
@@ -13,6 +13,7 @@ [workspace] members = [ ".", + "crates/attr", "crates/ffi", "crates/types", ]
diff --git a/src/gn/starlark/crates/attr/Cargo.toml b/src/gn/starlark/crates/attr/Cargo.toml new file mode 100644 index 0000000..32ded57 --- /dev/null +++ b/src/gn/starlark/crates/attr/Cargo.toml
@@ -0,0 +1,18 @@ +[package] +name = "attr" +version = "0.1.0" +edition = "2021" + +[lints] +workspace = true + +[lib] +doctest = false + +[dependencies] +starlark = { workspace = true } +starlark_derive = { workspace = true } +thiserror = { workspace = true } +allocative = { workspace = true } +types = { path = "../types" } +either = { workspace = true }
diff --git a/src/gn/starlark/crates/attr/src/allow_files.rs b/src/gn/starlark/crates/attr/src/allow_files.rs new file mode 100644 index 0000000..fca90f8 --- /dev/null +++ b/src/gn/starlark/crates/attr/src/allow_files.rs
@@ -0,0 +1,147 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use starlark::{ + typing::Ty, + values::{ + list::UnpackList, type_repr::StarlarkTypeRepr, Freeze, FreezeError, Freezer, UnpackValue, + Value, + }, +}; +use types::{Label, PackageRef, PathResolver}; + +use crate::attr::LabelOrFile; + +/// The rust type for the starlark value passed to attr.label(allow_files = ...) +#[derive(Debug, Clone, PartialEq, Eq, allocative::Allocative)] +pub enum AllowFiles { + None, + All, + Some(Vec<String>), +} + +impl StarlarkTypeRepr for AllowFiles { + type Canonical = either::Either<bool, UnpackList<String>>; + + fn starlark_type_repr() -> Ty { + Self::Canonical::starlark_type_repr() + } +} + +impl<'v> UnpackValue<'v> for AllowFiles { + type Error = starlark::Error; + + fn unpack_value_impl(value: Value<'v>) -> Result<Option<Self>, Self::Error> { + Ok(Self::Canonical::unpack_value(value)?.map(|c| match c { + either::Either::Left(false) => Self::None, + either::Either::Left(true) => Self::All, + either::Either::Right(list) => Self::Some(list.items), + })) + } +} + +impl AllowFiles { + pub(crate) fn validate(&self, path: &str) -> starlark::Result<()> { + match self { + Self::None => Err(crate::Error::NotALabel(path.to_owned()).into()), + Self::All => Ok(()), + Self::Some(exts) => { + let p = std::path::Path::new(path); + let file_name = p.file_name().and_then(|e| e.to_str()).unwrap_or(""); + if exts.iter().any(|ext| { + if ext.starts_with('.') { + file_name.ends_with(ext) + } else { + file_name + .strip_suffix(ext) + .is_some_and(|prefix| prefix.is_empty() || prefix.ends_with('.')) + } + }) { + Ok(()) + } else { + Err(crate::Error::DisallowedExtension { + file: p.to_path_buf(), + allowed: exts.clone(), + } + .into()) + } + }, + } + } +} + +impl Freeze for AllowFiles { + type Frozen = Self; + + fn freeze(self, _freezer: &Freezer) -> Result<Self::Frozen, FreezeError> { + Ok(self) + } +} + +pub(crate) fn parse_label_like( + s: &str, + allow_files: &AllowFiles, + relative_to: &PackageRef, + path_resolver: &PathResolver, +) -> starlark::Result<LabelOrFile> { + Ok( + if let Some(label) = Label::parse_maybe_label(s, relative_to)? { + LabelOrFile::Label(label) + } else { + // It's a file. + allow_files.validate(s)?; + LabelOrFile::File(path_resolver.source_file(relative_to, s)?) + }, + ) +} + +#[cfg(test)] +mod tests { + use starlark::values::FrozenHeap; + + use super::*; + use crate::{ + cfg::AttrCfg, + schema::{AllowFilesSchema, AttrKind, AttrSchema}, + Attr, + }; + + #[test] + fn test_allow_files_matching() { + let path_resolver = types::PathResolver::new_for_testing(); + let heap = FrozenHeap::new(); + + let check_match = |pattern: &str, file: &str| -> bool { + let schema = AttrSchema { + kind: AttrKind::Label, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::Many(AllowFiles::Some(vec![pattern.to_owned()])), + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }; + Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc(file))), + PackageRef::new("//allow_files").unwrap(), + &path_resolver, + ) + .is_ok() + }; + + assert!(check_match(".cc", "file.cc")); + assert!(check_match(".cc", "subdir/file.cc")); + assert!(!check_match(".cc", "file.h")); + assert!(!check_match(".cc", "cc")); + assert!(!check_match(".cc", "nonexistent.cc")); + + assert!(check_match("cc", "file.cc")); + assert!(!check_match("cc", "file.h")); + assert!(check_match("cc", "cc")); + + assert!(check_match("foo.cc", "foo.cc")); + assert!(check_match("foo.cc", "test.foo.cc")); + assert!(!check_match("foo.cc", "bar.cc")); + } +}
diff --git a/src/gn/starlark/crates/attr/src/attr.rs b/src/gn/starlark/crates/attr/src/attr.rs new file mode 100644 index 0000000..5e1fc56 --- /dev/null +++ b/src/gn/starlark/crates/attr/src/attr.rs
@@ -0,0 +1,535 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::fmt; + +use allocative::Allocative; +use starlark::{ + collections::{SmallMap, SmallSet}, + values::{ + list::UnpackList, none::NoneOr, Freeze, FreezeResult, Freezer, Heap, UnpackValue as _, + Value, + }, +}; +use types::{File, Label, LabelRef, PackageRef, PathResolver}; + +use crate::{ + allow_files::AllowFiles, + schema::{AttrKind, AttrSchema}, +}; + +/// A Starlark value that can be either a target `Label` or a source `File`. +/// +/// We do this because at the time we resolve attributes, the dependency has +/// not yet been resolved, and thus we don't know what files it expands to. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Allocative)] +pub enum LabelOrFile { + Label(Label), + File(File), +} + +impl LabelOrFile { + /// Converts this `LabelOrFile` into a Starlark `Value` allocated on the + /// heap. + pub fn to_value<'v>(&self, heap: &Heap<'v>) -> Value<'v> { + match self { + Self::Label(l) => heap.alloc(l.clone()), + Self::File(f) => heap.alloc(f.clone()), + } + } +} + +impl std::fmt::Display for LabelOrFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Label(l) => write!(f, "{l}"), + Self::File(file) => write!(f, "{file}"), + } + } +} + +/// Represents an actual attribute passed to a target. +/// +/// For example, sources = ["foo.cc"] => Attr::LabelList(vec![file for foo.cc]) +/// +/// Guaranteed to match the type of the corresponding `AttrSchema`'s `AttrKind`. +/// For example, the bool type defaults to false, attr.bool() can never produce +/// `Attr::Label(None)`. On the other hand, `attr.label()` defaults to +/// `Attr::Label(None)`. +#[derive(Clone, Debug, PartialEq, Eq, Allocative)] +pub enum Attr { + Bool(bool), + Int(i32), + String(String), + IntList(Vec<i32>), + StringList(Vec<String>), + StringListDict(SmallMap<String, Vec<String>>), + Label(Option<LabelOrFile>), + LabelList(Vec<LabelOrFile>), + StringDict(SmallMap<String, String>), + LabelKeyedStringDict(SmallMap<LabelOrFile, String>), + StringKeyedLabelDict(SmallMap<String, LabelOrFile>), + LabelListDict(SmallMap<String, Vec<LabelOrFile>>), +} + +impl std::fmt::Display for Attr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl Freeze for Attr { + type Frozen = Self; + + fn freeze(self, _freezer: &Freezer) -> FreezeResult<Self::Frozen> { + Ok(self) + } +} + +impl Attr { + /// Creates an `Attr` value from an input Starlark `Value`, applying default + /// values from the schema if not provided. + pub fn create( + schema: &AttrSchema, + // Distinguish between an explicit = None and an implicit value not provided. + value: Option<Value<'_>>, + package: &PackageRef, + path_resolver: &PathResolver, + ) -> starlark::Result<Self> { + if let Some(value) = value { + Self::create_without_defaults(schema, value, package, path_resolver) + } else if let Some(default) = schema.default.as_ref() { + // We already validated the default during attr schema creation. + Ok(default.clone()) + } else { + Err(crate::Error::MandatoryAttribute.into()) + } + } + + /// Coerces a Starlark `Value` into an `Attr` based on the validation rules + /// in the `AttrSchema`. + pub fn create_without_defaults( + schema: &AttrSchema, + value: Value<'_>, + package: &PackageRef, + path_resolver: &PathResolver, + ) -> starlark::Result<Self> { + let parse_label = |s: &str| -> starlark::Result<LabelOrFile> { + crate::allow_files::parse_label_like( + s, + schema.file_matcher().unwrap_or(&AllowFiles::None), + package, + path_resolver, + ) + }; + + let parse_label_list = |items: &[&str]| -> starlark::Result<Vec<LabelOrFile>> { + let mut unique = SmallSet::new(); + items + .iter() + .map(|s| { + let lf = parse_label(s)?; + if !unique.insert(lf.clone()) { + return Err(crate::Error::DuplicateLabel(lf).into()); + } + Ok(lf) + }) + .collect() + }; + + let parse_int = |val: Value<'_>| -> starlark::Result<i32> { + match i32::unpack_value_err(val) { + Ok(n) => Ok(n), + Err(e) => { + if let Ok(n) = i64::unpack_value_err(val) { + Err(starlark::Error::new_other(crate::Error::Int32Expected(n))) + } else { + Err(e) + } + }, + } + }; + + match &schema.kind { + AttrKind::Bool => Ok(Self::Bool(bool::unpack_value_err(value)?)), + AttrKind::Int { allowed } => { + let v = parse_int(value)?; + if allowed.as_ref().is_some_and(|a| !a.contains(&v)) { + return Err(crate::Error::IntNotAllowed(v).into()); + } + Ok(Self::Int(v)) + }, + AttrKind::String { allowed } => { + let v = <String>::unpack_value_err(value)?; + if allowed.as_ref().is_some_and(|a| !a.contains(&v)) { + return Err(crate::Error::StringNotAllowed(v).into()); + } + Ok(Self::String(v)) + }, + AttrKind::IntList => { + let list = UnpackList::<Value<'_>>::unpack_value_err(value)?; + if schema.disallow_empty && list.items.is_empty() { + return Err(crate::Error::Empty.into()); + } + let items = list + .items + .iter() + .map(|v| parse_int(*v)) + .collect::<starlark::Result<Vec<_>>>()?; + Ok(Self::IntList(items)) + }, + AttrKind::StringList => { + let list = UnpackList::<String>::unpack_value_err(value)?; + if schema.disallow_empty && list.items.is_empty() { + return Err(crate::Error::Empty.into()); + } + Ok(Self::StringList(list.items)) + }, + AttrKind::StringListDict => { + let dict = SmallMap::<&str, UnpackList<String>>::unpack_value_err(value)?; + if schema.disallow_empty && dict.is_empty() { + return Err(crate::Error::Empty.into()); + } + Ok(Self::StringListDict( + dict.into_iter() + .map(|(k, v)| (k.to_string(), v.items)) + .collect(), + )) + }, + AttrKind::Label => match NoneOr::<&str>::unpack_value_err(value)? { + // An explicit None is disallowed if the attribute is mandatory. + NoneOr::None => { + if schema.default.is_none() { + Err(crate::Error::MandatoryAttribute.into()) + } else { + Ok(Self::Label(None)) + } + }, + NoneOr::Other(s) => Ok(Self::Label(Some(parse_label(s)?))), + }, + AttrKind::LabelList => { + let list = UnpackList::<&str>::unpack_value_err(value)?; + if schema.disallow_empty && list.items.is_empty() { + return Err(crate::Error::Empty.into()); + } + Ok(Self::LabelList(parse_label_list(&list.items)?)) + }, + AttrKind::StringDict => { + let dict = SmallMap::<String, String>::unpack_value_err(value)?; + if schema.disallow_empty && dict.is_empty() { + return Err(crate::Error::Empty.into()); + } + Ok(Self::StringDict(dict)) + }, + AttrKind::LabelKeyedStringDict => { + let dict = SmallMap::<&str, &str>::unpack_value_err(value)?; + if schema.disallow_empty && dict.is_empty() { + return Err(crate::Error::Empty.into()); + } + let mut resolved = SmallMap::with_capacity(dict.len()); + for (k, v) in dict { + let lf = parse_label(k)?; + if resolved.insert(lf.clone(), v.to_string()).is_some() { + return Err(crate::Error::DuplicateLabel(lf).into()); + } + } + Ok(Self::LabelKeyedStringDict(resolved)) + }, + AttrKind::StringKeyedLabelDict => { + let dict = SmallMap::<&str, &str>::unpack_value_err(value)?; + if schema.disallow_empty && dict.is_empty() { + return Err(crate::Error::Empty.into()); + } + Ok(Self::StringKeyedLabelDict( + dict.into_iter() + .map(|(k, v)| Ok((k.to_string(), parse_label(v)?))) + .collect::<starlark::Result<SmallMap<_, _>>>()?, + )) + }, + AttrKind::LabelListDict => { + let dict = SmallMap::<&str, UnpackList<&str>>::unpack_value_err(value)?; + if schema.disallow_empty && dict.is_empty() { + return Err(crate::Error::Empty.into()); + } + Ok(Self::LabelListDict( + dict.into_iter() + .map(|(k, v)| Ok((k.to_string(), parse_label_list(&v.items)?))) + .collect::<starlark::Result<SmallMap<_, _>>>()?, + )) + }, + } + } + + /// Registers target dependencies contained within this attribute value. + pub fn register_dependencies<S: crate::Session>( + &self, + session: &S, + source: S::TargetRef, + toolchain: LabelRef<'_>, + ) { + match self { + Self::Label(Some(LabelOrFile::Label(lbl))) => { + session.register_dependency(source, lbl.as_ref(), toolchain); + }, + Self::LabelList(list) => { + for lf in list { + if let LabelOrFile::Label(lbl) = lf { + session.register_dependency(source.clone(), lbl.as_ref(), toolchain); + } + } + }, + Self::LabelKeyedStringDict(dict) => { + for (lf, _) in dict { + if let LabelOrFile::Label(lbl) = lf { + session.register_dependency(source.clone(), lbl.as_ref(), toolchain); + } + } + }, + Self::StringKeyedLabelDict(dict) => { + for (_, lf) in dict { + if let LabelOrFile::Label(lbl) = lf { + session.register_dependency(source.clone(), lbl.as_ref(), toolchain); + } + } + }, + Self::LabelListDict(dict) => { + for (_, list) in dict { + for lf in list { + if let LabelOrFile::Label(lbl) = lf { + session.register_dependency(source.clone(), lbl.as_ref(), toolchain); + } + } + } + }, + _ => {}, + } + } +} + +#[cfg(test)] +mod tests { + use starlark::values::FrozenHeap; + + use super::*; + use crate::{cfg::AttrCfg, schema::AllowFilesSchema}; + + #[test] + fn test_attr_bool() { + let pkg = PackageRef::new("//foo").unwrap(); + let path_resolver = types::PathResolver::new_for_testing(); + let heap = FrozenHeap::new(); + + let schema = AttrSchema { + kind: AttrKind::Bool, + default: Some(Attr::Bool(false)), + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }; + + // Test explicit true + assert_eq!( + Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc(true))), + pkg, + &path_resolver, + ) + .unwrap(), + Attr::Bool(true) + ); + + // Test default when value is None + assert_eq!( + Attr::create(&schema, None, pkg, &path_resolver).unwrap(), + Attr::Bool(false) + ); + + // Test non-boolean value fails + assert!(Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc(42))), + pkg, + &path_resolver, + ) + .is_err()); + } + + #[test] + fn test_attr_label_no_files() { + let pkg = PackageRef::new("//foo").unwrap(); + let path_resolver = types::PathResolver::new_for_testing(); + let heap = FrozenHeap::new(); + + let schema = AttrSchema { + kind: AttrKind::Label, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }; + + // Test parsing a label ":bar" + assert_eq!( + Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc(":bar"))), + pkg, + &path_resolver, + ) + .unwrap(), + Attr::Label(Some(LabelOrFile::Label(Label::new( + PackageRef::new("//foo").unwrap().to_owned(), + "bar".to_owned(), + )))) + ); + + // Test that a file string fails because files are not allowed + assert!(Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc("file.cc"))), + pkg, + &path_resolver, + ) + .is_err()); + + // Test that passing None to a mandatory label fails + assert!(Attr::create(&schema, Some(Value::new_none()), pkg, &path_resolver,).is_err()); + } + + #[test] + fn test_attr_label_allow_files() { + let pkg = PackageRef::new("//foo").unwrap(); + let path_resolver = types::PathResolver::new_for_testing(); + let heap = FrozenHeap::new(); + + let schema = AttrSchema { + kind: AttrKind::Label, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::Many(AllowFiles::Some(vec![".cc".to_owned()])), + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }; + + // Test a valid file "file.cc" (exists in testdata/foo/file.cc) + assert_eq!( + Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc("file.cc"))), + pkg, + &path_resolver, + ) + .unwrap(), + Attr::Label(Some(LabelOrFile::File( + path_resolver.source_file(pkg, "file.cc").unwrap() + ))) + ); + + // Test an invalid file "file.h" (extension not in allowed list) + assert!(Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc("file.h"))), + pkg, + &path_resolver, + ) + .is_err()); + + // Test that a label still resolves + assert_eq!( + Attr::create( + &schema, + Some(Value::new_frozen(heap.alloc(":bar"))), + pkg, + &path_resolver, + ) + .unwrap(), + Attr::Label(Some(LabelOrFile::Label(Label::new( + PackageRef::new("//foo").unwrap().to_owned(), + "bar".to_owned(), + )))) + ); + } + + #[test] + fn test_attr_label_list_duplicate_fail() { + use starlark::environment::Module; + + use crate::{cfg::AttrCfg, schema::AllowFilesSchema}; + + let pkg = PackageRef::new("//foo").unwrap(); + let path_resolver = types::PathResolver::new_for_testing(); + + Module::with_temp_heap(|module| { + let heap = module.heap(); + + let schema_list = AttrSchema { + kind: AttrKind::LabelList, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }; + + // Test same string label duplicate + assert!(Attr::create( + &schema_list, + Some(heap.alloc(vec![":bar", ":bar"])), + pkg, + &path_resolver, + ) + .is_err()); + + // Test different string representations resolving to same Label + assert!(Attr::create( + &schema_list, + Some(heap.alloc(vec![":bar", "//foo:bar"])), + pkg, + &path_resolver, + ) + .is_err()); + }); + } + + #[test] + fn test_attr_label_keyed_string_dict_duplicate_fail() { + use starlark::{collections::SmallMap, environment::Module, values::dict::Dict}; + + use crate::{cfg::AttrCfg, schema::AllowFilesSchema}; + + let pkg = PackageRef::new("//foo").unwrap(); + let path_resolver = types::PathResolver::new_for_testing(); + + Module::with_temp_heap(|module| { + let heap = module.heap(); + + let schema_dict = AttrSchema { + kind: AttrKind::LabelKeyedStringDict, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }; + + // Test different string keys resolving to same Label + let mut map = SmallMap::new(); + map.insert_hashed(heap.alloc(":bar").get_hashed().unwrap(), heap.alloc("val1")); + map.insert_hashed( + heap.alloc("//foo:bar").get_hashed().unwrap(), + heap.alloc("val2"), + ); + assert!(Attr::create( + &schema_dict, + Some(heap.alloc(Dict::new(map))), + pkg, + &path_resolver, + ) + .is_err()); + }); + } +}
diff --git a/src/gn/starlark/crates/attr/src/cfg.rs b/src/gn/starlark/crates/attr/src/cfg.rs new file mode 100644 index 0000000..e87f30d --- /dev/null +++ b/src/gn/starlark/crates/attr/src/cfg.rs
@@ -0,0 +1,35 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use allocative::Allocative; +use starlark::{ + typing::Ty, + values::{type_repr::StarlarkTypeRepr, UnpackValue, Value}, +}; + +/// The rust type for the starlark value passed to attr.label(cfg = ...) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Allocative)] +pub enum AttrCfg { + CurrentToolchain, +} + +impl StarlarkTypeRepr for AttrCfg { + type Canonical = String; + + fn starlark_type_repr() -> Ty { + String::starlark_type_repr() + } +} + +impl<'v> UnpackValue<'v> for AttrCfg { + type Error = starlark::Error; + + fn unpack_value_impl(value: Value<'v>) -> Result<Option<Self>, Self::Error> { + match value.unpack_str() { + Some("target") => Ok(Some(Self::CurrentToolchain)), + Some(s) => Err(crate::Error::ConfigTransitionNotImplemented(s.to_owned()).into()), + None => Ok(None), + } + } +}
diff --git a/src/gn/starlark/crates/attr/src/errors.rs b/src/gn/starlark/crates/attr/src/errors.rs new file mode 100644 index 0000000..f697bdd --- /dev/null +++ b/src/gn/starlark/crates/attr/src/errors.rs
@@ -0,0 +1,48 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::path::PathBuf; + +use starlark::values::UnpackValueError; + +/// Errors returned by target attribute validation and coercion. +#[derive(thiserror::Error, Debug, Clone)] +pub enum Error { + #[error("value is mandatory")] + MandatoryAttribute, + #[error("value cannot be empty")] + Empty, + #[error("file \"{file:?}\" has disallowed extension, allowed extensions are: {allowed:?}")] + DisallowedExtension { file: PathBuf, allowed: Vec<String> }, + #[error("config transition not implemented: {0}")] + ConfigTransitionNotImplemented(String), + #[error("mandatory and default are mutually exclusive")] + MandatoryAndDefaultMutuallyExclusive, + #[error("value {0} is not in allowed set")] + IntNotAllowed(i32), + #[error("value {0:?} is not in allowed set")] + StringNotAllowed(String), + #[error("allow_files and allow_single_file are mutually exclusive")] + AllowFilesMutuallyExclusive, + #[error("allow_empty = False requires the attribute to be mandatory or have a non-empty default value")] + AllowEmptyRequiresMandatoryOrDefault, + #[error("value `{0}` is not a label")] + NotALabel(String), + #[error("label `{0}` is duplicated")] + DuplicateLabel(crate::LabelOrFile), + #[error("got {0}, want value in signed 32-bit range")] + Int32Expected(i64), +} + +impl From<Error> for starlark::Error { + fn from(err: Error) -> Self { + Self::new_other(err) + } +} + +impl UnpackValueError for Error { + fn into_error(this: Self) -> starlark::Error { + starlark::Error::new_other(this) + } +}
diff --git a/src/gn/starlark/crates/attr/src/globals.rs b/src/gn/starlark/crates/attr/src/globals.rs new file mode 100644 index 0000000..e483de9 --- /dev/null +++ b/src/gn/starlark/crates/attr/src/globals.rs
@@ -0,0 +1,370 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::fmt::{self, Display, Formatter}; + +use allocative::{Allocative, Visitor}; +use starlark::{ + environment::{Methods, MethodsBuilder, MethodsStatic}, + eval::Evaluator, + values::{list::UnpackList, none::NoneOr, ProvidesStaticType, StarlarkValue, Value}, +}; +use starlark_derive::{starlark_module, starlark_value, NoSerialize}; + +use crate::{allow_files::AllowFiles, cfg::AttrCfg, AttrKind}; + +/// The type that all parameters of attr.* get converted to. +/// Mostly used because rust doesn't have default parameters, +/// so we just ..Default::default() the fields that aren't used. +#[derive(Default)] +pub struct AttrSpecArgs<'v> { + pub(crate) default: Option<Value<'v>>, + pub(crate) mandatory: Option<bool>, + pub(crate) allow_empty: Option<bool>, + pub(crate) allow_files: Option<AllowFiles>, + pub(crate) allow_single_file: Option<AllowFiles>, + pub(crate) cfg: Option<AttrCfg>, + pub(crate) doc: Option<NoneOr<String>>, +} + +/// The Starlark `attr` module containing functions to declare rule attributes. +#[derive(Debug, ProvidesStaticType, NoSerialize)] +pub struct AttrModule { + pub make_attr_schema: for<'v, 'a, 'e> fn( + AttrKind, + AttrSpecArgs<'v>, + &mut Evaluator<'v, 'a, 'e>, + ) -> starlark::Result<Value<'v>>, +} + +impl Allocative for AttrModule { + fn visit<'a, 'b: 'a>(&self, visitor: &'a mut Visitor<'b>) { + let visitor = visitor.enter_self_sized::<Self>(); + visitor.exit(); + } +} + +starlark::starlark_simple_value!(AttrModule); + +impl Display for AttrModule { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "attr") + } +} + +#[starlark_value(type = "attr")] +impl<'v> StarlarkValue<'v> for AttrModule { + fn get_methods() -> Option<&'static Methods> { + static RES: MethodsStatic = MethodsStatic::new("attr", attr_methods); + Some(RES.methods()) + } +} + +#[starlark_module] +pub fn attr_methods(builder: &mut MethodsBuilder) { + fn bool<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::Bool, + AttrSpecArgs { + default, + doc, + mandatory, + ..Default::default() + }, + eval, + ) + } + + fn int<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + values: Option<UnpackList<i32>>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::Int { + allowed: values.map(|v| v.into_iter().collect()), + }, + AttrSpecArgs { + default, + doc, + mandatory, + ..Default::default() + }, + eval, + ) + } + + fn int_list<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::IntList, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + ..Default::default() + }, + eval, + ) + } + + #[allow(clippy::too_many_arguments)] + fn label<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_files: Option<AllowFiles>, + allow_single_file: Option<AllowFiles>, + cfg: Option<AttrCfg>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::Label, + AttrSpecArgs { + default, + doc, + mandatory, + allow_files, + allow_single_file, + cfg, + ..Default::default() + }, + eval, + ) + } + + fn label_keyed_string_dict<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + allow_files: Option<AllowFiles>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::LabelKeyedStringDict, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + allow_files, + ..Default::default() + }, + eval, + ) + } + + #[allow(clippy::too_many_arguments)] + fn label_list<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + allow_files: Option<AllowFiles>, + cfg: Option<AttrCfg>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::LabelList, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + allow_files, + cfg, + ..Default::default() + }, + eval, + ) + } + + #[allow(clippy::too_many_arguments)] + fn label_list_dict<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + allow_files: Option<AllowFiles>, + cfg: Option<AttrCfg>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::LabelListDict, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + allow_files, + cfg, + ..Default::default() + }, + eval, + ) + } + + fn string<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + values: Option<UnpackList<String>>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::String { + allowed: values.map(|v| v.into_iter().collect()), + }, + AttrSpecArgs { + default, + doc, + mandatory, + ..Default::default() + }, + eval, + ) + } + + fn string_dict<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::StringDict, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + ..Default::default() + }, + eval, + ) + } + + #[allow(clippy::too_many_arguments)] + fn string_keyed_label_dict<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + allow_files: Option<AllowFiles>, + cfg: Option<AttrCfg>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::StringKeyedLabelDict, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + allow_files, + cfg, + ..Default::default() + }, + eval, + ) + } + + fn string_list<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::StringList, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + ..Default::default() + }, + eval, + ) + } + + fn string_list_dict<'v>( + #[starlark(this)] this: &AttrModule, + #[starlark(require = named)] default: Option<Value<'v>>, + doc: Option<NoneOr<String>>, + mandatory: Option<bool>, + allow_empty: Option<bool>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + (this.make_attr_schema)( + AttrKind::StringListDict, + AttrSpecArgs { + default, + doc, + mandatory, + allow_empty, + ..Default::default() + }, + eval, + ) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use starlark::assert::Assert; + + use super::*; + use crate::AttrSchema; + + fn make_attr_schema<'v>( + kind: crate::AttrKind, + args: AttrSpecArgs<'v>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<starlark::values::Value<'v>> { + let package = types::PackageRef::root(); + AttrSchema::create( + kind, + args, + package, + &types::PathResolver::new_for_testing(), + &eval.heap(), + ) + } + + pub fn new_attr_assert() -> Assert<'static> { + let mut assert = Assert::new(); + assert.globals_add(|builder| { + builder.set("attr", super::AttrModule { make_attr_schema }); + }); + assert + } +}
diff --git a/src/gn/starlark/crates/attr/src/lib.rs b/src/gn/starlark/crates/attr/src/lib.rs new file mode 100644 index 0000000..c3819d6 --- /dev/null +++ b/src/gn/starlark/crates/attr/src/lib.rs
@@ -0,0 +1,19 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +pub mod allow_files; +pub mod attr; +pub mod cfg; +pub mod errors; +pub mod globals; +pub mod schema; +pub mod traits; + +pub use allow_files::AllowFiles; +pub use attr::{Attr, LabelOrFile}; +pub use cfg::AttrCfg; +pub use errors::Error; +pub use globals::{AttrModule, AttrSpecArgs}; +pub use schema::{AllowFilesSchema, AttrKind, AttrSchema}; +pub use traits::{EvalContext, EvalContextAttrExt, Session, TargetRef};
diff --git a/src/gn/starlark/crates/attr/src/schema.rs b/src/gn/starlark/crates/attr/src/schema.rs new file mode 100644 index 0000000..642f99e --- /dev/null +++ b/src/gn/starlark/crates/attr/src/schema.rs
@@ -0,0 +1,422 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::{ + fmt, + fmt::{Display, Formatter}, +}; + +use allocative::Allocative; +use starlark::{ + starlark_simple_value, + values::{ + none::NoneOr, Freeze, FreezeResult, Freezer, Heap, ProvidesStaticType, StarlarkValue, + Trace, Value, + }, +}; +use starlark_derive::{starlark_value, NoSerialize}; +use types::{PackageRef, PathResolver}; + +use crate::{allow_files::AllowFiles, cfg::AttrCfg, globals::AttrSpecArgs, Attr}; + +/// The underlying data type of a target attribute (e.g. Bool, String, +/// LabelList). +#[derive(Debug, Clone, PartialEq, Eq, Allocative)] +pub enum AttrKind { + Bool, + // Allowed is typically *very* small, so we use a Vec. + Int { allowed: Option<Vec<i32>> }, + IntList, + Label, + LabelKeyedStringDict, + LabelList, + LabelListDict, + // Allowed is typically *very* small, so we use a Vec. + String { allowed: Option<Vec<String>> }, + StringDict, + StringKeyedLabelDict, + StringList, + StringListDict, +} + +/// Schema specifying what files (single or multiple) are allowed on a +/// label-like attribute. +#[derive(Debug, Clone, PartialEq, Eq, Allocative)] +pub enum AllowFilesSchema { + None, + Single(AllowFiles), + Many(AllowFiles), +} + +/// Represents an attr.foo(...) parameter. +/// Eg. attr.label(allow_single_file = True) +#[derive(Debug, Clone, PartialEq, Eq, Trace, NoSerialize, Allocative)] +pub struct AttrSchema { + pub(crate) kind: AttrKind, + pub(crate) default: Option<Attr>, + pub(crate) disallow_empty: bool, + pub(crate) allow_files: AllowFilesSchema, + pub(crate) cfg: AttrCfg, + pub(crate) doc: String, +} + +// Safety: AttrSchema does not contain lifetime parameters, so it satisfies the +// lifetime requirement of StaticType. +unsafe impl ProvidesStaticType<'_> for AttrSchema { + type StaticType = Self; +} + +starlark_simple_value!(AttrSchema); + +impl Freeze for AttrSchema { + type Frozen = Self; + + fn freeze(self, _freezer: &Freezer) -> FreezeResult<Self::Frozen> { + Ok(self) + } +} + +impl Display for AttrSchema { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +#[starlark_value(type = "AttrSchema")] +// Clippy recommends eliding the 'v lifetime, but the #[starlark_value] macro +// requires it to be explicitly declared. +#[allow(clippy::elidable_lifetime_names)] +impl<'v> StarlarkValue<'v> for AttrSchema { + fn collect_repr(&self, collector: &mut String) { + use std::fmt::Write as _; + write!(collector, "{self:?}").unwrap(); + } +} + +impl AttrSchema { + /// Creates an `AttrSchema` from validation attributes and registers it. + pub fn create<'v>( + kind: AttrKind, + args: AttrSpecArgs<'v>, + package: &PackageRef, + path_resolver: &PathResolver, + heap: &Heap<'v>, + ) -> starlark::Result<Value<'v>> { + let mut schema = Self { + kind, + default: None, + disallow_empty: match args.allow_empty { + None => false, + Some(b) => !b, + }, + allow_files: match ( + args.allow_single_file.unwrap_or(AllowFiles::None), + args.allow_files.unwrap_or(AllowFiles::None), + ) { + (AllowFiles::None, AllowFiles::None) => AllowFilesSchema::None, + (af, AllowFiles::None) => AllowFilesSchema::Single(af), + (AllowFiles::None, af) => AllowFilesSchema::Many(af), + _ => return Err(crate::Error::AllowFilesMutuallyExclusive.into()), + }, + cfg: args.cfg.unwrap_or(AttrCfg::CurrentToolchain), + doc: match args.doc { + None | Some(NoneOr::None) => String::new(), + Some(NoneOr::Other(s)) => s, + }, + }; + + // Unify explicit and implicit none specifically for labels. + // attr.label is the only type for which None is a valid value. + let default = match args.default { + None => None, + Some(x) => { + if schema.kind == AttrKind::Label && x.is_none() { + None + } else { + Some(x) + } + }, + }; + + let mandatory = args.mandatory.unwrap_or(false); + if schema.disallow_empty && !mandatory && default.is_none() { + return Err(crate::Error::AllowEmptyRequiresMandatoryOrDefault.into()); + } + + schema.default = match (mandatory, default) { + (true, None) => None, + (false, None) => Some(match &schema.kind { + AttrKind::Bool => Attr::Bool(false), + // If I create an `attr.int(values = [1])` and don't provide a value to bazel, it + // sets the default value 0. This is wierd, but by providing this, + // we maintain consistency with bazel. + AttrKind::Int { .. } => Attr::Int(0), + AttrKind::String { .. } => Attr::String(Default::default()), + AttrKind::IntList => Attr::IntList(Default::default()), + AttrKind::StringList => Attr::StringList(Default::default()), + AttrKind::LabelList => Attr::LabelList(Default::default()), + AttrKind::StringListDict => Attr::StringListDict(Default::default()), + AttrKind::StringDict => Attr::StringDict(Default::default()), + AttrKind::LabelKeyedStringDict => Attr::LabelKeyedStringDict(Default::default()), + AttrKind::StringKeyedLabelDict => Attr::StringKeyedLabelDict(Default::default()), + AttrKind::LabelListDict => Attr::LabelListDict(Default::default()), + AttrKind::Label => Attr::Label(None), + }), + (false, Some(v)) => { + // We provide an empty param name because you can write the following code: + // p = attr.string(default = 1) + // rule(..., attrs = {"foo": p}) + // This means that at the time you call attr.string, no parameter name has + // been set. + Some(Attr::create_without_defaults( + &schema, + v, + package, + path_resolver, + )?) + }, + (true, Some(_)) => { + return Err(crate::Error::MandatoryAndDefaultMutuallyExclusive.into()); + }, + }; + + Ok(heap.alloc(schema)) + } + + /// Returns the allowed files schema for this attribute. + pub fn allow_files(&self) -> &AllowFilesSchema { + &self.allow_files + } + + /// Returns the default value of this attribute, if any. + pub fn default(&self) -> Option<&Attr> { + self.default.as_ref() + } + + /// Returns the file matcher if this attribute schema allows files. + pub fn file_matcher(&self) -> Option<&AllowFiles> { + match &self.allow_files { + AllowFilesSchema::Single(s) => Some(s), + AllowFilesSchema::Many(s) => Some(s), + AllowFilesSchema::None => None, + } + } +} + +#[cfg(test)] +mod tests { + use types::Label; + + use super::*; + use crate::{globals::tests::new_attr_assert, LabelOrFile}; + + #[track_caller] + fn assert_eq_schema( + a: &mut starlark::assert::Assert<'static>, + code: &str, + expected: &AttrSchema, + ) { + let val = a.pass(code); + let unpacked: &AttrSchema = + starlark::values::UnpackValue::unpack_value_err(val.value()).unwrap(); + assert_eq!(unpacked, expected); + } + + #[test] + fn test_schema_bool() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.bool()", + &AttrSchema { + kind: AttrKind::Bool, + default: Some(Attr::Bool(false)), + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + + a.fail("attr.bool(default=None)", "Expected `bool`"); + } + + #[test] + fn test_schema_int() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.int(default=42, doc='An integer')", + &AttrSchema { + kind: AttrKind::Int { allowed: None }, + default: Some(Attr::Int(42)), + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: "An integer".to_string(), + }, + ); + + a.fail( + "attr.int(values=[1, 2], default=3)", + "value 3 is not in allowed set", + ); + } + + #[test] + fn test_schema_string() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.string(values=['foo', 'bar'], mandatory=True)", + &AttrSchema { + kind: AttrKind::String { + allowed: Some(vec!["foo".to_string(), "bar".to_string()]), + }, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + + a.fail( + "attr.string(values=['a', 'b'], default='c')", + "value \"c\" is not in allowed set", + ); + + a.fail("attr.string('hello')", "Found 1 extra positional argument"); + } + + #[test] + fn test_schema_label() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.label(allow_files=True, default=':foo')", + &AttrSchema { + kind: AttrKind::Label, + default: Some(Attr::Label(Some(LabelOrFile::Label(Label::new( + PackageRef::root().to_owned(), + "foo".to_owned(), + ))))), + disallow_empty: false, + allow_files: AllowFilesSchema::Many(AllowFiles::All), + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + + assert_eq_schema( + &mut a, + "attr.label(default=None)", + &AttrSchema { + kind: AttrKind::Label, + default: Some(Attr::Label(None)), + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + } + + #[test] + fn test_schema_string_list() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.string_list(allow_empty=False, mandatory=True)", + &AttrSchema { + kind: AttrKind::StringList, + default: None, + disallow_empty: true, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + + a.fail( + "attr.string_list(allow_empty=False)", + "allow_empty = False requires the attribute to be mandatory or have a non-empty default value", + ); + a.fail( + "attr.string_list(allow_empty=False, default=[])", + "value cannot be empty", + ); + } + + #[test] + fn test_schema_string_default() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.string(default='hello')", + &AttrSchema { + kind: AttrKind::String { allowed: None }, + default: Some(Attr::String("hello".to_string())), + disallow_empty: false, + allow_files: AllowFilesSchema::None, + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + } + + #[test] + fn test_schema_string_dict() { + let a = new_attr_assert(); + + a.fail( + "attr.string_dict(allow_empty=False, default={})", + "value cannot be empty", + ); + } + + #[test] + fn test_schema_allow_files_error() { + let a = new_attr_assert(); + + a.fail( + "attr.label(allow_files=True, allow_single_file=True)", + "allow_files and allow_single_file are mutually exclusive", + ); + } + + #[test] + fn test_schema_mandatory_default_error() { + let a = new_attr_assert(); + + a.fail( + "attr.bool(mandatory=True, default=True)", + "mandatory and default are mutually exclusive", + ); + } + + #[test] + fn test_schema_label_keyed_string_dict() { + let mut a = new_attr_assert(); + + assert_eq_schema( + &mut a, + "attr.label_keyed_string_dict(allow_files=True, mandatory=True)", + &AttrSchema { + kind: AttrKind::LabelKeyedStringDict, + default: None, + disallow_empty: false, + allow_files: AllowFilesSchema::Many(AllowFiles::All), + cfg: AttrCfg::CurrentToolchain, + doc: String::new(), + }, + ); + } +}
diff --git a/src/gn/starlark/crates/attr/src/traits.rs b/src/gn/starlark/crates/attr/src/traits.rs new file mode 100644 index 0000000..87132af --- /dev/null +++ b/src/gn/starlark/crates/attr/src/traits.rs
@@ -0,0 +1,16 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Re-export the traits from types so caller crates can access them seamlessly. +pub use types::{EvalContext, Session, TargetRef}; + +/// Extension trait for EvalContext to support target creation. +pub trait EvalContextAttrExt: types::EvalContext { + fn create_starlark_target( + &self, + target_name: &str, + rule: starlark::values::FrozenValue, + attrs: Vec<crate::Attr>, + ) -> starlark::Result<<Self::Session as types::Session>::TargetRef>; +}
diff --git a/src/gn/starlark/src/testdata/allow_files/bar.cc b/src/gn/starlark/src/testdata/allow_files/bar.cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/bar.cc
diff --git a/src/gn/starlark/src/testdata/allow_files/cc b/src/gn/starlark/src/testdata/allow_files/cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/cc
diff --git a/src/gn/starlark/src/testdata/allow_files/file.cc b/src/gn/starlark/src/testdata/allow_files/file.cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/file.cc
diff --git a/src/gn/starlark/src/testdata/allow_files/file.h b/src/gn/starlark/src/testdata/allow_files/file.h new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/file.h
diff --git a/src/gn/starlark/src/testdata/allow_files/foo.cc b/src/gn/starlark/src/testdata/allow_files/foo.cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/foo.cc
diff --git a/src/gn/starlark/src/testdata/allow_files/subdir/file.cc b/src/gn/starlark/src/testdata/allow_files/subdir/file.cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/subdir/file.cc
diff --git a/src/gn/starlark/src/testdata/allow_files/test.foo.cc b/src/gn/starlark/src/testdata/allow_files/test.foo.cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/allow_files/test.foo.cc
diff --git a/src/gn/starlark/src/testdata/foo/file.cc b/src/gn/starlark/src/testdata/foo/file.cc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gn/starlark/src/testdata/foo/file.cc