diff --git a/__tests__/default-export-declaration.test.ts b/__tests__/default-export-declaration.test.ts index 02d3034f6..ac7671031 100644 --- a/__tests__/default-export-declaration.test.ts +++ b/__tests__/default-export-declaration.test.ts @@ -273,6 +273,11 @@ export function check(s: string, ok: boolean) { return s } export default Main +`, + 'src/gen/multi.ts': `export function helper() { return 1; } +export default ( + value +) => value; `, 'src/gen/use.ts': `import Issue from "./issue" import Render from "./render" @@ -280,6 +285,7 @@ import Write from "./write" import Persisted from "./model" import Plain from "./plain" import Unbalanced from "./unbalanced" +import Multi from "./multi" export function generate() { Issue() @@ -288,6 +294,7 @@ export function generate() { new Persisted() Plain() Unbalanced() + Multi() } `, 'src/use.tsx': `import handler from "./api/vans" @@ -442,6 +449,10 @@ describe('an `export default` line inside a template literal', () => { expect(generated()).toContain('src/gen/model.ts:Model'); }); + it('binds a multiline anonymous arrow default to nothing, not to a function beside it', () => { + expect(generated()).not.toContain('src/gen/multi.ts:helper'); + }); + it('leaves regexes, strings and division alone, and a scan it cannot finish', () => { const targets = generated(); expect(targets).toContain('src/gen/plain.ts:Main'); diff --git a/__tests__/ruby-constructed-receivers.test.ts b/__tests__/ruby-constructed-receivers.test.ts index 93f86c568..55cd741c2 100644 --- a/__tests__/ruby-constructed-receivers.test.ts +++ b/__tests__/ruby-constructed-receivers.test.ts @@ -59,6 +59,18 @@ class OtherWriter false end end + +class ParentWriter + def flush + nil + end +end + +class ChildWriter < ParentWriter + def self.flush + nil + end +end `, 'app/billing.rb': `module Billing class Card @@ -120,6 +132,10 @@ end def outside(env) Request.new(env).preferred_type end + + def inherited_flush + ChildWriter.new.flush + end end `, 'lib/rack/show_exceptions.rb': `module Rack @@ -249,6 +265,13 @@ describe('Ruby bang and question methods, and calls on .new (#684)', () => { expect(callsAt(graph, 'app/counter.rb', 25)).toEqual(['Billing::Card::charge!:3']); }); + it('links an instance method a superclass declares before a class method its child defines', async () => { + const graph = await index(); + // `ChildWriter.new` is an instance: its own `self.flush` is a class + // method, so the call reaches `ParentWriter#flush` instead. + expect(callsAt(graph, 'app/counter.rb', 41)).toEqual(['ParentWriter::flush:38']); + }); + it('links nothing for a class the project does not declare', async () => { const graph = await index(); expect(callsAt(graph, 'app/counter.rb', 37)).toEqual([]); diff --git a/__tests__/ruby-ivar-receivers.test.ts b/__tests__/ruby-ivar-receivers.test.ts index dfd30813b..e28c097e1 100644 --- a/__tests__/ruby-ivar-receivers.test.ts +++ b/__tests__/ruby-ivar-receivers.test.ts @@ -85,6 +85,17 @@ module Mixin @writer.flush end end +`, + 'app/wide.rb': `class WideHolder + def initialize + @wide = CacheWriter.new + @label = "${'x'.repeat(184)}é" + end + + def via_wide + @wide.flush + end +end `, }; @@ -149,4 +160,11 @@ describe('Ruby calls on an instance variable initialize types', () => { expect(callsAt(graph, 'app/holder.rb', 32)).toEqual([]); expect(callsAt(graph, 'app/holder.rb', 38)).toEqual([]); }); + + it('still types an ivar when a multibyte character sits at the scan edge of its initializer', async () => { + const graph = await index(); + // `@wide`'s tail crosses byte 200 inside `é`; a byte-indexed slice would + // panic (caught per-ref) and leave the call unresolved. + expect(callsAt(graph, 'app/wide.rb', 8)).toEqual(['CacheWriter::flush:2']); + }); }); diff --git a/codegraph-kernel/src/resolve/query.rs b/codegraph-kernel/src/resolve/query.rs index 8adda04fb..fd41d19ed 100644 --- a/codegraph-kernel/src/resolve/query.rs +++ b/codegraph-kernel/src/resolve/query.rs @@ -599,8 +599,11 @@ impl KernelResolver { if !marker_lines.is_empty() { let mut anonymous = false; for &line in marker_lines.iter() { - let head = text.split('\n').nth(line.saturating_sub(1) as usize).unwrap_or(""); - if export_default_head_is_anonymous(head) { + // A default's head may span lines (`export default (\n x\n) => x`): + // upstream's readDefaultExport scans the whole statement, so the + // classifier gets the source from the marker's line to the end. + let head = text.split('\n').skip(line.saturating_sub(1) as usize).collect::>().join("\n"); + if export_default_head_is_anonymous(&head) { anonymous = true; } else { default_expr = true; diff --git a/codegraph-kernel/src/resolve/ruby_paths.rs b/codegraph-kernel/src/resolve/ruby_paths.rs index d8b4e7137..e58a68180 100644 --- a/codegraph-kernel/src/resolve/ruby_paths.rs +++ b/codegraph-kernel/src/resolve/ruby_paths.rs @@ -242,7 +242,10 @@ impl KernelResolver { let own: Vec> = self .nodes_by_qualified_name(&format!("{owner_qn}::{name}"))? .iter() - .filter(|n| n.language == "ruby" && n.kind == "method" && !(production && super::resolver_upstream::test_suite_path(&n.file_path))) + // A receiver bound to an instance (`new C().m`, a typed `@x`) can + // only reach instance methods: an own class method does not stand + // in for one an ancestor declares. + .filter(|n| n.language == "ruby" && n.kind == "method" && !(kind == "instance" && n.is_static) && !(production && super::resolver_upstream::test_suite_path(&n.file_path))) .cloned() .collect(); let own_pick = if kind == "value" { @@ -454,10 +457,10 @@ fn constructed_value(s: &str, from: usize) -> Option<(String, bool)> { let tail = &s[i.min(s.len())..]; let ok = match tail.chars().next() { None | Some('\n') | Some(';') | Some('{') => true, - _ => re!(r#"^[ \t]+(?:$|\n|;|\{|[\w:@$"'\[])"#).is_match(&tail[..tail.len().min(200)]), + _ => re!(r#"^[ \t]+(?:$|\n|;|\{|[\w:@$"'\[])"#).is_match(tail.get(..200).unwrap_or(tail)), }; // A chain may go on at the start of the next line: `Foo.new\n .tap {…}`. - let continued = re!(r"^[ \t]*\n\s*&?\.").is_match(&tail[..tail.len().min(200)]); + let continued = re!(r"^[ \t]*\n\s*&?\.").is_match(tail.get(..200).unwrap_or(tail)); (ok && !continued).then(|| (m[2].to_string(), m.get(1).is_some())) }