Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ It answers `call` as well, so it can be handed to anything expecting something c
| `filename` | `String` | The name to use in errors and warnings. |
| `minify` | `bool` | Whether to print the result as small as it goes. |
| `error_recovery` | `bool` | Whether to carry on past a rule it cannot read. Off by default, so such a rule raises. |
| `targets` | `Hash` | The browsers being compiled for, each as its major version. |
| `targets` | `Hash` | The browsers being compiled for, each with the oldest version supported. |
| `css_modules` | `bool`, `Hash` | Whether to compile as a CSS module, and how. |
| `scope` | `String` | A selector fragment to narrow every rule by. |

Expand Down Expand Up @@ -337,13 +337,40 @@ result = LightningCSS.transform(".a:deep(.b) { color: red }", minify: true)
result.warnings?
#=> true

result.warnings.first
result.warnings.first.to_s
#=> "'deep' is not recognized as a valid pseudo-class. Did you mean '::deep' (pseudo-element) or is this a typo? at :1:9"

result.code
#=> ".a:deep(.b){color:red}"
```

### Locations

A warning is a `LightningCSS::Warning` with a `message`, a `type`, and a `location`, the same as a diagnostic in Prism or Herb. A `LightningCSS::Error` carries the same `type` and `location`, and keeps the location in its message.

A `LightningCSS::Location` counts lines from 1 and columns from 0, the same as Prism, Herb, and Yerba, and prints the way an editor reads it:

```ruby
css = ".a { color: red; }\n.b, { color: blue; }\n"

begin
LightningCSS.transform(css, filename: "main.css")
rescue LightningCSS::ParseError => error
error.message #=> "Invalid empty selector at main.css:2:5"
error.type #=> "SelectorError"
error.location #=> #<data LightningCSS::Location filename="main.css", line=2, column=4>
end

warning = LightningCSS.transform(css, filename: "main.css", error_recovery: true).warnings.first

