use crate::SourceLocation;
use perl_semantic_facts::{
AnchorId, Confidence, ExportSet, ExportTag, FileId, ImportKind, ImportSpec, ImportSymbols,
Provenance, ScopeId, VisibleSymbol, VisibleSymbolContext, VisibleSymbolSource,
};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct HirId {
index: u32,
}
impl HirId {
#[inline]
pub const fn from_index(index: u32) -> Self {
Self { index }
}
#[inline]
pub const fn index(self) -> u32 {
self.index
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct HirScopeId {
index: u32,
}
impl HirScopeId {
#[inline]
pub const fn from_index(index: u32) -> Self {
Self { index }
}
#[inline]
pub const fn index(self) -> u32 {
self.index
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct HirBindingId {
index: u32,
}
impl HirBindingId {
#[inline]
pub const fn from_index(index: u32) -> Self {
Self { index }
}
#[inline]
pub const fn index(self) -> u32 {
self.index
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AstAnchor {
pub node_kind: &'static str,
pub range: SourceLocation,
pub name_range: Option<SourceLocation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RecoveryConfidence {
Parsed,
Recovered,
Partial,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct HirFile {
pub items: Vec<HirItem>,
pub scope_graph: ScopeGraph,
pub stash_graph: StashGraph,
pub compile_environment: CompileEnvironment,
}
impl HirFile {
#[inline]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
#[must_use]
pub fn compile_effects(&self) -> Vec<CompileEffect> {
self.compile_effects_with_source_hash(None)
}
#[must_use]
pub fn compile_effects_with_source_hash(
&self,
source_hash: Option<String>,
) -> Vec<CompileEffect> {
compile_effects_from_file(self, source_hash)
}
#[must_use]
pub fn framework_facts(&self) -> FrameworkFactGraph {
FrameworkAdapterRegistry::default().project_file(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HirItem {
pub id: HirId,
pub kind: HirKind,
pub range: SourceLocation,
pub anchor: AstAnchor,
pub recovery_confidence: RecoveryConfidence,
pub package_context: Option<String>,
pub scope_context: Option<HirScopeId>,
}
pub const COMPILE_EFFECT_MODEL_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompileEffect {
pub ordinal: u32,
pub kind: CompileEffectKind,
pub source_kind: CompileEffectSourceKind,
pub fact_kind: CompileEffectFactKind,
pub fact_name: Option<String>,
pub range: SourceLocation,
pub source_item: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub fact_anchor_id: Option<AnchorId>,
pub dynamic_reason: Option<String>,
pub source_hash: Option<String>,
pub model_version: u32,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileEffectKind {
DeclarePackage,
DeclareSub,
DeclareMethod,
DeclareBinding,
SetPragmaState,
AddIncludePath,
RemoveIncludePath,
RequestModule,
ImportSymbols,
AssignInheritance,
AssignGlobAlias,
DefineConstant,
RegisterPrototype,
EmitDynamicBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileEffectSourceKind {
PackageDecl,
SubDecl,
MethodDecl,
VariableDecl,
UseDirective,
NoDirective,
RequireDirective,
PhaseBlock,
SymbolicReferenceDeref,
Assignment,
TypeglobAssignment,
ScopeGraph,
StashGraph,
CompileEnvironment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileEffectFactKind {
Package,
Sub,
Method,
Binding,
PragmaState,
IncludeRoot,
ModuleRequest,
ImportSpec,
InheritanceEdge,
GlobSlot,
Constant,
Prototype,
DynamicBoundary,
}
#[derive(Debug)]
struct CompileEffectEntry {
source_order: u32,
effect: CompileEffect,
}
fn compile_effects_from_file(file: &HirFile, source_hash: Option<String>) -> Vec<CompileEffect> {
let mut entries = Vec::new();
let mut next_order = 0;
for item in &file.items {
push_item_effects(item, &source_hash, &mut entries, &mut next_order);
}
for binding in &file.scope_graph.bindings {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::DeclareBinding,
source_kind: CompileEffectSourceKind::ScopeGraph,
fact_kind: CompileEffectFactKind::Binding,
fact_name: Some(format!("{}{}", binding.sigil, binding.name)),
range: binding.range,
source_item: binding.declaration_item,
scope_id: Some(binding.scope_id),
package_context: binding.package_context.clone(),
fact_anchor_id: Some(AnchorId(binding.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: CompileProvenance::ExactAst,
confidence: CompileConfidence::High,
},
);
}
for fact in &file.compile_environment.pragma_state_facts {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::SetPragmaState,
source_kind: CompileEffectSourceKind::CompileEnvironment,
fact_kind: CompileEffectFactKind::PragmaState,
fact_name: Some("strict/warnings/feature".to_string()),
range: fact.range,
source_item: fact.directive_item,
scope_id: fact.scope_id,
package_context: fact.package_context.clone(),
fact_anchor_id: Some(fact.anchor_id),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: fact.provenance,
confidence: fact.confidence,
},
);
}
for root in &file.compile_environment.inc_roots {
let kind = match root.action {
IncRootAction::Add => CompileEffectKind::AddIncludePath,
IncRootAction::Remove => CompileEffectKind::RemoveIncludePath,
};
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind,
source_kind: match root.action {
IncRootAction::Add => CompileEffectSourceKind::UseDirective,
IncRootAction::Remove => CompileEffectSourceKind::NoDirective,
},
fact_kind: CompileEffectFactKind::IncludeRoot,
fact_name: Some(root.path.clone()),
range: root.range,
source_item: root.directive_item,
scope_id: root.scope_id,
package_context: root.package_context.clone(),
fact_anchor_id: Some(AnchorId(root.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: root.provenance,
confidence: root.confidence,
},
);
}
for request in &file.compile_environment.module_requests {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::RequestModule,
source_kind: module_request_source_kind(request.kind),
fact_kind: CompileEffectFactKind::ModuleRequest,
fact_name: request.target.clone().or_else(|| Some("<dynamic>".to_string())),
range: request.range,
source_item: request.directive_item,
scope_id: request.scope_id,
package_context: request.package_context.clone(),
fact_anchor_id: Some(AnchorId(request.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: request.provenance,
confidence: request.confidence,
},
);
}
for spec in file.compile_environment.import_specs(FileId(0)) {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::ImportSymbols,
source_kind: import_spec_source_kind(&spec),
fact_kind: CompileEffectFactKind::ImportSpec,
fact_name: Some(spec.module.clone()),
range: SourceLocation::new(
spec.span_start_byte.unwrap_or_default() as usize,
spec.span_start_byte.unwrap_or_default() as usize,
),
source_item: None,
scope_id: spec.scope_id.map(|scope| HirScopeId::from_index(scope.0 as u32)),
package_context: None,
fact_anchor_id: spec.anchor_id,
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: fact_provenance_to_compile(spec.provenance),
confidence: fact_confidence_to_compile(spec.confidence),
},
);
}
for edge in &file.stash_graph.inheritance_edges {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::AssignInheritance,
source_kind: CompileEffectSourceKind::StashGraph,
fact_kind: CompileEffectFactKind::InheritanceEdge,
fact_name: Some(format!("{}->{}", edge.from_package, edge.to_package)),
range: edge.range,
source_item: edge.declaration_item,
scope_id: None,
package_context: Some(edge.from_package.clone()),
fact_anchor_id: Some(AnchorId(edge.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: stash_provenance_to_compile(edge.provenance),
confidence: stash_confidence_to_compile(edge.confidence),
},
);
}
for package in &file.stash_graph.packages {
for slot in &package.slots {
push_slot_effects(package, slot, &source_hash, &mut entries, &mut next_order);
}
}
for boundary in &file.compile_environment.dynamic_boundaries {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::EmitDynamicBoundary,
source_kind: compile_boundary_source_kind(boundary.kind),
fact_kind: CompileEffectFactKind::DynamicBoundary,
fact_name: Some(format!("{:?}", boundary.kind)),
range: boundary.range,
source_item: boundary.boundary_item,
scope_id: boundary.scope_id,
package_context: boundary.package_context.clone(),
fact_anchor_id: Some(AnchorId(boundary.range.start as u64)),
dynamic_reason: Some(boundary.reason.clone()),
source_hash: source_hash.clone(),
provenance: boundary.provenance,
confidence: boundary.confidence,
},
);
}
for boundary in &file.stash_graph.dynamic_boundaries {
push_compile_effect(
&mut entries,
&mut next_order,
CompileEffectSeed {
kind: CompileEffectKind::EmitDynamicBoundary,
source_kind: CompileEffectSourceKind::StashGraph,
fact_kind: CompileEffectFactKind::DynamicBoundary,
fact_name: boundary.symbol.clone(),
range: boundary.range,
source_item: boundary.boundary_item,
scope_id: None,
package_context: boundary.package.clone(),
fact_anchor_id: Some(AnchorId(boundary.range.start as u64)),
dynamic_reason: Some(boundary.reason.clone()),
source_hash: source_hash.clone(),
provenance: stash_provenance_to_compile(boundary.provenance),
confidence: stash_confidence_to_compile(boundary.confidence),
},
);
}
entries.sort_by_key(|entry| (entry.effect.range.start, entry.source_order));
entries
.into_iter()
.enumerate()
.map(|(ordinal, mut entry)| {
entry.effect.ordinal = ordinal as u32;
entry.effect
})
.collect()
}
fn push_item_effects(
item: &HirItem,
source_hash: &Option<String>,
entries: &mut Vec<CompileEffectEntry>,
next_order: &mut u32,
) {
match &item.kind {
HirKind::PackageDecl(decl) => {
push_compile_effect(
entries,
next_order,
CompileEffectSeed {
kind: CompileEffectKind::DeclarePackage,
source_kind: CompileEffectSourceKind::PackageDecl,
fact_kind: CompileEffectFactKind::Package,
fact_name: Some(decl.name.clone()),
range: item.range,
source_item: Some(item.id),
scope_id: item.scope_context,
package_context: Some(decl.name.clone()),
fact_anchor_id: Some(AnchorId(item.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: CompileProvenance::ExactAst,
confidence: CompileConfidence::High,
},
);
}
HirKind::SubDecl(decl) => {
let Some(name) = &decl.name else {
return;
};
push_compile_effect(
entries,
next_order,
CompileEffectSeed {
kind: CompileEffectKind::DeclareSub,
source_kind: CompileEffectSourceKind::SubDecl,
fact_kind: CompileEffectFactKind::Sub,
fact_name: Some(name.clone()),
range: item.range,
source_item: Some(item.id),
scope_id: item.scope_context,
package_context: item.package_context.clone(),
fact_anchor_id: Some(AnchorId(item.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: CompileProvenance::ExactAst,
confidence: CompileConfidence::High,
},
);
if decl.has_prototype {
push_compile_effect(
entries,
next_order,
CompileEffectSeed {
kind: CompileEffectKind::RegisterPrototype,
source_kind: CompileEffectSourceKind::SubDecl,
fact_kind: CompileEffectFactKind::Prototype,
fact_name: Some(name.clone()),
range: item.range,
source_item: Some(item.id),
scope_id: item.scope_context,
package_context: item.package_context.clone(),
fact_anchor_id: Some(AnchorId(item.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: CompileProvenance::ExactAst,
confidence: CompileConfidence::High,
},
);
}
}
HirKind::MethodDecl(decl) => {
push_compile_effect(
entries,
next_order,
CompileEffectSeed {
kind: CompileEffectKind::DeclareMethod,
source_kind: CompileEffectSourceKind::MethodDecl,
fact_kind: CompileEffectFactKind::Method,
fact_name: Some(decl.name.clone()),
range: item.range,
source_item: Some(item.id),
scope_id: item.scope_context,
package_context: item.package_context.clone(),
fact_anchor_id: Some(AnchorId(item.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: CompileProvenance::ExactAst,
confidence: CompileConfidence::High,
},
);
}
_ => {}
}
}
fn push_slot_effects(
package: &PackageStash,
slot: &GlobSlot,
source_hash: &Option<String>,
entries: &mut Vec<CompileEffectEntry>,
next_order: &mut u32,
) {
let (kind, fact_kind) = match slot.source {
GlobSlotSource::TypeglobAlias => {
(CompileEffectKind::AssignGlobAlias, CompileEffectFactKind::GlobSlot)
}
GlobSlotSource::ConstantDeclaration => {
(CompileEffectKind::DefineConstant, CompileEffectFactKind::Constant)
}
_ => return,
};
push_compile_effect(
entries,
next_order,
CompileEffectSeed {
kind,
source_kind: match slot.source {
GlobSlotSource::TypeglobAlias => CompileEffectSourceKind::TypeglobAssignment,
_ => CompileEffectSourceKind::StashGraph,
},
fact_kind,
fact_name: Some(format!("{}::{}", package.package, slot.name)),
range: slot.range,
source_item: slot.declaration_item,
scope_id: None,
package_context: Some(package.package.clone()),
fact_anchor_id: Some(AnchorId(slot.range.start as u64)),
dynamic_reason: None,
source_hash: source_hash.clone(),
provenance: stash_provenance_to_compile(slot.provenance),
confidence: stash_confidence_to_compile(slot.confidence),
},
);
}
#[derive(Debug)]
struct CompileEffectSeed {
kind: CompileEffectKind,
source_kind: CompileEffectSourceKind,
fact_kind: CompileEffectFactKind,
fact_name: Option<String>,
range: SourceLocation,
source_item: Option<HirId>,
scope_id: Option<HirScopeId>,
package_context: Option<String>,
fact_anchor_id: Option<AnchorId>,
dynamic_reason: Option<String>,
source_hash: Option<String>,
provenance: CompileProvenance,
confidence: CompileConfidence,
}
fn push_compile_effect(
entries: &mut Vec<CompileEffectEntry>,
next_order: &mut u32,
seed: CompileEffectSeed,
) {
let order = *next_order;
*next_order += 1;
entries.push(CompileEffectEntry {
source_order: order,
effect: CompileEffect {
ordinal: 0,
kind: seed.kind,
source_kind: seed.source_kind,
fact_kind: seed.fact_kind,
fact_name: seed.fact_name,
range: seed.range,
source_item: seed.source_item,
scope_id: seed.scope_id,
package_context: seed.package_context,
fact_anchor_id: seed.fact_anchor_id,
dynamic_reason: seed.dynamic_reason,
source_hash: seed.source_hash,
model_version: COMPILE_EFFECT_MODEL_VERSION,
provenance: seed.provenance,
confidence: seed.confidence,
},
});
}
fn module_request_source_kind(kind: ModuleRequestKind) -> CompileEffectSourceKind {
match kind {
ModuleRequestKind::Require => CompileEffectSourceKind::RequireDirective,
_ => CompileEffectSourceKind::UseDirective,
}
}
fn import_spec_source_kind(spec: &ImportSpec) -> CompileEffectSourceKind {
match spec.kind {
ImportKind::Require | ImportKind::DynamicRequire => {
CompileEffectSourceKind::RequireDirective
}
_ => CompileEffectSourceKind::UseDirective,
}
}
fn compile_boundary_source_kind(kind: CompileEnvironmentBoundaryKind) -> CompileEffectSourceKind {
match kind {
CompileEnvironmentBoundaryKind::DynamicRequire => CompileEffectSourceKind::RequireDirective,
CompileEnvironmentBoundaryKind::DynamicPragmaArgs
| CompileEnvironmentBoundaryKind::DynamicIncRoot => CompileEffectSourceKind::UseDirective,
CompileEnvironmentBoundaryKind::PhaseBlockExecution => CompileEffectSourceKind::PhaseBlock,
CompileEnvironmentBoundaryKind::SymbolicReferenceDeref => {
CompileEffectSourceKind::SymbolicReferenceDeref
}
}
}
fn stash_provenance_to_compile(provenance: StashProvenance) -> CompileProvenance {
match provenance {
StashProvenance::ExactAst => CompileProvenance::ExactAst,
StashProvenance::DesugaredAst => CompileProvenance::DesugaredAst,
StashProvenance::DynamicBoundary => CompileProvenance::DynamicBoundary,
}
}
fn stash_confidence_to_compile(confidence: StashConfidence) -> CompileConfidence {
match confidence {
StashConfidence::High => CompileConfidence::High,
StashConfidence::Medium => CompileConfidence::Medium,
StashConfidence::Low => CompileConfidence::Low,
}
}
fn fact_provenance_to_compile(provenance: Provenance) -> CompileProvenance {
match provenance {
Provenance::ExactAst | Provenance::LiteralRequireImport => CompileProvenance::ExactAst,
Provenance::DesugaredAst
| Provenance::SemanticAnalyzer
| Provenance::FrameworkSynthesis
| Provenance::ImportExportInference
| Provenance::PragmaInference => CompileProvenance::DesugaredAst,
Provenance::NameHeuristic | Provenance::SearchFallback | Provenance::DynamicBoundary => {
CompileProvenance::DynamicBoundary
}
}
}
fn fact_confidence_to_compile(confidence: Confidence) -> CompileConfidence {
match confidence {
Confidence::High => CompileConfidence::High,
Confidence::Medium => CompileConfidence::Medium,
Confidence::Low => CompileConfidence::Low,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct ScopeGraph {
pub scopes: Vec<ScopeFrame>,
pub bindings: Vec<Binding>,
pub references: Vec<BindingReference>,
}
impl ScopeGraph {
#[inline]
pub fn root_scope(&self) -> Option<&ScopeFrame> {
self.scopes.first()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScopeFrame {
pub id: HirScopeId,
pub parent: Option<HirScopeId>,
pub kind: ScopeKind,
pub range: SourceLocation,
pub package_context: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ScopeKind {
File,
Package,
Block,
Subroutine,
Method,
Signature,
Format,
EvalString,
PhaseBlock,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Binding {
pub id: HirBindingId,
pub scope_id: HirScopeId,
pub sigil: String,
pub name: String,
pub range: SourceLocation,
pub storage: StorageClass,
pub package_context: Option<String>,
pub declaration_item: Option<HirId>,
pub shadows: Option<HirBindingId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StorageClass {
LexicalMy,
LexicalState,
PackageOur,
LocalizedPackage,
Parameter,
MethodInvocant,
Implicit,
PackageGlobal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BindingReference {
pub scope_id: HirScopeId,
pub sigil: String,
pub name: String,
pub range: SourceLocation,
pub resolved_binding: Option<HirBindingId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct StashGraph {
pub packages: Vec<PackageStash>,
pub inheritance_edges: Vec<PackageInheritanceEdge>,
pub export_declarations: Vec<ExportDeclaration>,
pub dynamic_boundaries: Vec<StashDynamicBoundary>,
}
impl StashGraph {
#[must_use]
pub fn export_sets(&self) -> Vec<ExportSet> {
let mut builders = BTreeMap::<String, ExportSetBuilder>::new();
for declaration in &self.export_declarations {
let builder = builders.entry(declaration.package.clone()).or_insert_with(|| {
ExportSetBuilder::new(
declaration.package.clone(),
declaration.range,
stash_provenance_to_fact(declaration.provenance),
stash_confidence_to_fact(declaration.confidence),
)
});
builder.absorb(declaration);
}
builders.into_values().map(ExportSetBuilder::into_export_set).collect()
}
}
#[derive(Debug)]
struct ExportSetBuilder {
module_name: String,
anchor_range: SourceLocation,
default_exports: Vec<String>,
optional_exports: Vec<String>,
tags: BTreeMap<String, Vec<String>>,
provenance: Provenance,
confidence: Confidence,
}
impl ExportSetBuilder {
fn new(
module_name: String,
anchor_range: SourceLocation,
provenance: Provenance,
confidence: Confidence,
) -> Self {
Self {
module_name,
anchor_range,
default_exports: Vec::new(),
optional_exports: Vec::new(),
tags: BTreeMap::new(),
provenance,
confidence,
}
}
fn absorb(&mut self, declaration: &ExportDeclaration) {
if declaration.range.start < self.anchor_range.start {
self.anchor_range = declaration.range;
}
self.provenance =
combine_provenance(self.provenance, stash_provenance_to_fact(declaration.provenance));
self.confidence =
combine_confidence(self.confidence, stash_confidence_to_fact(declaration.confidence));
match declaration.kind {
ExportDeclarationKind::Default => {
self.default_exports.extend(declaration.symbols.iter().cloned());
}
ExportDeclarationKind::Optional => {
self.optional_exports.extend(declaration.symbols.iter().cloned());
}
ExportDeclarationKind::Tag => {
if let Some(tag_name) = &declaration.tag_name {
self.tags
.entry(tag_name.clone())
.or_default()
.extend(declaration.symbols.iter().cloned());
}
}
}
}
fn into_export_set(mut self) -> ExportSet {
sort_dedup(&mut self.default_exports);
sort_dedup(&mut self.optional_exports);
let tags = self
.tags
.into_iter()
.map(|(name, mut members)| {
sort_dedup(&mut members);
ExportTag { name, members }
})
.collect();
ExportSet {
default_exports: self.default_exports,
optional_exports: self.optional_exports,
tags,
provenance: self.provenance,
confidence: self.confidence,
module_name: Some(self.module_name),
anchor_id: Some(AnchorId(self.anchor_range.start as u64)),
}
}
}
fn sort_dedup(values: &mut Vec<String>) {
values.sort();
values.dedup();
}
fn combine_provenance(current: Provenance, next: Provenance) -> Provenance {
if current == Provenance::DynamicBoundary || next == Provenance::DynamicBoundary {
Provenance::DynamicBoundary
} else if current == Provenance::ImportExportInference
|| next == Provenance::ImportExportInference
{
Provenance::ImportExportInference
} else {
current
}
}
fn combine_confidence(current: Confidence, next: Confidence) -> Confidence {
match (current, next) {
(Confidence::Low, _) | (_, Confidence::Low) => Confidence::Low,
(Confidence::Medium, _) | (_, Confidence::Medium) => Confidence::Medium,
(Confidence::High, Confidence::High) => Confidence::High,
}
}
fn stash_provenance_to_fact(provenance: StashProvenance) -> Provenance {
match provenance {
StashProvenance::ExactAst => Provenance::ExactAst,
StashProvenance::DesugaredAst => Provenance::DesugaredAst,
StashProvenance::DynamicBoundary => Provenance::DynamicBoundary,
}
}
fn stash_confidence_to_fact(confidence: StashConfidence) -> Confidence {
match confidence {
StashConfidence::High => Confidence::High,
StashConfidence::Medium => Confidence::Medium,
StashConfidence::Low => Confidence::Low,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PackageStash {
pub package: String,
pub range: SourceLocation,
pub declaration_item: Option<HirId>,
pub slots: Vec<GlobSlot>,
pub provenance: StashProvenance,
pub confidence: StashConfidence,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct GlobSlot {
pub name: String,
pub kind: GlobSlotKind,
pub range: SourceLocation,
pub declaration_item: Option<HirId>,
pub source: GlobSlotSource,
pub alias_target: Option<String>,
pub provenance: StashProvenance,
pub confidence: StashConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GlobSlotKind {
Scalar,
Array,
Hash,
Code,
Io,
Format,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GlobSlotSource {
PackageDeclaration,
SubDeclaration,
MethodDeclaration,
OurDeclaration,
FormatDeclaration,
ConstantDeclaration,
PackageAssignment,
TypeglobAlias,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StashProvenance {
ExactAst,
DesugaredAst,
DynamicBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StashConfidence {
High,
Medium,
Low,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PackageInheritanceEdge {
pub from_package: String,
pub to_package: String,
pub range: SourceLocation,
pub declaration_item: Option<HirId>,
pub source: InheritanceSource,
pub provenance: StashProvenance,
pub confidence: StashConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum InheritanceSource {
IsaAssignment,
UseParent,
UseBase,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ExportDeclaration {
pub package: String,
pub kind: ExportDeclarationKind,
pub tag_name: Option<String>,
pub symbols: Vec<String>,
pub range: SourceLocation,
pub declaration_item: Option<HirId>,
pub provenance: StashProvenance,
pub confidence: StashConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExportDeclarationKind {
Default,
Optional,
Tag,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct StashDynamicBoundary {
pub package: Option<String>,
pub symbol: Option<String>,
pub range: SourceLocation,
pub boundary_item: Option<HirId>,
pub kind: StashDynamicBoundaryKind,
pub reason: String,
pub provenance: StashProvenance,
pub confidence: StashConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StashDynamicBoundaryKind {
DynamicStashMutation,
DynamicExportDeclaration,
Autoload,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct CompileEnvironment {
pub directives: Vec<CompileDirective>,
pub pragma_effects: Vec<PragmaEffect>,
pub pragma_state_facts: Vec<PragmaStateFact>,
pub inc_roots: Vec<IncRootFact>,
pub module_requests: Vec<ModuleRequest>,
pub phase_blocks: Vec<CompilePhaseBlock>,
pub dynamic_boundaries: Vec<CompileEnvironmentBoundary>,
}
impl CompileEnvironment {
#[must_use]
pub fn pragma_state_facts(&self) -> &[PragmaStateFact] {
&self.pragma_state_facts
}
#[must_use]
pub fn pragma_state_at(&self, offset: usize) -> Option<&PragmaStateFact> {
let idx = self.pragma_state_facts.partition_point(|fact| fact.range.start <= offset);
if idx > 0 { self.pragma_state_facts.get(idx - 1) } else { None }
}
#[must_use]
pub fn import_specs(&self, file_id: FileId) -> Vec<ImportSpec> {
self.directives
.iter()
.filter_map(|directive| import_spec_from_directive(directive, file_id))
.collect()
}
#[must_use]
pub fn module_resolution_candidates(
&self,
supplied_roots: &[ModuleResolutionRoot],
) -> Vec<ModuleResolutionCandidate> {
self.module_requests
.iter()
.enumerate()
.filter_map(|(request_index, request)| {
let target = request.target.as_ref()?;
let normalized_target = normalize_module_target(target);
let relative_path = module_target_to_relative_path(&normalized_target)?;
let candidate_roots =
self.candidate_roots_for_request(request, &relative_path, supplied_roots);
let status = if candidate_roots.is_empty() {
ModuleResolutionCandidateStatus::NotFound
} else {
ModuleResolutionCandidateStatus::CandidateBuilt
};
Some(ModuleResolutionCandidate {
request_index,
directive_item: request.directive_item,
request_kind: request.kind,
target: normalized_target,
relative_path,
roots: candidate_roots,
status,
resolved_path: None,
range: request.range,
package_context: request.package_context.clone(),
provenance: request.provenance,
confidence: request.confidence,
})
})
.collect()
}
#[must_use]
pub fn resolved_module_resolution_candidates(
&self,
supplied_roots: &[ModuleResolutionRoot],
mut path_exists: impl FnMut(&str) -> bool,
) -> Vec<ModuleResolutionCandidate> {
let mut candidates = self.module_resolution_candidates(supplied_roots);
for candidate in &mut candidates {
if candidate.status != ModuleResolutionCandidateStatus::CandidateBuilt {
continue;
}
if let Some(root) =
candidate.roots.iter().find(|root| path_exists(&root.candidate_path))
{
candidate.status = ModuleResolutionCandidateStatus::Resolved;
candidate.resolved_path = Some(root.candidate_path.clone());
} else {
candidate.status = ModuleResolutionCandidateStatus::NotFound;
}
}
candidates
}
fn candidate_roots_for_request(
&self,
request: &ModuleRequest,
relative_path: &str,
supplied_roots: &[ModuleResolutionRoot],
) -> Vec<ModuleResolutionCandidateRoot> {
let active_lexical_roots = self.active_lexical_roots_for_request(request);
active_lexical_roots
.iter()
.map(|root| ModuleResolutionRoot {
path: root.path.clone(),
kind: root.kind,
source: root.source.clone(),
})
.chain(supplied_roots.iter().cloned())
.enumerate()
.map(|(precedence, root)| ModuleResolutionCandidateRoot {
path: root.path.clone(),
kind: root.kind,
source: root.source,
candidate_path: join_candidate_path(&root.path, relative_path),
precedence,
})
.collect()
}
fn active_lexical_roots_for_request(&self, request: &ModuleRequest) -> Vec<ActiveLexicalRoot> {
let mut active = Vec::new();
for (order, root) in self.inc_roots.iter().enumerate() {
if root.range.start > request.range.start {
continue;
}
if root.kind != IncRootKind::UseLib {
continue;
}
match root.action {
IncRootAction::Add => {
active.push(ActiveLexicalRoot {
path: root.path.clone(),
kind: root.kind,
source: "use-lib-lexical".to_string(),
range_start: root.range.start,
order,
});
}
IncRootAction::Remove => {
active.retain(|active_root| active_root.path != root.path);
}
}
}
active.sort_by(|left, right| {
right.range_start.cmp(&left.range_start).then_with(|| left.order.cmp(&right.order))
});
active
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ActiveLexicalRoot {
path: String,
kind: IncRootKind,
source: String,
range_start: usize,
order: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompileDirective {
pub action: CompileDirectiveAction,
pub module: Option<String>,
pub args: Vec<String>,
pub range: SourceLocation,
pub item_id: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub kind: CompileDirectiveKind,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileDirectiveAction {
Use,
No,
Require,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileDirectiveKind {
Strict,
Warnings,
Feature,
Lib,
Inheritance,
Constant,
Module,
Dynamic,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PragmaEffect {
pub pragma: String,
pub enabled: bool,
pub args: Vec<String>,
pub argument_kind: PragmaArgumentKind,
pub range: SourceLocation,
pub directive_item: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PragmaArgumentKind {
Broad,
Categories,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PragmaStateFact {
pub range: SourceLocation,
pub anchor_id: AnchorId,
pub directive_item: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub strict_vars: bool,
pub strict_subs: bool,
pub strict_refs: bool,
pub warnings: bool,
pub disabled_warning_categories: Vec<String>,
pub features: Vec<String>,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
impl PragmaStateFact {
#[must_use]
pub fn strict_enabled(&self) -> bool {
self.strict_vars && self.strict_subs && self.strict_refs
}
#[must_use]
pub fn warning_active(&self, category: &str) -> bool {
self.warnings && !self.disabled_warning_categories.iter().any(|name| name == category)
}
#[must_use]
pub fn has_feature(&self, feature: &str) -> bool {
self.features.iter().any(|name| name == feature)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FrameworkAdapterRegistry {
adapters: Vec<FrameworkAdapterKind>,
}
impl Default for FrameworkAdapterRegistry {
fn default() -> Self {
Self { adapters: vec![FrameworkAdapterKind::ExporterFamily] }
}
}
impl FrameworkAdapterRegistry {
#[must_use]
pub fn new(adapters: Vec<FrameworkAdapterKind>) -> Self {
Self { adapters }
}
#[must_use]
pub fn adapters(&self) -> &[FrameworkAdapterKind] {
&self.adapters
}
#[must_use]
pub fn project_file(&self, file: &HirFile) -> FrameworkFactGraph {
let mut graph = FrameworkFactGraph::default();
for adapter in &self.adapters {
match adapter {
FrameworkAdapterKind::ExporterFamily => {
project_exporter_family_facts(file, &mut graph);
}
}
}
graph
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FrameworkAdapterKind {
ExporterFamily,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct FrameworkFactGraph {
pub exported_symbols: Vec<FrameworkExportedSymbolFact>,
pub dynamic_boundaries: Vec<FrameworkDynamicBoundaryFact>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FrameworkExportedSymbolFact {
pub adapter: FrameworkAdapterKind,
pub package: String,
pub name: String,
pub kind: FrameworkExportedSymbolKind,
pub tag_name: Option<String>,
pub range: SourceLocation,
pub declaration_item: Option<HirId>,
pub visible_symbol: VisibleSymbol,
pub source_provenance: Provenance,
pub source_confidence: Confidence,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FrameworkExportedSymbolKind {
Default,
Optional,
TagMember,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FrameworkDynamicBoundaryFact {
pub adapter: FrameworkAdapterKind,
pub package: Option<String>,
pub symbol: Option<String>,
pub range: SourceLocation,
pub boundary_item: Option<HirId>,
pub kind: StashDynamicBoundaryKind,
pub reason: String,
pub provenance: Provenance,
pub confidence: Confidence,
}
fn project_exporter_family_facts(file: &HirFile, graph: &mut FrameworkFactGraph) {
for declaration in &file.stash_graph.export_declarations {
match declaration.kind {
ExportDeclarationKind::Default => {
project_exported_symbols_from_declaration(
declaration,
FrameworkExportedSymbolKind::Default,
None,
graph,
);
}
ExportDeclarationKind::Optional => {
project_exported_symbols_from_declaration(
declaration,
FrameworkExportedSymbolKind::Optional,
None,
graph,
);
}
ExportDeclarationKind::Tag => {
project_exported_symbols_from_declaration(
declaration,
FrameworkExportedSymbolKind::TagMember,
declaration.tag_name.as_deref(),
graph,
);
}
}
}
for boundary in &file.stash_graph.dynamic_boundaries {
if boundary.kind != StashDynamicBoundaryKind::DynamicExportDeclaration {
continue;
}
graph.dynamic_boundaries.push(FrameworkDynamicBoundaryFact {
adapter: FrameworkAdapterKind::ExporterFamily,
package: boundary.package.clone(),
symbol: boundary.symbol.clone(),
range: boundary.range,
boundary_item: boundary.boundary_item,
kind: boundary.kind,
reason: boundary.reason.clone(),
provenance: Provenance::DynamicBoundary,
confidence: Confidence::Low,
});
}
}
fn project_exported_symbols_from_declaration(
declaration: &ExportDeclaration,
kind: FrameworkExportedSymbolKind,
tag_name: Option<&str>,
graph: &mut FrameworkFactGraph,
) {
for symbol in &declaration.symbols {
graph.exported_symbols.push(FrameworkExportedSymbolFact {
adapter: FrameworkAdapterKind::ExporterFamily,
package: declaration.package.clone(),
name: symbol.clone(),
kind,
tag_name: tag_name.map(str::to_string),
range: declaration.range,
declaration_item: declaration.declaration_item,
visible_symbol: visible_symbol_for_export(declaration, symbol, kind),
source_provenance: stash_provenance_to_fact(declaration.provenance),
source_confidence: stash_confidence_to_fact(declaration.confidence),
provenance: Provenance::FrameworkSynthesis,
confidence: Confidence::Medium,
});
}
}
fn visible_symbol_for_export(
declaration: &ExportDeclaration,
symbol: &str,
kind: FrameworkExportedSymbolKind,
) -> VisibleSymbol {
let source = match kind {
FrameworkExportedSymbolKind::Default => VisibleSymbolSource::DefaultExport,
FrameworkExportedSymbolKind::Optional => VisibleSymbolSource::ExplicitImport,
FrameworkExportedSymbolKind::TagMember => VisibleSymbolSource::ExportTag,
};
VisibleSymbol {
name: symbol.to_string(),
entity_id: None,
source,
confidence: Confidence::Medium,
context: Some(VisibleSymbolContext::new(
Some(declaration.package.clone()),
None,
Some(AnchorId(declaration.range.start as u64)),
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct IncRootFact {
pub path: String,
pub action: IncRootAction,
pub kind: IncRootKind,
pub range: SourceLocation,
pub directive_item: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IncRootAction {
Add,
Remove,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IncRootKind {
UseLib,
Configured,
Perl5Lib,
SystemInc,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModuleResolutionRoot {
pub path: String,
pub kind: IncRootKind,
pub source: String,
}
impl ModuleResolutionRoot {
#[must_use]
pub fn new(path: impl Into<String>, kind: IncRootKind, source: impl Into<String>) -> Self {
Self { path: path.into(), kind, source: source.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModuleRequest {
pub target: Option<String>,
pub kind: ModuleRequestKind,
pub range: SourceLocation,
pub directive_item: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub resolution: ModuleResolutionStatus,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ModuleRequestKind {
Use,
Require,
Parent,
Base,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ModuleResolutionStatus {
Deferred,
Dynamic,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModuleResolutionCandidate {
pub request_index: usize,
pub directive_item: Option<HirId>,
pub request_kind: ModuleRequestKind,
pub target: String,
pub relative_path: String,
pub roots: Vec<ModuleResolutionCandidateRoot>,
pub status: ModuleResolutionCandidateStatus,
pub resolved_path: Option<String>,
pub range: SourceLocation,
pub package_context: Option<String>,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
impl ModuleResolutionCandidate {
#[must_use]
pub fn cache_key(&self, resolver_epoch: u64) -> ModuleResolutionCacheKey {
ModuleResolutionCacheKey {
resolver_epoch,
request_index: self.request_index,
directive_item: self.directive_item,
request_kind: self.request_kind,
target: self.target.clone(),
relative_path: self.relative_path.clone(),
roots: self
.roots
.iter()
.map(ModuleResolutionCacheRootKey::from_candidate_root)
.collect(),
range: self.range,
package_context: self.package_context.clone(),
}
}
#[must_use]
pub fn cache_invalidation(
&self,
resolver_epoch: u64,
mut path_exists: impl FnMut(&str) -> bool,
) -> ModuleResolutionCacheInvalidation {
let path_states = self
.roots
.iter()
.map(|root| ModuleResolutionCandidatePathState {
candidate_path: root.candidate_path.clone(),
exists: path_exists(&root.candidate_path),
})
.collect();
ModuleResolutionCacheInvalidation { key: self.cache_key(resolver_epoch), path_states }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModuleResolutionCandidateRoot {
pub path: String,
pub kind: IncRootKind,
pub source: String,
pub candidate_path: String,
pub precedence: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ModuleResolutionCacheKey {
pub resolver_epoch: u64,
pub request_index: usize,
pub directive_item: Option<HirId>,
pub request_kind: ModuleRequestKind,
pub target: String,
pub relative_path: String,
pub roots: Vec<ModuleResolutionCacheRootKey>,
pub range: SourceLocation,
pub package_context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ModuleResolutionCacheRootKey {
pub path: String,
pub kind: IncRootKind,
pub source: String,
pub candidate_path: String,
pub precedence: usize,
}
impl ModuleResolutionCacheRootKey {
fn from_candidate_root(root: &ModuleResolutionCandidateRoot) -> Self {
Self {
path: root.path.clone(),
kind: root.kind,
source: root.source.clone(),
candidate_path: root.candidate_path.clone(),
precedence: root.precedence,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ModuleResolutionCandidatePathState {
pub candidate_path: String,
pub exists: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ModuleResolutionCacheInvalidation {
pub key: ModuleResolutionCacheKey,
pub path_states: Vec<ModuleResolutionCandidatePathState>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ModuleResolutionCandidateStatus {
CandidateBuilt,
Dynamic,
NotFound,
Resolved,
TimedOut,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompilePhaseBlock {
pub phase: CompilePhase,
pub range: SourceLocation,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompilePhase {
Begin,
UnitCheck,
Check,
Init,
End,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompileEnvironmentBoundary {
pub kind: CompileEnvironmentBoundaryKind,
pub range: SourceLocation,
pub boundary_item: Option<HirId>,
pub scope_id: Option<HirScopeId>,
pub package_context: Option<String>,
pub reason: String,
pub provenance: CompileProvenance,
pub confidence: CompileConfidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileEnvironmentBoundaryKind {
DynamicRequire,
DynamicPragmaArgs,
DynamicIncRoot,
PhaseBlockExecution,
SymbolicReferenceDeref,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileProvenance {
ExactAst,
DesugaredAst,
DynamicBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompileConfidence {
High,
Medium,
Low,
}
fn import_spec_from_directive(directive: &CompileDirective, file_id: FileId) -> Option<ImportSpec> {
match directive.action {
CompileDirectiveAction::Use => {
let module = directive.module.as_deref()?;
if is_version_pragma(module) {
return None;
}
if module == "constant" {
return Some(classify_constant_import(directive, file_id));
}
let (kind, symbols, provenance, confidence) =
classify_import_args(&directive.args, module, directive.range);
Some(import_spec(
module.to_string(),
kind,
symbols,
provenance,
confidence,
directive,
file_id,
))
}
CompileDirectiveAction::Require => {
let (module, kind, symbols, provenance, confidence) =
if let Some(module) = directive.module.as_ref() {
(
module.clone(),
ImportKind::Require,
ImportSymbols::Default,
Provenance::ExactAst,
Confidence::High,
)
} else {
(
String::new(),
ImportKind::DynamicRequire,
ImportSymbols::Dynamic,
Provenance::DynamicBoundary,
Confidence::Low,
)
};
Some(import_spec(module, kind, symbols, provenance, confidence, directive, file_id))
}
CompileDirectiveAction::No => None,
}
}
fn import_spec(
module: String,
kind: ImportKind,
symbols: ImportSymbols,
provenance: Provenance,
confidence: Confidence,
directive: &CompileDirective,
file_id: FileId,
) -> ImportSpec {
ImportSpec {
module,
kind,
symbols,
provenance,
confidence,
file_id: Some(file_id),
anchor_id: Some(AnchorId(directive.range.start as u64)),
scope_id: directive.scope_id.map(|id| ScopeId(id.index() as u64)),
span_start_byte: Some(directive.range.start as u32),
}
}
fn classify_import_args(
args: &[String],
module: &str,
range: SourceLocation,
) -> (ImportKind, ImportSymbols, Provenance, Confidence) {
if args.is_empty() {
let bare_len = "use ".len() + module.len() + 1;
let span_len = range.end.saturating_sub(range.start);
if span_len > bare_len {
return (
ImportKind::UseEmpty,
ImportSymbols::None,
Provenance::ExactAst,
Confidence::High,
);
}
return (ImportKind::Use, ImportSymbols::Default, Provenance::ExactAst, Confidence::High);
}
let mut explicit_names = Vec::new();
let mut tags = Vec::new();
let mut has_dynamic_arg = false;
for arg in args {
let trimmed = arg.trim();
if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
continue;
}
if let Some(inner) = parse_qw_content(trimmed) {
collect_qw_import_words(inner, &mut explicit_names, &mut tags);
continue;
}
let was_quoted = is_quoted(trimmed);
let unquoted = unquote(trimmed);
if !was_quoted && looks_like_dynamic_import_arg(unquoted) {
has_dynamic_arg = true;
continue;
}
if let Some(tag) = unquoted.strip_prefix(':') {
tags.push(tag.to_string());
continue;
}
if looks_like_symbol_name(unquoted) {
explicit_names.push(unquoted.to_string());
}
}
if has_dynamic_arg {
return (
ImportKind::UseExplicitList,
ImportSymbols::Dynamic,
Provenance::DynamicBoundary,
Confidence::Low,
);
}
if !tags.is_empty() && explicit_names.is_empty() {
return (
ImportKind::UseTag,
ImportSymbols::Tags(tags),
Provenance::ExactAst,
Confidence::High,
);
}
if !tags.is_empty() && !explicit_names.is_empty() {
return (
ImportKind::UseExplicitList,
ImportSymbols::Mixed { tags, names: explicit_names },
Provenance::ExactAst,
Confidence::High,
);
}
if !explicit_names.is_empty() {
return (
ImportKind::UseExplicitList,
ImportSymbols::Explicit(explicit_names),
Provenance::ExactAst,
Confidence::High,
);
}
(ImportKind::UseEmpty, ImportSymbols::None, Provenance::ExactAst, Confidence::High)
}
fn classify_constant_import(directive: &CompileDirective, file_id: FileId) -> ImportSpec {
let mut constant_names = Vec::new();
let args = &directive.args;
if args.first().map(String::as_str) == Some("{") {
let mut index = 1;
while index < args.len() {
let token = args[index].trim();
if token == "}" || token == "=>" || token == "," {
index += 1;
continue;
}
if index + 1 < args.len() && args[index + 1].trim() == "=>" {
constant_names.push(unquote(token).to_string());
index += 3;
} else {
index += 1;
}
}
} else if let Some(inner) = args.first().and_then(|arg| parse_qw_content(arg.trim())) {
constant_names.extend(inner.split_whitespace().map(str::to_string));
} else if let Some(name) = args.first() {
let trimmed = unquote(name.trim());
if looks_like_constant_name(trimmed) {
constant_names.push(trimmed.to_string());
}
}
let mut seen = std::collections::HashSet::new();
constant_names.retain(|name| seen.insert(name.clone()));
let symbols = if constant_names.is_empty() {
ImportSymbols::None
} else {
ImportSymbols::Explicit(constant_names)
};
import_spec(
"constant".to_string(),
ImportKind::UseConstant,
symbols,
Provenance::ExactAst,
Confidence::High,
directive,
file_id,
)
}
fn collect_qw_import_words(inner: &str, explicit_names: &mut Vec<String>, tags: &mut Vec<String>) {
for word in inner.split_whitespace() {
if let Some(tag) = word.strip_prefix(':') {
tags.push(tag.to_string());
} else {
explicit_names.push(word.to_string());
}
}
}
fn is_version_pragma(module: &str) -> bool {
if module.chars().next().is_some_and(|character| character.is_ascii_digit()) {
return true;
}
module.starts_with('v')
&& module.len() > 1
&& module[1..].chars().all(|character| character.is_ascii_digit() || character == '.')
}
fn parse_qw_content(value: &str) -> Option<&str> {
let rest = value.strip_prefix("qw")?.trim_start();
let mut chars = rest.chars();
let open = chars.next()?;
let close = match open {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
other => other,
};
let inner_start = open.len_utf8();
let inner_end = rest.len().checked_sub(close.len_utf8())?;
if inner_start > inner_end || !rest.ends_with(close) {
return None;
}
Some(&rest[inner_start..inner_end])
}
fn is_quoted(value: &str) -> bool {
(value.starts_with('\'') && value.ends_with('\''))
|| (value.starts_with('"') && value.ends_with('"'))
}
fn unquote(value: &str) -> &str {
if is_quoted(value) && value.len() >= 2 { &value[1..value.len() - 1] } else { value }
}
fn looks_like_dynamic_import_arg(value: &str) -> bool {
value.starts_with('$')
|| value.starts_with('@')
|| value.starts_with('%')
|| value.starts_with('&')
|| value.starts_with('*')
}
fn looks_like_symbol_name(value: &str) -> bool {
let value = unquote(value);
if value.is_empty() {
return false;
}
if value.starts_with(':') {
return true;
}
value
.chars()
.next()
.is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
}
fn looks_like_constant_name(value: &str) -> bool {
if value.is_empty() {
return false;
}
value
.chars()
.next()
.is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
}
fn module_target_to_relative_path(target: &str) -> Option<String> {
let relative_path =
if target.ends_with(".pm") || target.ends_with(".pl") || target.contains(['/', '\\']) {
target.replace('\\', "/")
} else {
let canonical = target.replace('\'', "::");
format!("{}.pm", canonical.replace("::", "/"))
};
is_safe_relative_module_path(&relative_path).then_some(relative_path)
}
fn normalize_module_target(target: &str) -> String {
target.trim().trim_matches('"').trim_matches('\'').to_string()
}
fn is_safe_relative_module_path(path: &str) -> bool {
if path.is_empty() || path.starts_with('/') || path.contains(':') {
return false;
}
path.split('/').all(|segment| !matches!(segment, "" | "." | ".."))
}
fn join_candidate_path(root: &str, relative_path: &str) -> String {
let normalized_root = root.replace('\\', "/");
let trimmed_root = normalized_root.trim_end_matches('/');
if trimmed_root.is_empty() {
relative_path.to_string()
} else {
format!("{trimmed_root}/{relative_path}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum HirKind {
PackageDecl(PackageDecl),
SubDecl(SubDecl),
MethodDecl(MethodDecl),
UseDecl(UseDecl),
RequireDecl(RequireDecl),
VariableDecl(VariableDecl),
CallExpr(CallExpr),
MethodCallExpr(MethodCallExpr),
IndirectCallExpr(IndirectCallExpr),
BarewordExpr(BarewordExpr),
LiteralExpr(LiteralExpr),
BlockShell(BlockShell),
DynamicBoundary(DynamicBoundary),
}
impl HirKind {
pub const ALL_KIND_NAMES: &[&'static str] = &[
"BarewordExpr",
"BlockShell",
"CallExpr",
"DynamicBoundary",
"IndirectCallExpr",
"LiteralExpr",
"MethodCallExpr",
"MethodDecl",
"PackageDecl",
"RequireDecl",
"SubDecl",
"UseDecl",
"VariableDecl",
];
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PackageDecl {
pub name: String,
pub name_range: SourceLocation,
pub has_block: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SubDecl {
pub name: Option<String>,
pub name_range: Option<SourceLocation>,
pub has_prototype: bool,
pub has_signature: bool,
pub attribute_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MethodDecl {
pub name: String,
pub has_signature: bool,
pub attribute_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UseDecl {
pub module: String,
pub args: Vec<String>,
pub has_filter_risk: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RequireDecl {
pub target: Option<String>,
pub arg_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct VariableDecl {
pub declarator: String,
pub variables: Vec<VariableBinding>,
pub attribute_count: usize,
pub has_initializer: bool,
pub is_list: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct VariableBinding {
pub sigil: String,
pub name: String,
pub range: SourceLocation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CallExpr {
pub name: String,
pub arg_count: usize,
pub form: CallForm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CallForm {
NamedFunction,
Coderef,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MethodCallExpr {
pub method: String,
pub arg_count: usize,
pub object_kind: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct IndirectCallExpr {
pub method: String,
pub arg_count: usize,
pub object_kind: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BarewordExpr {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct LiteralExpr {
pub kind: LiteralKind,
pub value: Option<String>,
pub interpolated: Option<bool>,
pub element_count: Option<usize>,
pub pair_count: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LiteralKind {
Number,
String,
Undef,
Array,
Hash,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BlockShell {
pub statement_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DynamicBoundary {
pub kind: DynamicBoundaryKind,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DynamicBoundaryKind {
CoderefCall,
EvalExpression,
DoExpression,
DynamicStashMutation,
Autoload,
SymbolicReferenceDeref,
}