fallow-extract 3.32.0

AST extraction engine for fallow codebase intelligence (parser, complexity, SFC / Astro / MDX / CSS)
Documentation
use crate::tests::parse_ts;

fn signature_pairs(source: &str) -> Vec<(String, String)> {
    parse_ts(source)
        .public_signature_type_references
        .into_iter()
        .map(|reference| (reference.export_name, reference.type_name))
        .collect()
}

#[test]
fn signature_references_preserve_export_and_per_owner_order_with_duplicates() {
    let source = r"
type Input = { value: string };
type Output = { value: number };
function local(first: Input, second: Input): Output {
    return { value: first.value.length + second.value.length };
}
export { local as beta };
export { local as alpha };
";

    assert_eq!(
        signature_pairs(source),
        vec![
            ("beta".to_string(), "Input".to_string()),
            ("beta".to_string(), "Input".to_string()),
            ("beta".to_string(), "Output".to_string()),
            ("alpha".to_string(), "Input".to_string()),
            ("alpha".to_string(), "Input".to_string()),
            ("alpha".to_string(), "Output".to_string()),
        ]
    );
}

#[test]
fn signature_references_preserve_named_and_anonymous_default_exports() {
    let named = r"
type Input = { value: string };
type Output = { value: number };
export default function convert(value: Input): Output {
    return { value: value.value.length };
}
";
    let anonymous = r"
type Input = { value: string };
type Output = { value: number };
export default function (value: Input): Output {
    return { value: value.value.length };
}
";
    let expected = vec![
        ("default".to_string(), "Input".to_string()),
        ("default".to_string(), "Output".to_string()),
    ];

    assert_eq!(signature_pairs(named), expected);
    assert_eq!(signature_pairs(anonymous), expected);
}

fn types_for_export(source: &str, export_name: &str) -> Vec<String> {
    signature_pairs(source)
        .into_iter()
        .filter(|(export, _)| export == export_name)
        .map(|(_, type_name)| type_name)
        .collect()
}

#[test]
fn signature_references_include_function_argument_of_wrapper_call() {
    let source = r"
declare function wrap<T>(value: T): T;
interface CardProps { title: string }
export const Card = wrap(function Card({ title }: CardProps) { return title; });
";

    assert_eq!(
        types_for_export(source, "Card"),
        vec!["CardProps".to_string()]
    );
}

#[test]
fn signature_references_include_wrapper_call_type_arguments_and_arrow_argument() {
    let source = r"
declare function withRef<T, P>(render: (props: P, ref: T) => unknown): (props: P) => unknown;
interface FieldProps { label: string }
interface FieldHandle { focus(): void }
export const Field = withRef<FieldHandle, FieldProps>((props: FieldProps) => props.label);
";

    assert_eq!(
        types_for_export(source, "Field"),
        vec![
            "FieldHandle".to_string(),
            "FieldProps".to_string(),
            "FieldProps".to_string(),
        ]
    );
}

#[test]
fn signature_references_include_new_expression_type_arguments() {
    let source = r"
declare class Store<T> { constructor(value: T) }
interface StoreState { count: number }
export const store = new Store<StoreState>({ count: 0 });
";

    assert_eq!(
        types_for_export(source, "store"),
        vec!["StoreState".to_string()]
    );
}

#[test]
fn signature_references_include_type_assertions_on_initializers() {
    let source = r#"
type Level = "low" | "high";
type Shape = { size: number };
export const DEFAULT_LEVEL = "low" as Level;
export const DEFAULT_SHAPE = <Shape>{ size: 1 };
export const WRAPPED = ("high" as Level)!;
"#;

    assert_eq!(
        types_for_export(source, "DEFAULT_LEVEL"),
        vec!["Level".to_string()]
    );
    assert_eq!(
        types_for_export(source, "DEFAULT_SHAPE"),
        vec!["Shape".to_string()]
    );
    assert_eq!(
        types_for_export(source, "WRAPPED"),
        vec!["Level".to_string()]
    );
}

#[test]
fn signature_references_ignore_value_arguments_of_wrapper_calls() {
    let source = r"
declare function make<T>(value: T): T;
interface Hidden { value: number }
const local = (input: Hidden) => input.value;
export const made = make(local);
";

    assert!(types_for_export(source, "made").is_empty());
}

#[test]
fn signature_references_include_inferred_factory_return_shapes() {
    let source = r"
type Answers = Record<string, number>;
type Snapshot = { size: number };
type Settings = { mode: string };
type Scratch = { note: string };
export function makeCache() {
    function get(): Answers | undefined {
        return undefined;
    }
    return { get };
}
export const makeReader = () => ({
    read: (): Snapshot => ({ size: 1 }),
});
export function makeSettings() {
    return { mode: 'fast' } as Settings;
}
export function makeCounter() {
    const scratch: Scratch = { note: 'local' };
    const count = () => scratch.note.length;
    return { count };
}
";

    assert_eq!(
        signature_pairs(source),
        vec![
            ("makeCache".to_string(), "Answers".to_string()),
            ("makeReader".to_string(), "Snapshot".to_string()),
            ("makeSettings".to_string(), "Settings".to_string()),
        ]
    );
}

#[test]
fn signature_references_skip_body_when_return_type_is_explicit() {
    let source = r"
type Result = { ok: boolean };
type Hidden = { value: number };
export function run(): Result {
    function inner(): Hidden {
        return { value: 1 };
    }
    return { ok: inner().value > 0 };
}
";

    assert_eq!(
        signature_pairs(source),
        vec![("run".to_string(), "Result".to_string())]
    );
}

#[test]
fn signature_references_mark_satisfies_clause_types() {
    let source = r#"
type Channel = "email" | "sms";
type Handler = (input: Input) => void;
type Input = { id: string };
export const CHANNELS = (["email"] as const satisfies readonly Channel[])!;
export const handle = ((input: Input): void => {}) satisfies Handler;
"#;

    let refs: Vec<(String, String, bool)> = parse_ts(source)
        .public_signature_type_references
        .into_iter()
        .map(|reference| {
            (
                reference.export_name,
                reference.type_name,
                reference.from_satisfies,
            )
        })
        .collect();

    assert_eq!(
        refs,
        vec![
            ("CHANNELS".to_string(), "Channel".to_string(), true),
            ("handle".to_string(), "Input".to_string(), false),
            ("handle".to_string(), "Handler".to_string(), true),
        ]
    );
}