Skip to content

Commit 3ec4b3b

Browse files
committedSep 3, 2023
Auto merge of #115436 - GuillaumeGomez:fix-type-based-search, r=notriddle
[rustdoc] Fix type based search Fixes #114522. The problem was a bit more tricky than I originally thought it would be: we only kept type ID and generics in short, but as soon as there was a full path in the user query, the element didn't get an ID anymore because the ID map didn't know about `x::y` (although it knew about `y`). So for this first problem, I instead always pass the element name to get the ID. Then a new problem occurred: we actually needed to check if paths matched, otherwise whatever the path, as long as the "end types" match, it's all good. meaning, we needed to add path information, but to do so, we needed it to be added into the search index directly as there was no mapping between `"p"` and `"q"`. I hope this explanation makes sense to someone else than me. ^^' r? `@notriddle`
2 parents b588641 + e161fa1 commit 3ec4b3b

File tree

5 files changed

+309
-135
lines changed

5 files changed

+309
-135
lines changed
 

‎src/librustdoc/html/render/search_index.rs

+151-106
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::collections::hash_map::Entry;
22
use std::collections::BTreeMap;
33

4-
use rustc_data_structures::fx::FxHashMap;
4+
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
55
use rustc_middle::ty::TyCtxt;
66
use rustc_span::symbol::Symbol;
7-
use serde::ser::{Serialize, SerializeStruct, Serializer};
7+
use serde::ser::{Serialize, SerializeSeq, SerializeStruct, Serializer};
88

99
use crate::clean;
1010
use crate::clean::types::{Function, Generics, ItemId, Type, WherePredicate};
@@ -78,17 +78,17 @@ pub(crate) fn build_index<'tcx>(
7878
map: &mut FxHashMap<F, usize>,
7979
itemid: F,
8080
lastpathid: &mut usize,
81-
crate_paths: &mut Vec<(ItemType, Symbol)>,
81+
crate_paths: &mut Vec<(ItemType, Vec<Symbol>)>,
8282
item_type: ItemType,
83-
path: Symbol,
83+
path: &[Symbol],
8484
) {
8585
match map.entry(itemid) {
8686
Entry::Occupied(entry) => ty.id = Some(RenderTypeId::Index(*entry.get())),
8787
Entry::Vacant(entry) => {
8888
let pathid = *lastpathid;
8989
entry.insert(pathid);
9090
*lastpathid += 1;
91-
crate_paths.push((item_type, path));
91+
crate_paths.push((item_type, path.to_vec()));
9292
ty.id = Some(RenderTypeId::Index(pathid));
9393
}
9494
}
@@ -100,7 +100,7 @@ pub(crate) fn build_index<'tcx>(
100100
itemid_to_pathid: &mut FxHashMap<ItemId, usize>,
101101
primitives: &mut FxHashMap<Symbol, usize>,
102102
lastpathid: &mut usize,
103-
crate_paths: &mut Vec<(ItemType, Symbol)>,
103+
crate_paths: &mut Vec<(ItemType, Vec<Symbol>)>,
104104
) {
105105
if let Some(generics) = &mut ty.generics {
106106
for item in generics {
@@ -131,7 +131,7 @@ pub(crate) fn build_index<'tcx>(
131131
lastpathid,
132132
crate_paths,
133133
item_type,
134-
*fqp.last().unwrap(),
134+
fqp,
135135
);
136136
} else {
137137
ty.id = None;
@@ -146,7 +146,7 @@ pub(crate) fn build_index<'tcx>(
146146
lastpathid,
147147
crate_paths,
148148
ItemType::Primitive,
149-
sym,
149+
&[sym],
150150
);
151151
}
152152
RenderTypeId::Index(_) => {}
@@ -191,7 +191,7 @@ pub(crate) fn build_index<'tcx>(
191191
lastpathid += 1;
192192

