diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs
index f97470dbaed75..81daac7b90f0d 100644
--- a/src/librustdoc/html/render.rs
+++ b/src/librustdoc/html/render.rs
@@ -692,16 +692,23 @@ fn shortty(item: &clean::Item) -> ItemType {
 
 /// Takes a path to a source file and cleans the path to it. This canonicalizes
 /// things like ".." to components which preserve the "top down" hierarchy of a
-/// static HTML tree.
+/// static HTML tree. Each component in the cleaned path will be passed as an
+/// argument to `f`. The very last component of the path (ie the file name) will
+/// be passed to `f` if `keep_filename` is true, and ignored otherwise.
 // FIXME (#9639): The closure should deal with &[u8] instead of &str
 // FIXME (#9639): This is too conservative, rejecting non-UTF-8 paths
-fn clean_srcpath<F>(src_root: &Path, p: &Path, mut f: F) where
+fn clean_srcpath<F>(src_root: &Path, p: &Path, keep_filename: bool, mut f: F) where
     F: FnMut(&str),
 {
     // make it relative, if possible
     let p = p.relative_from(src_root).unwrap_or(p);
 
-    for c in p.iter().map(|x| x.to_str().unwrap()) {
+    let mut iter = p.iter().map(|x| x.to_str().unwrap()).peekable();
+    while let Some(c) = iter.next() {
+        if !keep_filename && iter.peek().is_none() {
+            break;
+        }
+
         if ".." == c {
             f("up");
         } else {
@@ -803,7 +810,7 @@ impl<'a> SourceCollector<'a> {
         // Create the intermediate directories
         let mut cur = self.dst.clone();
         let mut root_path = String::from_str("../../");
-        clean_srcpath(&self.cx.src_root, &p, |component| {
+        clean_srcpath(&self.cx.src_root, &p, false, |component| {
             cur.push(component);
             mkdir(&cur).unwrap();
             root_path.push_str("../");
@@ -1368,7 +1375,7 @@ impl<'a> Item<'a> {
         if ast_util::is_local(self.item.def_id) {
             let mut path = Vec::new();
             clean_srcpath(&cx.src_root, Path::new(&self.item.source.filename),
-                          |component| {
+                          true, |component| {
                 path.push(component.to_string());
             });
             let href = if self.item.source.loline == self.item.source.hiline {
diff --git a/src/test/run-make/rustdoc-src-links/Makefile b/src/test/run-make/rustdoc-src-links/Makefile
new file mode 100644
index 0000000000000..419603e82f715
--- /dev/null
+++ b/src/test/run-make/rustdoc-src-links/Makefile
@@ -0,0 +1,5 @@
+-include ../tools.mk
+all:
+	$(HOST_RPATH_ENV) $(RUSTDOC) -w html -o $(TMPDIR)/doc foo.rs
+	$(HTMLDOCCK) $(TMPDIR)/doc foo.rs
+	$(HTMLDOCCK) $(TMPDIR)/doc qux/mod.rs
diff --git a/src/test/run-make/rustdoc-src-links/foo.rs b/src/test/run-make/rustdoc-src-links/foo.rs
new file mode 100644
index 0000000000000..9a964f1125252
--- /dev/null
+++ b/src/test/run-make/rustdoc-src-links/foo.rs
@@ -0,0 +1,43 @@
+// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+#![crate_name = "foo"]
+
+//! Dox
+// @has src/foo/foo.rs.html
+// @has foo/index.html '//a/@href' '../src/foo/foo.rs.html'
+
+pub mod qux;
+
+// @has foo/bar/index.html '//a/@href' '../../src/foo/foo.rs.html'
+pub mod bar {
+
+    /// Dox
+    // @has foo/bar/baz/index.html '//a/@href' '../../../src/foo/foo.rs.html'
+    pub mod baz {
+        /// Dox
+        // @has foo/bar/baz/fn.baz.html '//a/@href' '../../../src/foo/foo.rs.html'
+        pub fn baz() { }
+    }
+
+    /// Dox
+    // @has foo/bar/trait.Foobar.html '//a/@href' '../../src/foo/foo.rs.html'
+    pub trait Foobar { fn dummy(&self) { } }
+
+    // @has foo/bar/struct.Foo.html '//a/@href' '../../src/foo/foo.rs.html'
+    pub struct Foo { x: i32, y: u32 }
+
+    // @has foo/bar/fn.prawns.html '//a/@href' '../../src/foo/foo.rs.html'
+    pub fn prawns((a, b): (i32, u32), Foo { x, y }: Foo) { }
+}
+
+/// Dox
+// @has foo/fn.modfn.html '//a/@href' '../src/foo/foo.rs.html'
+pub fn modfn() { }
diff --git a/src/test/run-make/rustdoc-src-links/qux/mod.rs b/src/test/run-make/rustdoc-src-links/qux/mod.rs
new file mode 100644
index 0000000000000..9b1563d32ac71
--- /dev/null
+++ b/src/test/run-make/rustdoc-src-links/qux/mod.rs
@@ -0,0 +1,39 @@
+// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Dox
+// @has src/foo/qux/mod.rs.html
+// @has foo/qux/index.html '//a/@href' '../../src/foo/qux/mod.rs.html'
+
+// @has foo/qux/bar/index.html '//a/@href' '../../../src/foo/qux/mod.rs.html'
+pub mod bar {
+
+    /// Dox
+    // @has foo/qux/bar/baz/index.html '//a/@href' '../../../../src/foo/qux/mod.rs.html'
+    pub mod baz {
+        /// Dox
+        // @has foo/qux/bar/baz/fn.baz.html '//a/@href' '../../../../src/foo/qux/mod.rs.html'
+        pub fn baz() { }
+    }
+
+    /// Dox
+    // @has foo/qux/bar/trait.Foobar.html '//a/@href' '../../../src/foo/qux/mod.rs.html'
+    pub trait Foobar { fn dummy(&self) { } }
+
+    // @has foo/qux/bar/struct.Foo.html '//a/@href' '../../../src/foo/qux/mod.rs.html'
+    pub struct Foo { x: i32, y: u32 }
+
+    // @has foo/qux/bar/fn.prawns.html '//a/@href' '../../../src/foo/qux/mod.rs.html'
+    pub fn prawns((a, b): (i32, u32), Foo { x, y }: Foo) { }
+}
+
+/// Dox
+// @has foo/qux/fn.modfn.html '//a/@href' '../../src/foo/qux/mod.rs.html'
+pub fn modfn() { }