Skip to content

Commit

Permalink
feat(turbopack): Lint invalid CSS modules (#7381)
Browse files Browse the repository at this point in the history
### Description

`css-loader` seems to have a linting rule for invalid CSS Modules.
This PR implements same rule for `swc_css` and `lightningcss`.

### Testing Instructions

See next.js counterpart: vercel/next.js#62040
Closes PACK-2408
  • Loading branch information
kdy1 authored Feb 19, 2024
1 parent d6aadf3 commit 7b77d1b
Showing 1 changed file with 126 additions and 8 deletions.
134 changes: 126 additions & 8 deletions crates/turbopack-css/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use lightningcss::{
stylesheet::{ParserOptions, PrinterOptions, StyleSheet, ToCssResult},
targets::{Features, Targets},
values::url::Url,
visit_types,
visitor::Visit,
};
use once_cell::sync::Lazy;
use regex::Regex;
Expand All @@ -18,10 +20,10 @@ use swc_core::{
base::sourcemap::SourceMapBuilder,
common::{BytePos, FileName, LineCol},
css::{
ast::UrlValue,
ast::{TypeSelector, UrlValue},
codegen::{writer::basic::BasicCssWriter, CodeGenerator},
modules::{CssClassName, TransformConfig},
visit::{VisitMut, VisitMutWith},
visit::{VisitMut, VisitMutWith, VisitWith},
},
};
use tracing::Instrument;
Expand All @@ -30,12 +32,13 @@ use turbo_tasks_fs::{FileContent, FileSystemPath};
use turbopack_core::{
asset::{Asset, AssetContent},
chunk::ChunkingContext,
issue::{Issue, IssueExt, OptionStyledString, StyledString},
issue::{Issue, IssueExt, IssueSource, OptionIssueSource, OptionStyledString, StyledString},
reference::ModuleReferences,
reference_type::ImportContext,
resolve::origin::ResolveOrigin,
source::Source,
source_map::{GenerateSourceMap, OptionSourceMap},
source_pos::SourcePos,
};
use turbopack_swc_utils::emitter::IssueEmitter;

Expand Down Expand Up @@ -470,7 +473,7 @@ pub async fn parse_css(

async fn process_content(
code: String,
fs_path: Vc<FileSystemPath>,
fs_path_vc: Vc<FileSystemPath>,
ident_str: &str,
source: Vc<Box<dyn Source>>,
origin: Vc<Box<dyn ResolveOrigin>>,
Expand Down Expand Up @@ -516,19 +519,36 @@ async fn process_content(

let stylesheet = if use_lightningcss {
StyleSheetLike::LightningCss(match StyleSheet::parse(&code, config.clone()) {
Ok(stylesheet) => stylesheet_into_static(&stylesheet, without_warnings(config.clone())),
Ok(mut ss) => {
ss.visit(&mut CssModuleValidator {
source,
file: fs_path_vc,
})
.unwrap();

stylesheet_into_static(&ss, without_warnings(config.clone()))
}
Err(e) => {
let source = e.loc.as_ref().map(|loc| {
let pos = SourcePos {
line: loc.line as _,
column: loc.column as _,
};
IssueSource::from_line_col(source, pos, pos)
});

ParsingIssue {
file: fs_path,
file: fs_path_vc,
msg: Vc::cell(e.to_string()),
source: Vc::cell(source),
}
.cell()
.emit();
return Ok(ParseCssResult::Unparseable.into());
}
})
} else {
let fs_path = &*fs_path.await?;
let fs_path = &*fs_path_vc.await?;

let handler = swc_core::common::errors::Handler::with_emitter(
true,
Expand Down Expand Up @@ -558,7 +578,7 @@ async fn process_content(
err.to_diagnostics(&handler).emit();
}

let ss = match ss {
let ss: swc_core::css::ast::Stylesheet = match ss {
Ok(v) => v,
Err(err) => {
err.to_diagnostics(&handler).emit();
Expand All @@ -570,6 +590,13 @@ async fn process_content(
return Ok(ParseCssResult::Unparseable.into());
}

if matches!(ty, CssModuleAssetType::Module) {
ss.visit_with(&mut CssModuleValidator {
source,
file: fs_path_vc,
});
}

StyleSheetLike::Swc {
stylesheet: ss,
css_modules: if matches!(ty, CssModuleAssetType::Module) {
Expand Down Expand Up @@ -608,6 +635,91 @@ async fn process_content(
.into())
}

/// Visitor that lints wrong css module usage.
///
/// ```css
/// button {
/// }
/// ```
///
/// is wrong for a css module because it doesn't have a class name.
struct CssModuleValidator {
source: Vc<Box<dyn Source>>,
file: Vc<FileSystemPath>,
}

const CSS_MODULE_ERROR: &str =
"Selector is not pure (pure selectors must contain at least one local class or id)";

/// We only vist top-level selectors.
impl swc_core::css::visit::Visit for CssModuleValidator {
// TODO: SKip some
fn visit_complex_selector(&mut self, n: &swc_core::css::ast::ComplexSelector) {
if n.children.iter().all(|sel| match sel {
swc_core::css::ast::ComplexSelectorChildren::CompoundSelector(sel) => {
sel.subclass_selectors.is_empty()
&& match &sel.type_selector.as_deref() {
Some(TypeSelector::TagName(tag)) => {
!matches!(&*tag.name.value.value, "html" | "body")
}
Some(..) => true,
None => false,
}
}
swc_core::css::ast::ComplexSelectorChildren::Combinator(_) => true,
}) {
ParsingIssue {
file: self.file,
msg: Vc::cell(CSS_MODULE_ERROR.to_string()),
source: Vc::cell(Some(IssueSource::from_swc_offsets(
self.source,
n.span.lo.0 as _,
n.span.hi.0 as _,
))),
}
.cell()
.emit();
}
}

fn visit_simple_block(&mut self, _: &swc_core::css::ast::SimpleBlock) {}
}

/// We only vist top-level selectors.
impl lightningcss::visitor::Visitor<'_> for CssModuleValidator {
type Error = ();

// TODO: Skip some

fn visit_types(&self) -> lightningcss::visitor::VisitTypes {
visit_types!(SELECTORS)
}

fn visit_selector(
&mut self,
selector: &mut lightningcss::selector::Selector<'_>,
) -> Result<(), Self::Error> {
if selector.iter().all(|component| !match component {
parcel_selectors::parser::Component::ID(_)
| parcel_selectors::parser::Component::Class(_) => true,
parcel_selectors::parser::Component::LocalName(local) => {
!matches!(&*local.name.0, "html" | "body")
}
_ => false,
}) {
ParsingIssue {
file: self.file,
msg: Vc::cell(CSS_MODULE_ERROR.to_string()),
source: Vc::cell(None),
}
.cell()
.emit();
}

Ok(())
}
}

#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
pub enum ParseCssResultSourceMap {
Parcel {
Expand Down Expand Up @@ -762,6 +874,7 @@ impl TransformConfig for ModuleTransformConfig {
struct ParsingIssue {
msg: Vc<String>,
file: Vc<FileSystemPath>,
source: Vc<OptionIssueSource>,
}

#[turbo_tasks::value_impl]
Expand All @@ -776,6 +889,11 @@ impl Issue for ParsingIssue {
StyledString::Text("Parsing css source code failed".to_string()).cell()
}

#[turbo_tasks::function]
fn source(&self) -> Vc<OptionIssueSource> {
self.source
}

#[turbo_tasks::function]
async fn description(&self) -> Result<Vc<OptionStyledString>> {
Ok(Vc::cell(Some(
Expand Down

0 comments on commit 7b77d1b

Please sign in to comment.