diff --git a/crates/oxc_codegen/src/comment.rs b/crates/oxc_codegen/src/comment.rs index 16735868f5fb9..682693b8530f7 100644 --- a/crates/oxc_codegen/src/comment.rs +++ b/crates/oxc_codegen/src/comment.rs @@ -9,6 +9,58 @@ use crate::{Codegen, LegalComment, options::CommentOptions}; pub type CommentsMap = FxHashMap>; +/// Custom iterator that splits text on line terminators while handling CRLF as a single unit. +/// This avoids creating empty strings between CR and LF characters. +/// +/// # Example +/// Standard split would turn `"line1\r\nline2"` into `["line1", "", "line2"]` because +/// it treats \r and \n as separate terminators. This iterator correctly produces +/// `["line1", "line2"]` by treating \r\n as a single terminator. +struct LineTerminatorSplitter<'a> { + text: &'a str, + position: usize, +} + +impl<'a> LineTerminatorSplitter<'a> { + fn new(text: &'a str) -> Self { + Self { text, position: 0 } + } +} + +impl<'a> Iterator for LineTerminatorSplitter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + if self.position >= self.text.len() { + return None; + } + + let start = self.position; + let chars = self.text[self.position..].char_indices(); + + for (i, c) in chars { + if is_line_terminator(c) { + let line = &self.text[start..start + i]; + self.position = start + i + c.len_utf8(); + + // If this is CR followed by LF, skip the LF to treat CRLF as a single terminator + if c == '\r' + && self.text.as_bytes().get(self.position).is_some_and(|&next| next == b'\n') + { + self.position += 1; + } + + return Some(line); + } + } + + // Return the remaining text + let line = &self.text[start..]; + self.position = self.text.len(); + Some(line) + } +} + impl Codegen<'_> { pub(crate) fn build_comments(&mut self, comments: &[Comment]) { if self.options.comments == CommentOptions::disabled() { @@ -132,8 +184,7 @@ impl Codegen<'_> { self.print_str_escaping_script_close_tag(comment_source); } CommentKind::Block => { - // Print block comments with our own indentation. - for line in comment_source.split(is_line_terminator) { + for line in LineTerminatorSplitter::new(comment_source) { if !line.starts_with("/*") { self.print_indent(); } @@ -167,7 +218,7 @@ impl Codegen<'_> { if comment.is_block() && text.contains(is_line_terminator) { let mut buffer = String::with_capacity(text.len()); // Print block comments with our own indentation. - for line in text.split(is_line_terminator) { + for line in LineTerminatorSplitter::new(&text) { if !line.starts_with("/*") { buffer.push('\t'); } diff --git a/crates/oxc_isolated_declarations/tests/fixtures/class-decorator.ts b/crates/oxc_isolated_declarations/tests/fixtures/class-decorator.ts index 967f79da01f74..8e5b1b8598424 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/class-decorator.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/class-decorator.ts @@ -1,13 +1,17 @@ declare const decorator: any; export class Test1 { - /** This method will trigger the feature highlight dialog load/show based on dialogId and analyticsId */ + /** + * This method will trigger the feature highlight dialog load/show based on dialogId and analyticsId + */ @decorator property: (() => any) | undefined; } export class Test2 { - /** This method will trigger the feature highlight dialog load/show based on dialogId and analyticsI */ + /** + * This method will trigger the feature highlight dialog load/show based on dialogId and analyticsI + */ @decorator property: ((arg: any) => any) | undefined; } diff --git a/crates/oxc_isolated_declarations/tests/fixtures/class.ts b/crates/oxc_isolated_declarations/tests/fixtures/class.ts index 22f1e3c4ecfff..7e9131355bc90 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/class.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/class.ts @@ -20,17 +20,29 @@ export abstract class Qux { } export class Baz { - /** Just a comment */ + /** + * Just a comment + */ readonly prop1 = "some string"; - /** Just a comment */ + /** + * Just a comment + */ prop2 = "another string"; - /** Just a comment */ + /** + * Just a comment + */ private prop3 = "yet another string"; - /** Just a comment */ + /** + * Just a comment + */ private prop4(): void {} - /** Just a comment */ + /** + * Just a comment + */ private static prop5 = "yet another string"; - /** Just a comment */ + /** + * Just a comment + */ private static prop6(): void {} } @@ -142,4 +154,4 @@ export class PrivateConstructorWithDefaultParameters { readonly prop3: boolean = true, normalParam: string = "normal", ) {} -} \ No newline at end of file +} diff --git a/crates/oxc_isolated_declarations/tests/fixtures/export-default.ts b/crates/oxc_isolated_declarations/tests/fixtures/export-default.ts index facd97dbbadf4..9a8a498d41553 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/export-default.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/export-default.ts @@ -1,6 +1,8 @@ const defaultDelimitersClose = new Uint8Array([125, 125]); -/** comment should be a leading comment of the class */ +/** + * comment should be a leading comment of the class + */ export default class Tokenizer { public delimiterClose: Uint8Array = defaultDelimitersClose; } diff --git a/crates/oxc_isolated_declarations/tests/fixtures/export-default2.ts b/crates/oxc_isolated_declarations/tests/fixtures/export-default2.ts index 60f275fd1fd23..20ebd4cfc78eb 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/export-default2.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/export-default2.ts @@ -1,4 +1,6 @@ -/** comment should be a leading comment of the arrow function */ +/** + * comment should be a leading comment of the arrow function + */ export default () => { return 0; }; diff --git a/crates/oxc_isolated_declarations/tests/fixtures/strip-internal.ts b/crates/oxc_isolated_declarations/tests/fixtures/strip-internal.ts index 89430bd222bc7..e0d74e46d35e6 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/strip-internal.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/strip-internal.ts @@ -50,7 +50,9 @@ export interface StripInternalInterfaceSignatures { * @internal */ internalProperty: number; - /**@internal */ + /** + * @internal + */ new (): any; } @@ -63,7 +65,9 @@ export type StripInternalTypeSignatures = { * @internal */ internalProperty: number; - /**@internal */ + /** + * @internal + */ new (): any; }; @@ -79,4 +83,4 @@ export namespace StripInternalNamespaceInner { /** * @internal */ -export namespace StripInternalNamespace {} \ No newline at end of file +export namespace StripInternalNamespace {} diff --git a/crates/oxc_isolated_declarations/tests/fixtures/ts-export-assignment.ts b/crates/oxc_isolated_declarations/tests/fixtures/ts-export-assignment.ts index c91931032b367..dcafc62005580 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/ts-export-assignment.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/ts-export-assignment.ts @@ -1,6 +1,8 @@ const Res = 0; -/** comment should be a leading comment of the function Foo */ +/** + * comment should be a leading comment of the function Foo + */ export = function Foo(): typeof Res { return Res; }; diff --git a/crates/oxc_isolated_declarations/tests/snapshots/class-decorator.snap b/crates/oxc_isolated_declarations/tests/snapshots/class-decorator.snap index b544315dad8bb..57927ae0b2427 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/class-decorator.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/class-decorator.snap @@ -6,11 +6,15 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/class-decorator.ts ==================== .D.TS ==================== export declare class Test1 { - /** This method will trigger the feature highlight dialog load/show based on dialogId and analyticsId */ + /** + * This method will trigger the feature highlight dialog load/show based on dialogId and analyticsId + */ property: (() => any) | undefined; } export declare class Test2 { - /** This method will trigger the feature highlight dialog load/show based on dialogId and analyticsI */ + /** + * This method will trigger the feature highlight dialog load/show based on dialogId and analyticsI + */ property: ((arg: any) => any) | undefined; } export declare class Test3 { diff --git a/crates/oxc_isolated_declarations/tests/snapshots/class.snap b/crates/oxc_isolated_declarations/tests/snapshots/class.snap index 633505f5e5c7c..be6ada59c679d 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/class.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/class.snap @@ -21,17 +21,29 @@ export declare abstract class Qux { baz(): void; } export declare class Baz { - /** Just a comment */ + /** + * Just a comment + */ readonly prop1 = "some string"; - /** Just a comment */ + /** + * Just a comment + */ prop2: string; - /** Just a comment */ + /** + * Just a comment + */ private prop3; - /** Just a comment */ + /** + * Just a comment + */ private prop4; - /** Just a comment */ + /** + * Just a comment + */ private static prop5; - /** Just a comment */ + /** + * Just a comment + */ private static prop6; } export declare class Boo { @@ -114,20 +126,20 @@ export declare class PrivateConstructorWithDefaultParameters { x TS9038: Computed property names on class or object literals cannot be | inferred with --isolatedDeclarations. - ,-[69:14] - 68 | public get badGetter() { - 69 | return {[('x')]: 1}; + ,-[81:14] + 80 | public get badGetter() { + 81 | return {[('x')]: 1}; : ^^^^^ - 70 | } + 82 | } `---- x TS9011: Parameter must have an explicit type annotation with | --isolatedDeclarations. - ,-[67:14] - 66 | export class PublicMethodClass { - 67 | public bad(a): void {} + ,-[79:14] + 78 | export class PublicMethodClass { + 79 | public bad(a): void {} : ^ - 68 | public get badGetter() { + 80 | public get badGetter() { `---- diff --git a/crates/oxc_isolated_declarations/tests/snapshots/export-default.snap b/crates/oxc_isolated_declarations/tests/snapshots/export-default.snap index df54d8a07fdc4..1a90cf8284eea 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/export-default.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/export-default.snap @@ -5,7 +5,9 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/export-default.ts ``` ==================== .D.TS ==================== -/** comment should be a leading comment of the class */ +/** +* comment should be a leading comment of the class +*/ export default class Tokenizer { delimiterClose: Uint8Array; } diff --git a/crates/oxc_isolated_declarations/tests/snapshots/export-default2.snap b/crates/oxc_isolated_declarations/tests/snapshots/export-default2.snap index df89164f3c5ba..0dd8e975d9817 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/export-default2.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/export-default2.snap @@ -5,6 +5,8 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/export-default2.ts ``` ==================== .D.TS ==================== -/** comment should be a leading comment of the arrow function */ +/** +* comment should be a leading comment of the arrow function +*/ declare const _default: () => number; export default _default; diff --git a/crates/oxc_isolated_declarations/tests/snapshots/ts-export-assignment.snap b/crates/oxc_isolated_declarations/tests/snapshots/ts-export-assignment.snap index 948d5be9c4b69..e358ebe3664df 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/ts-export-assignment.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/ts-export-assignment.snap @@ -6,6 +6,8 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/ts-export-assignment ==================== .D.TS ==================== declare const Res = 0; -/** comment should be a leading comment of the function Foo */ +/** +* comment should be a leading comment of the function Foo +*/ declare const _default: () => typeof Res; export = _default;