warning.message #=> "Invalid empty selector"
warning.type #=> "SelectorError"
warning.location.line #=> 2
warning.to_s #=> "Invalid empty selector at main.css:2:5"
```

A warning converts to the string it always was wherever a `String` is expected and is equal to it, so `warnings.include?("...")` still works. An error without a location, such as an `OptionError`, has `nil` for `type` and `location`.

### Development

The gem is a C extension over a Rust crate. `rust/` builds a static library and generates the C header with [cbindgen](https://github.com/mozilla/cbindgen), `ext/lightningcss/` wraps it, and `lib/` is the Ruby API over that.
Expand Down
4 changes: 2 additions & 2 deletions ext/lightningcss/lightningcss.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ static VALUE error_class_for(enum LightningCssErrorCode code) {

static VALUE unwrap(struct LightningCssResult result) {
if (result.error) {
VALUE message = make_utf8_string(result.error);
VALUE payload = make_utf8_string(result.error);
VALUE error_class = error_class_for(result.code);

lightningcss_result_free(result);

rb_raise(error_class, "%s", StringValueCStr(message));
rb_exc_raise(rb_funcall(error_class, rb_intern("from_json"), 1, payload));
}

if (!result.value) {
Expand Down
2 changes: 2 additions & 0 deletions lib/lightningcss.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "json"

require_relative "lightningcss/version"
require_relative "lightningcss/location"
require_relative "lightningcss/errors"
require_relative "lightningcss/backend"

Expand All @@ -14,6 +15,7 @@
end

require_relative "lightningcss/options"
require_relative "lightningcss/warning"
require_relative "lightningcss/result"
require_relative "lightningcss/node"
require_relative "lightningcss/parse_result"
Expand Down
23 changes: 22 additions & 1 deletion lib/lightningcss/errors.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
# frozen_string_literal: true

module LightningCSS
class Error < StandardError; end
class Error < StandardError
attr_reader :type #: String?
attr_reader :location #: LightningCSS::Location?

#: (String) -> instance
def self.from_json(payload)
parsed = JSON.parse(payload)
location = parsed["location"] && Location.from_hash(parsed["location"])
message = parsed.fetch("message")

new(location ? "#{message} at #{location}" : message, type: parsed["type"], location: location)
end

#: (?String?, ?type: String?, ?location: LightningCSS::Location?) -> void
def initialize(message = nil, type: nil, location: nil)
super(message)

@type = type
@location = location
end
end

class ParseError < Error; end
class OptionError < Error; end
class BundleError < Error; end
Expand Down
21 changes: 21 additions & 0 deletions lib/lightningcss/location.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module LightningCSS
Location = Data.define(
:filename, #: String?
:line, #: Integer
:column #: Integer
)

class Location
#: (Hash[String, untyped]) -> LightningCSS::Location
def self.from_hash(hash)
new(filename: hash["filename"], line: hash.fetch("line"), column: hash.fetch("column"))
end

#: () -> String
def to_s
"#{filename}:#{line}:#{column + 1}"
end
end
end
8 changes: 4 additions & 4 deletions lib/lightningcss/parse_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module LightningCSS
class ParseResult
attr_reader :source #: String?
attr_reader :stylesheet #: Hash[String, untyped]
attr_reader :warnings #: Array[String]
attr_reader :warnings #: Array[LightningCSS::Warning]

#: (String, ?String?) -> LightningCSS::ParseResult
def self.from_json(payload, source = nil)
Expand All @@ -18,11 +18,11 @@ def self.from_json(payload, source = nil)
new(
source: source,
stylesheet: parsed.fetch("stylesheet"),
warnings: parsed.fetch("warnings")
warnings: parsed.fetch("warnings").map { |warning| Warning.from_hash(warning) }
)
end

#: (stylesheet: Hash[String, untyped], warnings: Array[String], ?source: String?) -> void
#: (stylesheet: Hash[String, untyped], warnings: Array[LightningCSS::Warning], ?source: String?) -> void
def initialize(stylesheet:, warnings:, source: nil)
@source = source
@stylesheet = stylesheet.freeze
Expand All @@ -44,7 +44,7 @@ def warnings?
#: () -> String
def inspect
parts = ["rules=#{stylesheet.fetch("rules").length}"]
parts << "warnings=#{warnings.inspect}" if warnings?
parts << "warnings=#{warnings.map(&:to_s).inspect}" if warnings?

"#<#{self.class.name} #{parts.join(" ")}>"
end
Expand Down
8 changes: 4 additions & 4 deletions lib/lightningcss/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module LightningCSS
class Result
attr_reader :code #: String
attr_reader :exports #: Hash[String, String]?
attr_reader :warnings #: Array[String]
attr_reader :warnings #: Array[LightningCSS::Warning]

#: (String) -> LightningCSS::Result
def self.from_json(payload)
Expand All @@ -22,11 +22,11 @@ def self.from_json(payload)
new(
code: parsed.fetch("code"),
exports: parsed["exports"],
warnings: parsed.fetch("warnings")
warnings: parsed.fetch("warnings").map { |warning| Warning.from_hash(warning) }
)
end

#: (code: String, warnings: Array[String], ?exports: Hash[String, String]?) -> void
#: (code: String, warnings: Array[LightningCSS::Warning], ?exports: Hash[String, String]?) -> void
def initialize(code:, warnings:, exports: nil)
@code = code
@exports = exports
Expand All @@ -49,7 +49,7 @@ def to_s
def inspect
parts = ["code=#{code.inspect}"]
parts << "exports=#{exports.inspect}" if exports
parts << "warnings=#{warnings.inspect}" if warnings?
parts << "warnings=#{warnings.map(&:to_s).inspect}" if warnings?

"#<#{self.class.name} #{parts.join(" ")}>"
end
Expand Down
60 changes: 60 additions & 0 deletions lib/lightningcss/warning.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module LightningCSS
class Warning
attr_reader :message #: String
attr_reader :type #: String?
attr_reader :location #: LightningCSS::Location?

#: (Hash[String, untyped]) -> LightningCSS::Warning
def self.from_hash(hash)
new(
message: hash.fetch("message"),
type: hash["type"],
location: hash["location"] && Location.from_hash(hash["location"])
)
end

#: (message: String, ?type: String?, ?location: LightningCSS::Location?) -> void
def initialize(message:, type: nil, location: nil)
@message = message
@type = type
@location = location

freeze
end

#: () -> Hash[Symbol, untyped]
def to_h
{ message: message, type: type, location: location&.to_h }
end

#: () -> String
def to_s
location ? "#{message} at #{location}" : message
end

alias to_str to_s

#: (untyped) -> bool
def ==(other)
case other
when Warning then to_h == other.to_h
when String then to_s == other
else false
end
end

alias eql? ==

#: () -> Integer
def hash
to_h.hash
end

#: () -> String
def inspect
"#<#{self.class.name} #{to_s.inspect}>"
end
end
end
49 changes: 22 additions & 27 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ use std::ptr;
use std::sync::{Arc, RwLock};

use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider};
use lightningcss::error::Error;
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleAttribute, StyleSheet};
use lightningcss::visitor::Visit;

use crate::options::{ParseResult, TransformOptions, TransformResult};
use crate::options::{Diagnostic, ParseResult, TransformOptions, TransformResult};
use crate::scope::Scoper;

pub const VERSION: &str = env!("GEM_VERSION");
Expand Down Expand Up @@ -57,50 +56,43 @@ impl LightningCssResult {
fn err(failure: Failure) -> Self {
Self {
value: ptr::null_mut(),
error: into_c_string(failure.message),
error: into_c_string(serde_json::to_string(&failure.diagnostic).unwrap_or(failure.diagnostic.message)),
code: failure.code,
}
}
}

pub struct Failure {
code: LightningCssErrorCode,
message: String,
}

fn describe<T: std::fmt::Display>(error: &Error<T>) -> String {
match &error.loc {
Some(loc) => format!("{} at {}:{}:{}", error.kind, loc.filename, loc.line + 1, loc.column),
None => error.kind.to_string(),
}
diagnostic: Diagnostic,
}

impl Failure {
fn parse(message: impl Into<String>) -> Self {
fn parse(diagnostic: impl Into<Diagnostic>) -> Self {
Self {
code: LightningCssErrorCode::Parse,
message: message.into(),
diagnostic: diagnostic.into(),
}
}

fn option(message: impl Into<String>) -> Self {
fn option(diagnostic: impl Into<Diagnostic>) -> Self {
Self {
code: LightningCssErrorCode::Option,
message: message.into(),
diagnostic: diagnostic.into(),
}
}

fn bundle(message: impl Into<String>) -> Self {
fn bundle(diagnostic: impl Into<Diagnostic>) -> Self {
Self {
code: LightningCssErrorCode::Bundle,
message: message.into(),
diagnostic: diagnostic.into(),
}
}

fn internal(message: impl Into<String>) -> Self {
fn internal(diagnostic: impl Into<Diagnostic>) -> Self {
Self {
code: LightningCssErrorCode::Internal,
message: message.into(),
diagnostic: diagnostic.into(),
}
}
}
Expand Down Expand Up @@ -143,11 +135,12 @@ fn parse_source<'i>(code: &'i str, options: &TransformOptions) -> Result<ParseRe
..ParserOptions::default()
};

let stylesheet = StyleSheet::parse(code, parser_options).map_err(|error| Failure::parse(describe(&error)))?;
let stylesheet =
StyleSheet::parse(code, parser_options).map_err(|error| Failure::parse(Diagnostic::from_error(&error)))?;

let warnings = collected
.read()
.map(|warnings| warnings.iter().map(describe).collect())
.map(|warnings| warnings.iter().map(Diagnostic::from_error).collect())
.unwrap_or_default();

Ok(ParseResult { stylesheet, warnings })
Expand All @@ -170,7 +163,8 @@ fn transform_source(code: &str, options: &TransformOptions) -> Result<TransformR
..ParserOptions::default()
};

let mut stylesheet = StyleSheet::parse(code, parser_options).map_err(|error| Failure::parse(describe(&error)))?;
let mut stylesheet =
StyleSheet::parse(code, parser_options).map_err(|error| Failure::parse(Diagnostic::from_error(&error)))?;

if let Some(fragment) = &options.scope {
let mut scoper = Scoper::parse(fragment).map_err(Failure::option)?;
Expand Down Expand Up @@ -209,7 +203,7 @@ fn transform_source(code: &str, options: &TransformOptions) -> Result<TransformR

let warnings = collected
.read()
.map(|warnings| warnings.iter().map(describe).collect())
.map(|warnings| warnings.iter().map(Diagnostic::from_error).collect())
.unwrap_or_default();

Ok(TransformResult {
Expand Down Expand Up @@ -240,8 +234,8 @@ fn bundle_source(path: &str, options: &TransformOptions) -> Result<TransformResu
let mut stylesheet = bundler
.bundle(std::path::Path::new(path))
.map_err(|error| match &error.kind {
BundleErrorKind::ParserError(_) => Failure::parse(describe(&error)),
_ => Failure::bundle(describe(&error)),
BundleErrorKind::ParserError(_) => Failure::parse(Diagnostic::from_error(&error)),
_ => Failure::bundle(Diagnostic::from_error(&error)),
})?;

if let Some(fragment) = &options.scope {
Expand Down Expand Up @@ -278,7 +272,7 @@ fn bundle_source(path: &str, options: &TransformOptions) -> Result<TransformResu

let warnings = collected
.read()
.map(|warnings| warnings.iter().map(describe).collect())
.map(|warnings| warnings.iter().map(Diagnostic::from_error).collect())
.unwrap_or_default();

Ok(TransformResult {
Expand All @@ -295,7 +289,8 @@ fn transform_attribute(code: &str, options: &TransformOptions) -> Result<Transfo
..ParserOptions::default()
};

let mut attribute = StyleAttribute::parse(code, parser_options).map_err(|error| Failure::parse(describe(&error)))?;
let mut attribute =
StyleAttribute::parse(code, parser_options).map_err(|error| Failure::parse(Diagnostic::from_error(&error)))?;

let targets = options.to_targets().map_err(Failure::option)?;

Expand Down
Loading
Loading