use crate::lint::{lint_failure, LintResult};
use addr::parse_dns_name;
use anyhow::Result;
use field33_rdftk_core_temporary_fork::model::{
literal::{LanguageTag, Literal},
statement::Statement,
};
use plow_package_management::resolve::Dependency;
use plow_package_management::version::SemanticVersion;
use rustrict::CensorStr;
use semver::Version;
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use thiserror::Error;
pub fn catch_single_annotations_which_may_exist(
annotations: &HashSet<&Rc<dyn Statement>>,
related_field: &str,
) -> Option<LintResult> {
if annotations.len() > 1 {
return Some(lint_failure!(&format!(
"More than 1 {related_field} annotations found."
)));
}
None
}
pub fn catch_single_annotations_which_must_exist(
annotations: &HashSet<&Rc<dyn Statement>>,
related_field: &str,
) -> Option<LintResult> {
if annotations.is_empty() {
return Some(lint_failure!(&format!(
"No `{related_field}` annotations found."
)));
}
if annotations.len() > 1 {
return Some(lint_failure!(&format!(
"More than 1 `{related_field}` annotations found."
)));
}
None
}
pub fn catch_single_or_multiple_annotations_which_must_exist(
annotations: &HashSet<&Rc<dyn Statement>>,
related_field: &str,
) -> Option<LintResult> {
if annotations.is_empty() {
return Some(lint_failure!(&format!(
"No {related_field} annotations found."
)));
}
None
}
#[allow(dead_code)]
pub fn literal_has_language_tag_and_it_is_english(literal: &Rc<dyn Literal>) -> bool {
true
}
pub fn fail_if_has_language_tag(
literal: &Rc<dyn Literal>,
related_field: &str,
) -> Option<LintResult> {
if literal.has_language() {
return Some(lint_failure!(&format!(
"{related_field} does not accept language tags."
)));
}
None
}
pub fn fail_if_contains_inappropriate_word(
literal: &Rc<dyn Literal>,
related_field: &str,
) -> Option<LintResult> {
let raw_literal = literal.lexical_form();
if raw_literal.is_inappropriate() {
return Some(lint_failure!(&format!(
"The value of {related_field} contains one or more inappropriate words which are not allowed in Plow registry."
)));
}
None
}
pub fn fail_if_domain_name_is_invalid(
literal: &Rc<dyn Literal>,
related_field: &str,
) -> Option<LintResult> {
let raw_literal = literal.lexical_form();
parse_dns_name(raw_literal).map_or_else(
|err| match err.kind() {
addr::error::Kind::LabelTooLong => None,
_ => Some(lint_failure!(&format!(
"The value of {related_field} is not a valid domain name."
))),
},
|_| None,
)
}
#[derive(Error, Debug)]
pub enum NamespaceAndNameLintError {
#[error(
"should be alphanumeric. Only underscores ('_') as an additional character is allowed."
)]
InvalidFormat,
#[error("should be in the form of `@<namespace>/<package-name>`")]
InvalidChars,
}
pub fn validate_namespace_and_name(
namespace_and_name_literal: &str,
) -> Result<(), NamespaceAndNameLintError> {
let mut namespace_and_name: Vec<_> = namespace_and_name_literal.split('/').collect();
if let Some(namespace) = namespace_and_name.first() {
if !namespace.starts_with('@') {
return Err(NamespaceAndNameLintError::InvalidFormat);
}
if namespace_and_name.len() != 2 {
return Err(NamespaceAndNameLintError::InvalidFormat);
}
} else {
return Err(NamespaceAndNameLintError::InvalidFormat);
}
#[allow(clippy::unwrap_used)]
let name = namespace_and_name.pop().unwrap().replace('_', "");
#[allow(clippy::unwrap_used)]
let namespace = namespace_and_name
.pop()
.unwrap()
.replace('_', "")
.replace('@', "");
if !name.chars().all(char::is_alphanumeric) || !namespace.chars().all(char::is_alphanumeric) {
return Err(NamespaceAndNameLintError::InvalidChars);
}
Ok(())
}
#[derive(Error, Debug, Clone)]
pub enum VersionLiteralLintFailureOrWarning {
#[error(transparent)]
Failure(VersionLiteralLintFailure),
#[error(transparent)]
Warning(VersionLiteralLintWarning),
}
#[derive(Error, Debug, Clone)]
pub enum VersionLiteralLintFailure {
#[error("does not allow for empty version literals")]
Empty,
#[error("does not allow for pre-release or build indicators, e.g. `1.0.0-alpha.1+001`")]
PreReleaseOrBuildNotAllowed,
#[error("does not allow versions other than a single version or a pair of versions.")]
OnlySingleOrPair,
#[error("does not allow version pairs with `=` character in it.")]
NoExactPrefixOnVersionPairs,
#[error("can not be solved.")]
CanNotBeSolved,
#[error("does not allow for bare versions, e.g. `1.0.0`. The version which is bare is {0}")]
BareVersionNotAllowed(String),
#[error("is not valid: {0}\nPlease avoid prefixes and make your version complete, including all `major`, `minor` and `patch` fields.")]
InvalidSemanticVersionLiteral(String),
}
#[derive(Error, Debug, Clone)]
pub enum VersionLiteralLintWarning {
#[error("contains a single wildcard `*` it might be better to be more specific.")]
ContainsSingleWildcard,
#[error("contains one or more wildcards `*` or `x` it might be better to be more specific. The version which contains wildcards is: {0}")]
ContainsWildcards(String),
}
pub fn validate_semantic_version_literal(
version_literal: &str,
) -> Result<(), VersionLiteralLintFailureOrWarning> {
use VersionLiteralLintFailureOrWarning::*;
Version::parse(version_literal)
.map_err(|err| {
Failure(VersionLiteralLintFailure::InvalidSemanticVersionLiteral(
err.to_string(),
))
})
.and_then(|parsed| {
if parsed.pre.is_empty() && parsed.build.is_empty() {
Ok(())
} else {
Err(Failure(
VersionLiteralLintFailure::PreReleaseOrBuildNotAllowed,
))
}
})
}
#[allow(clippy::too_many_lines)]
pub fn validate_semantic_version_requirement_literal(
version_literal: &str,
) -> Result<(), Vec<VersionLiteralLintFailureOrWarning>> {
use VersionLiteralLintFailureOrWarning::*;
let failures_and_warnings: RefCell<Vec<VersionLiteralLintFailureOrWarning>> =
RefCell::new(vec![]);
if version_literal.is_empty() {
failures_and_warnings
.borrow_mut()
.push(Failure(VersionLiteralLintFailure::Empty));
return Err(failures_and_warnings.take());
}
let versions: Vec<&str> = version_literal
.split(|character| character == ',' || character == ' ')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
for version in &versions {
let has_wildcards = version.contains('*') || version.contains('x');
#[allow(clippy::else_if_without_else)]
if has_wildcards {
failures_and_warnings.borrow_mut().push(Warning(
VersionLiteralLintWarning::ContainsWildcards((*version_literal).to_owned()),
));
} else if char::is_digit(version.chars().next().expect("Unreachable"), 10) {
failures_and_warnings.borrow_mut().push(Failure(
VersionLiteralLintFailure::BareVersionNotAllowed((*version_literal).to_owned()),
));
} else if *version == "*" {
failures_and_warnings
.borrow_mut()
.push(Warning(VersionLiteralLintWarning::ContainsSingleWildcard));
}
}
match versions.len() {
1 => {
if failures_and_warnings.borrow().is_empty() {
return Ok(());
}
Err(failures_and_warnings.take())
}
2 => Dependency::<SemanticVersion>::try_new("@dummy/dummy", version_literal).map_or_else(
|err| {
failures_and_warnings.borrow_mut().push(Failure(
VersionLiteralLintFailure::InvalidSemanticVersionLiteral(err.to_string()),
));
Err(failures_and_warnings.take())
},
|d: Dependency<SemanticVersion>| {
if d.is_version_range_none() {
failures_and_warnings
.borrow_mut()
.push(Failure(VersionLiteralLintFailure::CanNotBeSolved));
}
for version in &versions {
if version.starts_with('=') {
failures_and_warnings.borrow_mut().push(Failure(
VersionLiteralLintFailure::NoExactPrefixOnVersionPairs,
));
}
}
if failures_and_warnings.borrow().is_empty() {
return Ok(());
}
Err(failures_and_warnings.take())
},
),
_ => {
failures_and_warnings
.borrow_mut()
.push(Failure(VersionLiteralLintFailure::OnlySingleOrPair));
Err(failures_and_warnings.take())
}
}
}