193193
if let Some(&(ref fqp, short)) = paths.get(&defid) {
194-
crate_paths.push((short, *fqp.last().unwrap()));
194+
crate_paths.push((short, fqp.clone()));
195195
Some(pathid)
196196
} else {
197197
None
@@ -213,118 +213,163 @@ pub(crate) fn build_index<'tcx>(
213213
struct CrateData<'a> {
214214
doc: String,
215215
items: Vec<&'a IndexItem>,
216-
paths: Vec<(ItemType, Symbol)>,
216+
paths: Vec<(ItemType, Vec<Symbol>)>,
217217
// The String is alias name and the vec is the list of the elements with this alias.
218218
//
219219
// To be noted: the `usize` elements are indexes to `items`.
220220
aliases: &'a BTreeMap<String, Vec<usize>>,
221221
}
222222

223+
struct Paths {
224+
ty: ItemType,
225+
name: Symbol,
226+
path: Option<usize>,
227+
}
228+
229+
impl Serialize for Paths {
230+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231+
where
232+
S: Serializer,
233+
{
234+
let mut seq = serializer.serialize_seq(None)?;
235+
seq.serialize_element(&self.ty)?;
236+
seq.serialize_element(self.name.as_str())?;
237+
if let Some(ref path) = self.path {
238+
seq.serialize_element(path)?;
239+
}
240+
seq.end()
241+
}
242+
}
243+
223244
impl<'a> Serialize for CrateData<'a> {
224245
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
225246
where
226247
S: Serializer,
227248
{
249+
let mut extra_paths = FxHashMap::default();
250+
// We need to keep the order of insertion, hence why we use an `IndexMap`. Then we will
251+
// insert these "extra paths" (which are paths of items from external crates) into the
252+
// `full_paths` list at the end.
253+
let mut revert_extra_paths = FxIndexMap::default();
254+
let mut mod_paths = FxHashMap::default();
255+
for (index, item) in self.items.iter().enumerate() {
256+
if item.path.is_empty() {
257+
continue;
258+
}
259+
mod_paths.insert(&item.path, index);
260+
}
261+
let mut paths = Vec::with_capacity(self.paths.len());
262+
for (ty, path) in &self.paths {
263+
if path.len() < 2 {
264+
paths.push(Paths { ty: *ty, name: path[0], path: None });
265+
continue;
266+
}
267+
let full_path = join_with_double_colon(&path[..path.len() - 1]);
268+
if let Some(index) = mod_paths.get(&full_path) {
269+
paths.push(Paths { ty: *ty, name: *path.last().unwrap(), path: Some(*index) });
270+
continue;
271+
}
272+
// It means it comes from an external crate so the item and its path will be
273+
// stored into another array.
274+
//
275+
// `index` is put after the last `mod_paths`
276+
let index = extra_paths.len() + self.items.len();
277+
if !revert_extra_paths.contains_key(&index) {
278+
revert_extra_paths.insert(index, full_path.clone());
279+
}
280+
match extra_paths.entry(full_path) {
281+
Entry::Occupied(entry) => {
282+
paths.push(Paths {
283+
ty: *ty,
284+
name: *path.last().unwrap(),
285+
path: Some(*entry.get()),
286+
});
287+
}
288+
Entry::Vacant(entry) => {
289+
entry.insert(index);
290+
paths.push(Paths {
291+
ty: *ty,
292+
name: *path.last().unwrap(),
293+
path: Some(index),
294+
});
295+
}
296+
}
297+
}
298+
299+
let mut names = Vec::with_capacity(self.items.len());
300+
let mut types = String::with_capacity(self.items.len());
301+
let mut full_paths = Vec::with_capacity(self.items.len());
302+
let mut descriptions = Vec::with_capacity(self.items.len());
303+
let mut parents = Vec::with_capacity(self.items.len());
304+
let mut functions = Vec::with_capacity(self.items.len());
305+
let mut deprecated = Vec::with_capacity(self.items.len());
306+
307+
for (index, item) in self.items.iter().enumerate() {
308+
let n = item.ty as u8;
309+
let c = char::try_from(n + b'A').expect("item types must fit in ASCII");
310+
assert!(c <= 'z', "item types must fit within ASCII printables");
311+
types.push(c);
312+
313+
assert_eq!(
314+
item.parent.is_some(),
315+
item.parent_idx.is_some(),
316+
"`{}` is missing idx",
317+
item.name
318+
);
319+
// 0 is a sentinel, everything else is one-indexed
320+
parents.push(item.parent_idx.map(|x| x + 1).unwrap_or(0));
321+
322+
names.push(item.name.as_str());
323+
descriptions.push(&item.desc);
324+
325+
if !item.path.is_empty() {
326+
full_paths.push((index, &item.path));
327+
}
328+
329+
// Fake option to get `0` out as a sentinel instead of `null`.
330+
// We want to use `0` because it's three less bytes.
331+
enum FunctionOption<'a> {
332+
Function(&'a IndexItemFunctionType),
333+
None,
334+
}
335+
impl<'a> Serialize for FunctionOption<'a> {
336+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
337+
where
338+
S: Serializer,
339+
{
340+
match self {
341+
FunctionOption::None => 0.serialize(serializer),
342+
FunctionOption::Function(ty) => ty.serialize(serializer),
343+
}
344+
}
345+
}
346+
functions.push(match &item.search_type {
347+
Some(ty) => FunctionOption::Function(ty),
348+
None => FunctionOption::None,
349+
});
350+
351+
if item.deprecation.is_some() {
352+
deprecated.push(index);
353+
}
354+
}
355+
356+
for (index, path) in &revert_extra_paths {
357+
full_paths.push((*index, path));
358+
}
359+
228360
let has_aliases = !self.aliases.is_empty();
229361
let mut crate_data =
230362
serializer.serialize_struct("CrateData", if has_aliases { 9 } else { 8 })?;
231363
crate_data.serialize_field("doc", &self.doc)?;
232-
crate_data.serialize_field(
233-
"t",
234-
&self
235-
.items
236-
.iter()
237-
.map(|item| {
238-
let n = item.ty as u8;
239-
let c = char::try_from(n + b'A').expect("item types must fit in ASCII");
240-
assert!(c <= 'z', "item types must fit within ASCII printables");
241-
c
242-
})
243-
.collect::<String>(),
244-
)?;
245-
crate_data.serialize_field(
246-
"n",
247-
&self.items.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
248-
)?;
249-
crate_data.serialize_field(
250-
"q",
251-
&self
252-
.items
253-
.iter()
254-
.enumerate()
255-
// Serialize as an array of item indices and full paths
256-
.filter_map(
257-
|(index, item)| {
258-
if item.path.is_empty() { None } else { Some((index, &item.path)) }
259-
},
260-
)
261-
.collect::<Vec<_>>(),
262-
)?;
263-
crate_data.serialize_field(
264-
"d",
265-
&self.items.iter().map(|item| &item.desc).collect::<Vec<_>>(),
266-
)?;
267-
crate_data.serialize_field(
268-
"i",
269-
&self
270-
.items
271-
.iter()
272-
.map(|item| {
273-
assert_eq!(
274-
item.parent.is_some(),
275-
item.parent_idx.is_some(),
276-
"`{}` is missing idx",
277-
item.name
278-
);
279-
// 0 is a sentinel, everything else is one-indexed
280-
item.parent_idx.map(|x| x + 1).unwrap_or(0)
281-
})
282-
.collect::<Vec<_>>(),
283-
)?;
284-
crate_data.serialize_field(
285-
"f",
286-
&self
287-
.items
288-
.iter()
289-
.map(|item| {
290-
// Fake option to get `0` out as a sentinel instead of `null`.
291-
// We want to use `0` because it's three less bytes.
292-
enum FunctionOption<'a> {
293-
Function(&'a IndexItemFunctionType),
294-
None,
295-
}
296-
impl<'a> Serialize for FunctionOption<'a> {
297-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
298-
where
299-
S: Serializer,
300-
{
301-
match self {
302-
FunctionOption::None => 0.serialize(serializer),
303-
FunctionOption::Function(ty) => ty.serialize(serializer),
304-
}
305-
}
306-
}
307-
match &item.search_type {
308-
Some(ty) => FunctionOption::Function(ty),
309-
None => FunctionOption::None,
310-
}
311-
})
312-
.collect::<Vec<_>>(),
313-
)?;
314-
crate_data.serialize_field(
315-
"c",
316-
&self
317-
.items
318-
.iter()
319-
.enumerate()
320-
// Serialize as an array of deprecated item indices
321-
.filter_map(|(index, item)| item.deprecation.map(|_| index))
322-
.collect::<Vec<_>>(),
323-
)?;
324-
crate_data.serialize_field(
325-
"p",
326-
&self.paths.iter().map(|(it, s)| (it, s.as_str())).collect::<Vec<_>>(),
327-
)?;
364+
crate_data.serialize_field("t", &types)?;
365+
crate_data.serialize_field("n", &names)?;
366+
// Serialize as an array of item indices and full paths
367+
crate_data.serialize_field("q", &full_paths)?;
368+
crate_data.serialize_field("d", &descriptions)?;
369+
crate_data.serialize_field("i", &parents)?;
370+
crate_data.serialize_field("f", &functions)?;
371+
crate_data.serialize_field("c", &deprecated)?;
372+
crate_data.serialize_field("p", &paths)?;
328373
if has_aliases {
329374
crate_data.serialize_field("a", &self.aliases)?;
330375
}

‎src/librustdoc/html/static/js/search.js

+91-29
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,6 @@ function initSearch(rawSearchIndex) {
263263
* @returns {integer}
264264
*/
265265
function buildTypeMapIndex(name) {
266-
267266
if (name === "" || name === null) {
268267
return -1;
269268
}
@@ -1380,7 +1379,7 @@ function initSearch(rawSearchIndex) {
13801379
* @type Map<integer, QueryElement[]>
13811380
*/
13821381
const queryElemSet = new Map();
1383-
const addQueryElemToQueryElemSet = function addQueryElemToQueryElemSet(queryElem) {
1382+
const addQueryElemToQueryElemSet = queryElem => {
13841383
let currentQueryElemList;
13851384
if (queryElemSet.has(queryElem.id)) {
13861385
currentQueryElemList = queryElemSet.get(queryElem.id);
@@ -1397,7 +1396,7 @@ function initSearch(rawSearchIndex) {
13971396
* @type Map<integer, FunctionType[]>
13981397
*/
13991398
const fnTypeSet = new Map();
1400-
const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) {
1399+
const addFnTypeToFnTypeSet = fnType => {
14011400
// Pure generic, or an item that's not matched by any query elems.
14021401
// Try [unboxing] it.
14031402
//
@@ -1463,6 +1462,32 @@ function initSearch(rawSearchIndex) {
14631462
if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) {
14641463
continue;
14651464
}
1465+
const queryElemPathLength = queryElem.pathWithoutLast.length;
1466+
// If the query element is a path (it contains `::`), we need to check if this
1467+
// path is compatible with the target type.
1468+
if (queryElemPathLength > 0) {
1469+
const fnTypePath = fnType.path !== undefined && fnType.path !== null ?
1470+
fnType.path.split("::") : [];
1471+
// If the path provided in the query element is longer than this type,
1472+
// no need to check it since it won't match in any case.
1473+
if (queryElemPathLength > fnTypePath.length) {
1474+
continue;
1475+
}
1476+
let i = 0;
1477+
for (const path of fnTypePath) {
1478+
if (path === queryElem.pathWithoutLast[i]) {
1479+
i += 1;
1480+
if (i >= queryElemPathLength) {
1481+
break;
1482+
}
1483+
}
1484+
}
1485+
if (i < queryElemPathLength) {
1486+
// If we didn't find all parts of the path of the query element inside
1487+
// the fn type, then it's not the right one.
1488+
continue;
1489+
}
1490+
}
14661491
if (queryElem.generics.length === 0 || checkGenerics(fnType, queryElem)) {
14671492
currentFnTypeList.splice(i, 1);
14681493
const result = doHandleQueryElemList(currentFnTypeList, queryElemList);
@@ -1863,14 +1888,14 @@ function initSearch(rawSearchIndex) {
18631888
* @param {QueryElement} elem
18641889
*/
18651890
function convertNameToId(elem) {
1866-
if (typeNameIdMap.has(elem.name)) {
1867-
elem.id = typeNameIdMap.get(elem.name);
1891+
if (typeNameIdMap.has(elem.pathLast)) {
1892+
elem.id = typeNameIdMap.get(elem.pathLast);
18681893
} else if (!parsedQuery.literalSearch) {
18691894
let match = -1;
18701895
let matchDist = maxEditDistance + 1;
18711896
let matchName = "";
18721897
for (const [name, id] of typeNameIdMap) {
1873-
const dist = editDistance(name, elem.name, maxEditDistance);
1898+
const dist = editDistance(name, elem.pathLast, maxEditDistance);
18741899
if (dist <= matchDist && dist <= maxEditDistance) {
18751900
if (dist === matchDist && matchName > name) {
18761901
continue;
@@ -2385,12 +2410,20 @@ ${item.displayPath}<span class="${type}">${name}</span>\
23852410
lowercasePaths
23862411
);
23872412
}
2413+
// `0` is used as a sentinel because it's fewer bytes than `null`
2414+
if (pathIndex === 0) {
2415+
return {
2416+
id: -1,
2417+
ty: null,
2418+
path: null,
2419+
generics: generics,
2420+
};
2421+
}
2422+
const item = lowercasePaths[pathIndex - 1];
23882423
return {
2389-
// `0` is used as a sentinel because it's fewer bytes than `null`
2390-
id: pathIndex === 0
2391-
? -1
2392-
: buildTypeMapIndex(lowercasePaths[pathIndex - 1].name),
2393-
ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
2424+
id: buildTypeMapIndex(item.name),
2425+
ty: item.ty,
2426+
path: item.path,
23942427
generics: generics,
23952428
};
23962429
});
@@ -2422,13 +2455,22 @@ ${item.displayPath}<span class="${type}">${name}</span>\
24222455
let inputs, output;
24232456
if (typeof functionSearchType[INPUTS_DATA] === "number") {
24242457
const pathIndex = functionSearchType[INPUTS_DATA];
2425-
inputs = [{
2426-
id: pathIndex === 0
2427-
? -1
2428-
: buildTypeMapIndex(lowercasePaths[pathIndex - 1].name),
2429-
ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
2430-
generics: [],
2431-
}];
2458+
if (pathIndex === 0) {
2459+
inputs = [{
2460+
id: -1,
2461+
ty: null,
2462+
path: null,
2463+
generics: [],
2464+
}];
2465+
} else {
2466+
const item = lowercasePaths[pathIndex - 1];
2467+
inputs = [{
2468+
id: buildTypeMapIndex(item.name),
2469+
ty: item.ty,
2470+
path: item.path,
2471+
generics: [],
2472+
}];
2473+
}
24322474
} else {
24332475
inputs = buildItemSearchTypeAll(
24342476
functionSearchType[INPUTS_DATA],
@@ -2438,13 +2480,22 @@ ${item.displayPath}<span class="${type}">${name}</span>\
24382480
if (functionSearchType.length > 1) {
24392481
if (typeof functionSearchType[OUTPUT_DATA] === "number") {
24402482
const pathIndex = functionSearchType[OUTPUT_DATA];
2441-
output = [{
2442-
id: pathIndex === 0
2443-
? -1
2444-
: buildTypeMapIndex(lowercasePaths[pathIndex - 1].name),
2445-
ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
2446-
generics: [],
2447-
}];
2483+
if (pathIndex === 0) {
2484+
output = [{
2485+
id: -1,
2486+
ty: null,
2487+
path: null,
2488+
generics: [],
2489+
}];
2490+
} else {
2491+
const item = lowercasePaths[pathIndex - 1];
2492+
output = [{
2493+
id: buildTypeMapIndex(item.name),
2494+
ty: item.ty,
2495+
path: item.path,
2496+
generics: [],
2497+
}];
2498+
}
24482499
} else {
24492500
output = buildItemSearchTypeAll(
24502501
functionSearchType[OUTPUT_DATA],
@@ -2577,9 +2628,19 @@ ${item.displayPath}<span class="${type}">${name}</span>\
25772628
// convert `rawPaths` entries into object form
25782629
// generate normalizedPaths for function search mode
25792630
let len = paths.length;
2631+
let lastPath = itemPaths.get(0);
25802632
for (let i = 0; i < len; ++i) {
2581-
lowercasePaths.push({ty: paths[i][0], name: paths[i][1].toLowerCase()});
2582-
paths[i] = {ty: paths[i][0], name: paths[i][1]};
2633+
const elem = paths[i];
2634+
const ty = elem[0];
2635+
const name = elem[1];
2636+
let path = null;
2637+
if (elem.length > 2) {
2638+
path = itemPaths.has(elem[2]) ? itemPaths.get(elem[2]) : lastPath;
2639+
lastPath = path;
2640+
}
2641+
2642+
lowercasePaths.push({ty: ty, name: name.toLowerCase(), path: path});
2643+
paths[i] = {ty: ty, name: name, path: path};
25832644
}
25842645

25852646
// convert `item*` into an object form, and construct word indices.
@@ -2589,8 +2650,8 @@ ${item.displayPath}<span class="${type}">${name}</span>\
25892650
// operation that is cached for the life of the page state so that
25902651
// all other search operations have access to this cached data for
25912652
// faster analysis operations
2653+
lastPath = "";
25922654
len = itemTypes.length;
2593-
let lastPath = "";
25942655
for (let i = 0; i < len; ++i) {
25952656
let word = "";
25962657
// This object should have exactly the same set of fields as the "crateRow"
@@ -2599,11 +2660,12 @@ ${item.displayPath}<span class="${type}">${name}</span>\
25992660
word = itemNames[i].toLowerCase();
26002661
}
26012662
searchWords.push(word);
2663+
const path = itemPaths.has(i) ? itemPaths.get(i) : lastPath;
26022664
const row = {
26032665
crate: crate,
26042666
ty: itemTypes.charCodeAt(i) - charA,
26052667
name: itemNames[i],
2606-
path: itemPaths.has(i) ? itemPaths.get(i) : lastPath,
2668+
path: path,
26072669
desc: itemDescs[i],
26082670
parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined,
26092671
type: buildFunctionSearchType(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const EXPECTED = {
2+
'query': 'vec::vec -> usize',
3+
'others': [
4+
{ 'path': 'std::vec::Vec', 'name': 'len' },
5+
{ 'path': 'std::vec::Vec', 'name': 'capacity' },
6+
],
7+
};
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// exact-check
2+
3+
const EXPECTED = [
4+
{
5+
'query': 'sac -> usize',
6+
'others': [
7+
{ 'path': 'full_path_function::b::Sac', 'name': 'bar' },
8+
{ 'path': 'full_path_function::b::Sac', 'name': 'len' },
9+
{ 'path': 'full_path_function::sac::Sac', 'name': 'len' },
10+
],
11+
},
12+
{
13+
'query': 'b::sac -> usize',
14+
'others': [
15+
{ 'path': 'full_path_function::b::Sac', 'name': 'bar' },
16+
{ 'path': 'full_path_function::b::Sac', 'name': 'len' },
17+
],
18+
},
19+
{
20+
'query': 'b::sac -> u32',
21+
'others': [
22+
{ 'path': 'full_path_function::b::Sac', 'name': 'bar2' },
23+
],
24+
},
25+
{
26+
'query': 'string::string -> u32',
27+
'others': [
28+
{ 'path': 'full_path_function::b::Sac', 'name': 'string' },
29+
],
30+
},
31+
{
32+
'query': 'alloc::string::string -> u32',
33+
'others': [
34+
{ 'path': 'full_path_function::b::Sac', 'name': 'string' },
35+
],
36+
},
37+
{
38+
'query': 'alloc::string -> u32',
39+
'others': [
40+
{ 'path': 'full_path_function::b::Sac', 'name': 'string' },
41+
],
42+
},
43+
];
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
pub mod sac {
2+
pub struct Sac;
3+
4+
impl Sac {
5+
pub fn len(&self) -> usize { 0 }
6+
}
7+
}
8+
9+
pub mod b {
10+
pub struct Sac;
11+
impl Sac {
12+
pub fn len(&self) -> usize { 0 }
13+
pub fn bar(&self, w: u32) -> usize { 0 }
14+
pub fn bar2(&self, w: u32) -> u32 { 0 }
15+
pub fn string(w: String) -> u32 { 0 }
16+
}
17+
}

0 commit comments

Comments
 (0)
Please sign in to comment.