extern crate rustc_ast;
extern crate rustc_span;
use crate::lint_utils::{filename_str, is_temp_path};
use gts::{GtsIdSegment, GtsOps};
use rustc_ast::token::LitKind;
use rustc_ast::{AttrKind, Attribute, Expr, ExprKind, Item, ItemKind};
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
use rustc_span::Span;
use std::cell::RefCell;
use std::collections::HashSet;
thread_local! {
static SKIP_SPANS: RefCell<HashSet<Span>> = RefCell::new(HashSet::new());
static IN_TEST_DEPTH: RefCell<u32> = const { RefCell::new(0) };
}
fn default_allowed_vendors() -> Vec<String> {
vec!["cf".to_owned()]
}
fn default_test_allowed_vendors() -> Vec<String> {
[
"cf", "vendor", "example", "fabrikam", "contoso", "acme", "globex",
]
.iter()
.map(|&s| s.to_owned())
.collect()
}
#[derive(serde::Deserialize)]
struct Config {
#[serde(default = "default_allowed_vendors")]
allowed_vendors: Vec<String>,
#[serde(default = "default_test_allowed_vendors")]
test_allowed_vendors: Vec<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
allowed_vendors: default_allowed_vendors(),
test_allowed_vendors: default_test_allowed_vendors(),
}
}
}
pub(crate) struct De0901GtsStringPattern {
allowed_vendors: Vec<String>,
test_allowed_vendors: Vec<String>,
}
impl De0901GtsStringPattern {
pub fn new() -> Self {
let config: Config = dylint_linting::config_or_default(crate::LIBRARY_NAME);
Self {
allowed_vendors: config.allowed_vendors,
test_allowed_vendors: config.test_allowed_vendors,
}
}
}
dylint_linting::impl_pre_expansion_lint! {
#[doc = include_str!("../../docs/de09_gts_layer/de0901_gts_string_pattern/README.md")]
pub DE0901_GTS_STRING_PATTERN,
Deny,
"invalid GTS string pattern (DE0901)",
De0901GtsStringPattern::new()
}
impl EarlyLintPass for De0901GtsStringPattern {
fn check_crate_post(&mut self, _cx: &EarlyContext<'_>, _krate: &rustc_ast::Crate) {
SKIP_SPANS.with(|s| s.borrow_mut().clear());
IN_TEST_DEPTH.with(|d| *d.borrow_mut() = 0);
}
fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) {
self.check_struct_to_gts_schema_attr(cx, attr);
}
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
if is_test_item(item) {
IN_TEST_DEPTH.with(|d| *d.borrow_mut() += 1);
}
let (item_name, init_expr): (&str, Option<&Expr>) = match &item.kind {
ItemKind::Const(ci) => (
ci.ident.name.as_str(),
ci.rhs.as_ref().map(|rhs| rhs.expr()),
),
ItemKind::Static(si) => (si.ident.name.as_str(), si.expr.as_deref()),
_ => return,
};
let Some(init) = init_expr else { return };
let Some(s) = Self::string_lit_value(init) else {
return;
};
if !s.starts_with("gts.") || !s.contains('*') {
return;
}
let result = GtsOps::parse_id(s);
if !result.ok {
cx.span_lint(DE0901_GTS_STRING_PATTERN, item.span, |diag| {
diag.primary_message(format!(
"invalid GTS wildcard pattern in `{item_name}`: '{s}' (DE0901)"
));
diag.note(result.error);
diag.help("Example: gts.cf.core.srr.resource.v1~*");
});
SKIP_SPANS.with(|spans| {
collect_nested_spans(init, &mut spans.borrow_mut());
});
return;
}
self.check_vendors_in_parse_result(cx, item.span, s, &result);
if !item_name.ends_with("_WILDCARD") {
cx.span_lint(DE0901_GTS_STRING_PATTERN, item.span, |diag| {
diag.primary_message(format!(
"GTS wildcard string in `const`/`static` `{item_name}` must have a name ending with `_WILDCARD` (DE0901)"
));
diag.note(format!(
"found wildcard GTS pattern `{s}` stored in `{item_name}`"
));
diag.help(format!(
"rename to `{item_name}_WILDCARD` or use a non-wildcard value"
));
});
}
SKIP_SPANS.with(|spans| {
collect_nested_spans(init, &mut spans.borrow_mut());
});
}
fn check_item_post(&mut self, _cx: &EarlyContext<'_>, item: &Item) {
if is_test_item(item) {
IN_TEST_DEPTH.with(|d| *d.borrow_mut() -= 1);
}
}
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
if let ExprKind::MethodCall(method_call) = &expr.kind {
let method_name = method_call.seg.ident.name.as_str();
if method_name == "starts_with" {
SKIP_SPANS.with(|spans| {
let mut spans = spans.borrow_mut();
spans.insert(method_call.receiver.span);
for arg in &method_call.args {
spans.insert(arg.span);
}
});
return;
}
if method_name == "resource_pattern"
|| method_name == "with_pattern"
|| method_name == "resolve_to_uuids"
{
for arg in &method_call.args {
self.validate_nested_gts_strings(cx, arg, true);
}
SKIP_SPANS.with(|spans| {
let mut spans = spans.borrow_mut();
for arg in &method_call.args {
collect_nested_spans(arg, &mut spans);
}
});
}
}
if let ExprKind::Call(func, args) = &expr.kind
&& is_gts_wildcard_new_call(func)
{
SKIP_SPANS.with(|spans| {
let mut spans = spans.borrow_mut();
for arg in args {
collect_nested_spans(arg, &mut spans);
}
});
for arg in args {
self.validate_nested_gts_strings(cx, arg, true);
}
return;
}
let should_skip = SKIP_SPANS.with(|spans| spans.borrow().contains(&expr.span));
if should_skip {
return;
}
self.check_gts_make_instance_id_call(cx, expr);
if let ExprKind::MethodCall(method_call) = &expr.kind {
let method_name = method_call.seg.ident.name.as_str();
if method_name == "resource_pattern"
|| method_name == "with_pattern"
|| method_name == "resolve_to_uuids"
{
return;
}
for arg in &method_call.args {
self.check_gts_string_literal(cx, arg);
}
return;
}
self.check_gts_string_literal(cx, expr);
}
}
fn collect_nested_spans(expr: &Expr, spans: &mut HashSet<Span>) {
spans.insert(expr.span);
match &expr.kind {
ExprKind::MethodCall(mc) => {
collect_nested_spans(&mc.receiver, spans);
for arg in &mc.args {
collect_nested_spans(arg, spans);
}
}
ExprKind::AddrOf(_, _, inner) => {
collect_nested_spans(inner, spans);
}
ExprKind::Array(elements) => {
for elem in elements {
collect_nested_spans(elem, spans);
}
}
ExprKind::Call(func, args) => {
collect_nested_spans(func, spans);
for arg in args {
collect_nested_spans(arg, spans);
}
}
ExprKind::Tup(elements) => {
for elem in elements {
collect_nested_spans(elem, spans);
}
}
ExprKind::Paren(inner) => {
collect_nested_spans(inner, spans);
}
_ => {}
}
}
fn is_gts_wildcard_new_call(func_expr: &Expr) -> bool {
let ExprKind::Path(_, path) = &func_expr.kind else {
return false;
};
let segments = &path.segments;
if segments.len() < 2 {
return false;
}
let last = segments.last().unwrap();
if last.ident.name.as_str() != "new" {
return false;
}
segments
.iter()
.any(|seg| seg.ident.name.as_str() == "GtsWildcard")
}
fn is_in_test() -> bool {
IN_TEST_DEPTH.with(|d| *d.borrow() > 0)
}
fn is_ui_test(cx: &EarlyContext<'_>, span: Span) -> bool {
let Some(file_path) = filename_str(cx.sess().source_map(), span) else {
return false;
};
is_temp_path(&file_path)
}
fn is_test_item(item: &Item) -> bool {
item.attrs.iter().any(|attr| {
let AttrKind::Normal(normal) = &attr.kind else {
return false;
};
let segments = &normal.item.path.segments;
if segments.len() != 1 {
return false;
}
let name = segments[0].ident.name.as_str();
if name == "test" {
return true;
}
if name == "cfg"
&& let Some(items) = normal.item.meta_item_list()
{
return items.iter().any(|nested| {
nested.meta_item().is_some_and(|mi| {
mi.path.segments.len() == 1 && mi.path.segments[0].ident.name.as_str() == "test"
})
});
}
false
})
}
impl De0901GtsStringPattern {
fn check_gts_make_instance_id_call(&self, cx: &EarlyContext<'_>, expr: &Expr) {
let ExprKind::Call(func, args) = &expr.kind else {
return;
};
if args.len() != 1 {
return;
}
let Some(arg0) = args.first() else {
return;
};
let Some(arg_str) = Self::string_lit_value(arg0) else {
return;
};
let ExprKind::Path(_, path) = &func.kind else {
return;
};
let Some(last) = path.segments.last() else {
return;
};
if last.ident.name.as_str() != "gts_make_instance_id" {
return;
}
self.validate_instance_id_segment(cx, expr.span, arg_str);
}
fn validate_nested_gts_strings(
&self,
cx: &EarlyContext<'_>,
expr: &Expr,
allow_wildcards: bool,
) {
if Self::string_lit_value(expr).is_some() {
self.check_gts_string_literal_with_wildcard_flag(cx, expr, allow_wildcards);
return;
}
match &expr.kind {
ExprKind::MethodCall(mc) => {
self.validate_nested_gts_strings(cx, &mc.receiver, allow_wildcards);
for arg in &mc.args {
self.validate_nested_gts_strings(cx, arg, allow_wildcards);
}
}
ExprKind::AddrOf(_, _, inner) => {
self.validate_nested_gts_strings(cx, inner, allow_wildcards);
}
ExprKind::Array(elements) => {
for elem in elements {
self.validate_nested_gts_strings(cx, elem, allow_wildcards);
}
}
ExprKind::Call(_, args) => {
for arg in args {
self.validate_nested_gts_strings(cx, arg, allow_wildcards);
}
}
ExprKind::Tup(elements) => {
for elem in elements {
self.validate_nested_gts_strings(cx, elem, allow_wildcards);
}
}
ExprKind::Paren(inner) => {
self.validate_nested_gts_strings(cx, inner, allow_wildcards);
}
_ => {}
}
}
fn check_gts_string_literal(&self, cx: &EarlyContext<'_>, expr: &Expr) {
self.check_gts_string_literal_with_wildcard_flag(cx, expr, false);
}
fn check_gts_string_literal_with_wildcard_flag(
&self,
cx: &EarlyContext<'_>,
expr: &Expr,
allow_wildcards: bool,
) {
if let Some(s) = Self::string_lit_value(expr) {
let s = s.trim();
if s.starts_with("gts.") {
if allow_wildcards {
self.validate_any_gts_id_allow_wildcards(cx, expr.span, s);
} else {
self.validate_any_gts_id(cx, expr.span, s);
}
return;
}
if s.contains(':') {
for part in s.split(':') {
if part.trim().starts_with("gts.") {
self.validate_any_gts_id_allow_wildcards(cx, expr.span, part.trim());
break; }
}
}
}
}
fn string_lit_value(expr: &Expr) -> Option<&str> {
match &expr.kind {
ExprKind::Lit(lit) => match lit.kind {
LitKind::Str | LitKind::StrRaw(_) => Some(lit.symbol.as_str()),
_ => None,
},
_ => None,
}
}
fn check_struct_to_gts_schema_attr(&self, cx: &EarlyContext<'_>, attr: &Attribute) {
let AttrKind::Normal(normal_attr) = &attr.kind else {
return;
};
if normal_attr.item.path.segments.len() != 1
|| normal_attr.item.path.segments[0].ident.name.as_str() != "struct_to_gts_schema"
{
return;
}
let Some(items) = normal_attr.item.meta_item_list() else {
return;
};
for nested in items {
let Some(mi) = nested.meta_item() else {
continue;
};
if mi.path.segments.len() != 1 || mi.path.segments[0].ident.name.as_str() != "schema_id"
{
continue;
}
let Some(val) = mi.value_str() else {
continue;
};
self.validate_schema_id(cx, mi.span, val.as_str());
}
}
fn validate_schema_id(&self, cx: &EarlyContext<'_>, span: rustc_span::Span, s: &str) {
let s = s.trim();
if s.contains('*') {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!("wildcards are not allowed in schema_id: '{}' (DE0901)", s));
diag.note("Wildcards (*) are only allowed in permission strings, not in schema_id attributes");
diag.help("Use concrete type names in schema_id");
});
return;
}
let result = GtsOps::parse_id(s);
if !result.ok {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!("invalid GTS schema_id: '{}' (DE0901)", s));
diag.note(result.error);
diag.help("Example: gts.cf.core.events.type.v1~");
});
return;
}
if result.is_schema != Some(true) {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!(
"schema_id must be a type schema, not an instance: '{}' (DE0901)",
s
));
diag.note("schema_id must end with '~' to indicate it's a type schema");
diag.help("Example: gts.cf.core.events.type.v1~");
});
} else {
self.check_vendors_in_parse_result(cx, span, s, &result);
}
}
fn allowed_vendors(&self, cx: &EarlyContext<'_>, span: Span) -> &[String] {
if is_in_test() || is_ui_test(cx, span) {
&self.test_allowed_vendors
} else {
&self.allowed_vendors
}
}
fn format_vendors(vendors: &[String]) -> String {
vendors.join(", ")
}
fn validate_instance_id_segment(&self, cx: &EarlyContext<'_>, span: rustc_span::Span, s: &str) {
let s = s.trim();
if s.contains('~') || s.contains(':') {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!(
"gts_make_instance_id expects a single GTS segment, got: '{}' (DE0901)",
s
));
diag.help("Example: vendor.package.sku.abc.v1");
});
return;
}
if s.contains('*') {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!(
"wildcards are not allowed in instance id segments: '{}' (DE0901)",
s
));
diag.help("Example: vendor.package.sku.abc.v1");
});
return;
}
let vendors = self.allowed_vendors(cx, span);
match GtsIdSegment::new(0, 0, s) {
Err(e) => {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!("invalid GTS segment: '{}' (DE0901)", s));
diag.note(e.to_string());
diag.help("Example: vendor.package.sku.abc.v1");
});
}
Ok(seg) if !vendors.iter().any(|v| v == &seg.vendor) => {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!(
"invalid GTS vendor in segment: '{}' (DE0901)",
s
));
diag.note(format!(
"found vendor '{}', allowed vendors: {}",
seg.vendor,
Self::format_vendors(vendors),
));
});
}
Ok(_) => {}
}
}
fn check_vendors_in_parse_result(
&self,
cx: &EarlyContext<'_>,
span: rustc_span::Span,
s: &str,
result: >s::ops::GtsIdParseResult,
) {
let vendors = self.allowed_vendors(cx, span);
for (idx, seg) in result.segments.iter().enumerate() {
if seg.vendor.is_empty()
|| seg.vendor == "*"
|| vendors.iter().any(|v| v == &seg.vendor)
{
continue;
}
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!(
"invalid GTS vendor in segment #{idx}: '{}' (DE0901)",
s
));
diag.note(format!(
"found vendor '{}', allowed vendors: {}",
seg.vendor,
Self::format_vendors(vendors),
));
});
break;
}
}
fn validate_any_gts_id(&self, cx: &EarlyContext<'_>, span: rustc_span::Span, s: &str) {
let s = s.trim();
if s.contains('*') {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!("invalid GTS string (wildcards not allowed): '{}' (DE0901)", s));
diag.note("Wildcards (*) are only allowed in permission strings, not in regular GTS identifiers");
diag.help("Use concrete type names");
});
return;
}
let result = GtsOps::parse_id(s);
if !result.ok {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!("invalid GTS string: '{}' (DE0901)", s));
diag.note(result.error);
});
} else {
self.check_vendors_in_parse_result(cx, span, s, &result);
}
}
fn validate_any_gts_id_allow_wildcards(
&self,
cx: &EarlyContext<'_>,
span: rustc_span::Span,
s: &str,
) {
let s = s.trim();
let result = GtsOps::parse_id(s);
if !result.ok {
cx.span_lint(DE0901_GTS_STRING_PATTERN, span, |diag| {
diag.primary_message(format!("invalid GTS string: '{}' (DE0901)", s));
diag.note(result.error);
});
} else {
self.check_vendors_in_parse_result(cx, span, s, &result);
}
}
}