use github_actions_expressions::context;
use github_actions_models::{
action,
common::{self, expr::LoE},
};
use terminal_link::Link;
use crate::{
InputKey,
finding::location::{Locatable, SymbolicFeature, SymbolicLocation},
models::{
AsDocument, StepBodyCommon, StepCommon,
inputs::{Capability, HasInputs},
workflow::matrix::Matrix,
},
registry::input::CollectionError,
utils::{self, ACTION_VALIDATOR, from_str_with_validation},
};
pub(crate) struct Action {
pub(crate) key: InputKey,
pub(crate) link: Option<String>,
document: yamlpath::Document,
inner: action::Action,
}
impl<'a> AsDocument<'a, 'a> for Action {
fn as_document(&'a self) -> &'a yamlpath::Document {
&self.document
}
}
impl std::ops::Deref for Action {
type Target = action::Action;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::fmt::Debug for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{key}", key = self.key)
}
}
impl HasInputs for Action {
fn get_input(&self, name: &str) -> Option<Capability> {
self.inputs.get(name).map(|_| Capability::Arbitrary)
}
}
impl Action {
pub(crate) fn from_string(contents: String, key: InputKey) -> Result<Self, CollectionError> {
let inner = from_str_with_validation(&contents, &ACTION_VALIDATOR)?;
let document = yamlpath::Document::new(&contents)?;
let link = match key {
InputKey::Local(_) | InputKey::Stdin(_) => None,
InputKey::Remote(_) => {
Some(Link::new(key.presentation_path(), &key.to_string()).to_string())
}
};
Ok(Self {
key,
link,
document,
inner,
})
}
pub(crate) fn steps(&self) -> Option<CompositeSteps<'_>> {
CompositeSteps::new(self)
}
pub(crate) fn location(&self) -> SymbolicLocation<'_> {
SymbolicLocation {
key: &self.key,
annotation: "this action".into(),
link: None,
route: Default::default(),
feature_kind: SymbolicFeature::Normal,
kind: Default::default(),
}
}
pub(crate) fn conditions(&self) -> impl Iterator<Item = (&common::If, SymbolicLocation<'_>)> {
self.steps()
.into_iter()
.flatten()
.filter_map(|step| step.r#if.as_ref().map(|cond| (cond, step.location())))
}
}
pub(crate) struct CompositeSteps<'a> {
inner: std::iter::Enumerate<std::slice::Iter<'a, github_actions_models::action::Step>>,
parent: &'a Action,
}
impl<'a> CompositeSteps<'a> {
fn new(action: &'a Action) -> Option<Self> {
match &action.inner.runs {
action::Runs::Composite(composite) => Some(Self {
inner: composite.steps.iter().enumerate(),
parent: action,
}),
_ => None,
}
}
}
impl<'a> Iterator for CompositeSteps<'a> {
type Item = CompositeStep<'a>;
fn next(&mut self) -> Option<Self::Item> {
let item = self.inner.next();
match item {
Some((idx, step)) => Some(CompositeStep::new(idx, step, self.parent)),
None => None,
}
}
}
pub(crate) struct CompositeStep<'a> {
pub(crate) index: usize,
pub(crate) inner: &'a action::Step,
pub(crate) parent: &'a Action,
}
impl<'a> std::ops::Deref for CompositeStep<'a> {
type Target = &'a action::Step;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<'doc> Locatable<'doc> for CompositeStep<'doc> {
fn location(&self) -> SymbolicLocation<'doc> {
self.parent.location().annotated("this step").with_keys([
"runs".into(),
"steps".into(),
self.index.into(),
])
}
fn location_with_grip(&self) -> SymbolicLocation<'doc> {
if self.inner.name.is_some() {
self.location().with_keys(["name".into()])
} else if self.inner.id.is_some() {
self.location().with_keys(["id".into()])
} else {
self.location()
}
}
}
impl HasInputs for CompositeStep<'_> {
fn get_input(&self, name: &str) -> Option<Capability> {
self.parent.get_input(name)
}
}
impl<'doc> StepCommon<'doc> for CompositeStep<'doc> {
fn index(&self) -> usize {
self.index
}
fn env_is_static(&self, ctx: &context::Context) -> bool {
utils::env_is_static(ctx, &[&self.env])
}
fn uses(&self) -> Option<&'doc common::Uses> {
let action::StepBody::Uses { uses, .. } = &self.inner.body else {
return None;
};
Some(uses)
}
fn matrix(&self) -> Option<Matrix<'doc>> {
None
}
fn body(&self) -> StepBodyCommon<'doc> {
match &self.body {
action::StepBody::Uses { uses, with } => StepBodyCommon::Uses { uses, with },
action::StepBody::Run {
run,
working_directory,
shell,
} => StepBodyCommon::Run {
run,
_working_directory: working_directory.as_deref(),
_shell: Some(shell),
},
}
}
fn document(&self) -> &'doc yamlpath::Document {
self.action().as_document()
}
fn shell(&self) -> Option<(&str, SymbolicLocation<'doc>)> {
if let action::StepBody::Run {
shell: LoE::Literal(shell),
..
} = &self.inner.body
{
Some((
shell,
self.location()
.with_keys(["shell".into()])
.annotated("shell defined here"),
))
} else {
None
}
}
}
impl<'a> CompositeStep<'a> {
pub(crate) fn new(index: usize, inner: &'a action::Step, parent: &'a Action) -> Self {
Self {
index,
inner,
parent,
}
}
pub(crate) fn action(&self) -> &'a Action {
self.parent
}
}